diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 866af3dbac2..cee977e6688 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -434,7 +434,7 @@ jobs: tar -xJf "${{ needs.build.outputs.viewer_channel }}.sym.tar.xz" -C _artifacts - name: Post Windows symbols if: env.BUGSPLAT_DATABASE && env.SYMBOL_UPLOAD_CLIENT_ID - uses: BugSplat-Git/symbol-upload@095d163ae9ceb006d286a731dcd35cf6a1b458c8 + uses: BugSplat-Git/symbol-upload@33f604b631df9b4be22a06c52e46acccd2db596b with: clientId: "${{ env.SYMBOL_UPLOAD_CLIENT_ID }}" clientSecret: "${{ env.SYMBOL_UPLOAD_CLIENT_SECRET }}" @@ -462,7 +462,7 @@ jobs: name: macOS-symbols - name: Post Mac symbols if: env.BUGSPLAT_DATABASE && env.SYMBOL_UPLOAD_CLIENT_ID - uses: BugSplat-Git/symbol-upload@095d163ae9ceb006d286a731dcd35cf6a1b458c8 + uses: BugSplat-Git/symbol-upload@33f604b631df9b4be22a06c52e46acccd2db596b with: clientId: "${{ env.SYMBOL_UPLOAD_CLIENT_ID }}" clientSecret: "${{ env.SYMBOL_UPLOAD_CLIENT_SECRET }}" @@ -487,7 +487,7 @@ jobs: with: pattern: "*-metadata" - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: pattern: "*-releases" diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 08e907e83f5..68ca8244c09 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check PR description - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const description = context.payload.pull_request.body || ''; diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml index 0f826222a05..f1afb0f1a29 100644 --- a/.github/workflows/tag-release.yaml +++ b/.github/workflows/tag-release.yaml @@ -37,7 +37,7 @@ jobs: echo NIGHTLY_DATE=${NIGHTLY_DATE} >> ${GITHUB_ENV} echo TAG_ID="$(echo ${{ github.sha }} | cut -c1-8)-${{ inputs.project || '${NIGHTLY_DATE}' }}" >> ${GITHUB_ENV} - name: Create Tag - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: # use a real access token instead of GITHUB_TOKEN default. # required so that the results of this tag creation can trigger the build workflow diff --git a/.gitignore b/.gitignore index c4accf37b5b..62a6b462f93 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ *~ # Specific paths and/or names +/.autobuild-installables/ +/.build-variables/ +/.logs/ CMakeCache.txt cmake_install.cmake LICENSES diff --git a/autobuild.xml b/autobuild.xml index 3a2cfa0b467..351803cfee8 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2019,11 +2019,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 94a72c6ddbfb23796ce913c55bc47c128542a582 + 8dc190451c5b7af0d72e7ab54f8fbde6dccf79c6 hash_algorithm sha1 url - https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.3-r1/openjpeg-2.5.3.15590356935-darwin64-15590356935.tar.zst + https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.4-r1/openjpeg-2.5.4.18754730947-darwin64-18754730947.tar.zst name darwin64 @@ -2033,11 +2033,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 751172af405f4a47a3aebb37729d62229cab6c07 + 9c89879f81ee0434e1f59c47d74a25958ae08e9e hash_algorithm sha1 url - https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.3-r1/openjpeg-2.5.3.15590356935-linux64-15590356935.tar.zst + https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.4-r1/openjpeg-2.5.4.18754730947-linux64-18754730947.tar.zst name linux64 @@ -2047,11 +2047,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 8aab9cf250dfee252386e1c79b5205e6d3b3e19e + b78887212f18ae59dc7961e0e1af781e568bd92c hash_algorithm sha1 url - https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.3-r1/openjpeg-2.5.3.15590356935-windows64-15590356935.tar.zst + https://github.com/secondlife/3p-openjpeg/releases/download/v2.5.4-r1/openjpeg-2.5.4.18754730947-windows64-18754730947.tar.zst name windows64 @@ -2401,11 +2401,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - e88a7c97a6843d43e0093388f211299ec2892790 + d1584b3a0011dbb741bc64d32e8bd28845ddd4da hash_algorithm sha1 url - https://github.com/secondlife/3p-viewer-fonts/releases/download/v1.1.0-r1/viewer_fonts-1.0.0.10204976553-common-10204976553.tar.zst + https://github.com/secondlife/3p-viewer-fonts/releases/download/v1.1.0-r2/viewer_fonts-1.0.0.25128287087-common-25128287087.tar.zst name common @@ -2416,9 +2416,9 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors license_file LICENSES/fonts.txt copyright - Copyright 2016-2022 Brad Erickson CC-BY-4.0/MIT, Copyright 2016-2022 Twitter, Inc. CC-BY-4.0, Copyright 2013 Joe Loughry and Terence Eden MIT + Copyright 2016-2022 Brad Erickson CC-BY-4.0/MIT, Copyright 2016-2022 Twitter, Inc. CC-BY-4.0, Copyright 2013 Joe Loughry and Terence Eden MIT, Copyright (c) 2003 by Bitstream, Inc., Copyright (c) 2006 by Tavmjong Bah. version - 1.0.0.10204976553 + 1.0.0.25128287087 name viewer-fonts description @@ -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 @@ -2896,14 +2896,12 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive - creds - github hash - 91abbc360640b5b2e0a4c001a36ad411a9a42602 + df2260187110aa51c20101183e18a55f86e71090 hash_algorithm sha1 url - https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583560 + https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-windows64-24685318450.tar.zst name windows64 @@ -2912,14 +2910,12 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive - creds - github hash - 05563a79bdeb83d66a72ac1e97587dc2a8f64511 + ead94386d7b9a143824d7ccedc86433127028a91 hash_algorithm sha1 url - https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583554 + https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-darwin64-24685318450.tar.zst name darwin64 @@ -2932,7 +2928,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors copyright Velopack Ltd. version - 40232ef.23500976684 + 40232ef.24685318450 name velopack description @@ -3390,7 +3386,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors license_file docs/LICENSE-source.txt copyright - Copyright (c) 2020, Linden Research, Inc. + Copyright (c) 2026, Linden Research, Inc. version_file newview/viewer_version.txt name diff --git a/indra/cmake/00-Common.cmake b/indra/cmake/00-Common.cmake index 99ea22ab4b6..72347446b92 100644 --- a/indra/cmake/00-Common.cmake +++ b/indra/cmake/00-Common.cmake @@ -18,6 +18,17 @@ include(Variables) # We go to some trouble to set LL_BUILD to the set of relevant compiler flags. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} $ENV{LL_BUILD}") + +if (DARWIN) + # LL_BUILD carries a literal -mmacosx-version-min= from + # viewer-build-variables. We derive the macOS deployment target in + # Variables.cmake (clamping up to the SDK's supported minimum when needed) + # and let CMAKE_OSX_DEPLOYMENT_TARGET emit the flag to both compiler and + # linker. Strip the verbatim flag here so it can't conflict with the + # clamped deployment target. + string(REGEX REPLACE "-mmacosx-version-min=[0-9.]+" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +endif (DARWIN) + # Given that, all the flags you see added below are flags NOT present in # https://bitbucket.org/lindenlab/viewer-build-variables/src/tip/variables. # Before adding new ones here, it's important to ask: can this flag really be diff --git a/indra/cmake/Linking.cmake b/indra/cmake/Linking.cmake index 900a64e2dd4..5d4e8d86348 100644 --- a/indra/cmake/Linking.cmake +++ b/indra/cmake/Linking.cmake @@ -77,6 +77,7 @@ else() find_library(COREAUDIO_LIBRARY CoreAudio) find_library(COREGRAPHICS_LIBRARY CoreGraphics) find_library(AUDIOTOOLBOX_LIBRARY AudioToolbox) + find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers) target_link_libraries( ll::oslibraries INTERFACE ${COCOA_LIBRARY} @@ -87,6 +88,7 @@ else() ${COREAUDIO_LIBRARY} ${AUDIOTOOLBOX_LIBRARY} ${COREGRAPHICS_LIBRARY} + ${UNIFORMTYPEIDENTIFIERS_LIBRARY} ) endif() diff --git a/indra/cmake/Variables.cmake b/indra/cmake/Variables.cmake index 22c2156bb88..baa93bd128a 100644 --- a/indra/cmake/Variables.cmake +++ b/indra/cmake/Variables.cmake @@ -144,7 +144,38 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set(DARWIN 1) string(REGEX MATCH "-mmacosx-version-min=([^ ]+)" scratch "$ENV{LL_BUILD}") - set(CMAKE_OSX_DEPLOYMENT_TARGET "${CMAKE_MATCH_1}" CACHE STRING "macOS Deploy Target" FORCE) + set(LL_REQUESTED_DEPLOYMENT_TARGET "${CMAKE_MATCH_1}") + + # Determine the lowest deployment target the active macOS SDK still supports. + # We aim for 11.0 in our public builds, but newer Xcode/SDK releases + # periodically raise this floor (e.g. Xcode 26 -> 13.3, Xcode 27 -> 14), and + # linking against a deployment target below the SDK's minimum fails. Read the + # supported minimum from the SDK and clamp our requested target up to it when + # necessary, so the build tracks whatever the SDK allows automatically. + set(LL_SDK_MINIMUM_DEPLOYMENT_TARGET "") + execute_process( + COMMAND xcrun --sdk macosx --show-sdk-path + OUTPUT_VARIABLE LL_MACOS_SDK_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if (LL_MACOS_SDK_PATH AND EXISTS "${LL_MACOS_SDK_PATH}/SDKSettings.plist") + execute_process( + COMMAND /usr/libexec/PlistBuddy -c + "Print :SupportedTargets:macosx:MinimumDeploymentTarget" + "${LL_MACOS_SDK_PATH}/SDKSettings.plist" + OUTPUT_VARIABLE LL_SDK_MINIMUM_DEPLOYMENT_TARGET + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + endif () + + set(LL_EFFECTIVE_DEPLOYMENT_TARGET "${LL_REQUESTED_DEPLOYMENT_TARGET}") + if (LL_SDK_MINIMUM_DEPLOYMENT_TARGET AND + LL_REQUESTED_DEPLOYMENT_TARGET VERSION_LESS LL_SDK_MINIMUM_DEPLOYMENT_TARGET) + message(STATUS "Requested macOS deploy target ${LL_REQUESTED_DEPLOYMENT_TARGET} is below the SDK minimum ${LL_SDK_MINIMUM_DEPLOYMENT_TARGET}; clamping to ${LL_SDK_MINIMUM_DEPLOYMENT_TARGET}") + set(LL_EFFECTIVE_DEPLOYMENT_TARGET "${LL_SDK_MINIMUM_DEPLOYMENT_TARGET}") + endif () + + set(CMAKE_OSX_DEPLOYMENT_TARGET "${LL_EFFECTIVE_DEPLOYMENT_TARGET}" CACHE STRING "macOS Deploy Target" FORCE) message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET = '${CMAKE_OSX_DEPLOYMENT_TARGET}'") # Use dwarf symbols for most libraries for compilation speed diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index dab18c240d5..c8fd2cba70c 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -1647,10 +1647,10 @@ glm::mat4 LLAvatarBoneInfo::getJointMatrix() glm::mat4 mat(1.0f); // 1. Scaling mat = glm::scale(mat, glm::vec3(mScale[0], mScale[1], mScale[2])); - // 2. Rotation (Euler angles rad) - mat = glm::rotate(mat, mRot[0], glm::vec3(1, 0, 0)); - mat = glm::rotate(mat, mRot[1], glm::vec3(0, 1, 0)); - mat = glm::rotate(mat, mRot[2], glm::vec3(0, 0, 1)); + // 2. Rotation (avatar_skeleton.xml stores Euler angles in degrees) + mat = glm::rotate(mat, glm::radians(mRot[0]), glm::vec3(1, 0, 0)); + mat = glm::rotate(mat, glm::radians(mRot[1]), glm::vec3(0, 1, 0)); + mat = glm::rotate(mat, glm::radians(mRot[2]), glm::vec3(0, 0, 1)); // 3. Position mat = glm::translate(mat, glm::vec3(mPos[0], mPos[1], mPos[2])); return mat; @@ -1698,6 +1698,7 @@ void LLAvatarSkeletonInfo::getJointMatricesAndHierarhy( data.mRestMatrix = parent_mat * data.mJointMatrix; data.mIsJoint = bone_info->mIsJoint; data.mGroup = bone_info->mGroup; + data.setSupport(bone_info->mSupport); for (LLAvatarBoneInfo* child_info : bone_info->mChildren) { LLJointData& child_data = data.mChildren.emplace_back(); diff --git a/indra/llcommon/llerror.h b/indra/llcommon/llerror.h index 41893a35e56..0083865cf7e 100644 --- a/indra/llcommon/llerror.h +++ b/indra/llcommon/llerror.h @@ -314,6 +314,7 @@ namespace LLError ERROR_OTHER = 0, ERROR_BAD_ALLOC = 1, ERROR_MISSING_FILES = 2, + ERROR_INIT_FAILED = 3, } eLastExecEvent; // tittle, message and error code to include in error marker file diff --git a/indra/llcommon/llsdserialize_xml.cpp b/indra/llcommon/llsdserialize_xml.cpp index f399c51608e..af79fabe81c 100644 --- a/indra/llcommon/llsdserialize_xml.cpp +++ b/indra/llcommon/llsdserialize_xml.cpp @@ -63,15 +63,80 @@ S32 LLSDXMLFormatter::format(const LLSD& data, std::ostream& ostr, EFormatterOptions options) const { std::streamsize old_precision = ostr.precision(25); + std::ios_base::iostate old_exceptions = ostr.exceptions(); + // Merged exception mask: preserve the caller's bits and add failbit|badbit + // for I/O error detection, so we never drop bits the caller already enabled. + std::ios_base::iostate new_exceptions = + old_exceptions | std::ios_base::badbit | std::ios_base::failbit; + // Bits we are newly adding (not already in the caller's mask). + std::ios_base::iostate added_bits = new_exceptions & ~old_exceptions; + + // If the stream already has error-state bits that we would newly add to the + // exception mask, enabling those bits would throw immediately; bail out early. + if (added_bits && (ostr.rdstate() & added_bits)) + { + LL_WARNS() << "LLSDXMLFormatter::format: Stream already in error state" << LL_ENDL; + ostr.precision(old_precision); + return -1; + } - std::string post; - if (options & LLSDFormatter::OPTIONS_PRETTY) + S32 rv = 0; + + try { - post = "\n"; + // Enable the merged exception mask to detect I/O errors during formatting. + if (added_bits) + { + ostr.exceptions(new_exceptions); + } + + std::string post; + if (options & LLSDFormatter::OPTIONS_PRETTY) + { + post = "\n"; + } + ostr << "" << post; + rv = format_impl(data, ostr, options, 1); + ostr << "\n"; + } + catch (const std::ios_base::failure& e) + { + LL_WARNS() << "LLSDXMLFormatter::format: Stream I/O exception: " << e.what() + << " - Stream state: good=" << ostr.good() + << " eof=" << ostr.eof() + << " fail=" << ostr.fail() + << " bad=" << ostr.bad() << LL_ENDL; + rv = -1; + } + catch (const std::bad_alloc&) + { + // we might be saving something massive, don't error or crash + LL_WARNS() << "LLSDXMLFormatter::format: Memory allocation failed during formatting" << LL_ENDL; + rv = -1; + } + catch (const std::exception& e) + { + LL_WARNS() << "LLSDXMLFormatter::format: Standard exception: " << e.what() << LL_ENDL; + rv = -1; + } + catch (...) + { + LL_WARNS() << "LLSDXMLFormatter::format: Unknown exception during formatting" << LL_ENDL; + rv = -1; + } + + // Restore original exception mask. First set to goodbit (never throws) so + // the subsequent restore call won't immediately throw if the stream is in + // error state for bits in old_exceptions. + try + { + ostr.exceptions(std::ios_base::goodbit); + ostr.exceptions(old_exceptions); + } + catch (...) + { + LL_WARNS() << "LLSDXMLFormatter::format: failed to restore exceptions" << LL_ENDL; } - ostr << "" << post; - S32 rv = format_impl(data, ostr, options, 1); - ostr << "\n"; ostr.precision(old_precision); return rv; diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 66b565c7634..fd0702733c0 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -116,6 +116,11 @@ bool LLWatchdogTimeout::isAlive() const return (mTimer.getStarted() && !mTimer.hasExpired()); } +bool LLWatchdogTimeout::hasExpired() const +{ + return mTimer.hasExpired(); +} + void LLWatchdogTimeout::reset() { mTimer.setTimerExpirySec(mTimeout); diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index f138fbccb05..b286dd179d5 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -47,6 +47,7 @@ class LLWatchdogEntry // This may mean that resources used by // isAlive and other method may need synchronization. virtual bool isAlive() const = 0; + virtual bool hasExpired() const = 0; virtual void reset() = 0; virtual void start(); virtual void stop(); @@ -66,6 +67,7 @@ class LLWatchdogTimeout : public LLWatchdogEntry virtual ~LLWatchdogTimeout(); bool isAlive() const override; + bool hasExpired() const override; void reset() override; void start() override { start(""); } void stop() override; diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index fae9f7023f4..f52b3fec714 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -243,6 +243,58 @@ namespace tut xml_test("binary", expected); } + template<> template<> + void sd_xml_object::test<7>() + { + // Test that stream state (precision and exceptions) is correctly restored after format() + { + std::ostringstream ostr; + + // Set custom precision + ostr.precision(10); + + // Set some exception bits + ostr.exceptions(std::ios_base::badbit); + std::ios_base::iostate original_exceptions = ostr.exceptions(); + + // Format some LLSD data + mSD = 3.141592653589793; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify precision was restored + ensure_equals("precision should be restored", + ostr.precision(), 10); + + // Verify exceptions were restored + ensure_equals("exception bits should be restored", + ostr.exceptions(), original_exceptions); + } + + // Test with no bits set + { + std::ostringstream ostr; + ostr.precision(5); + std::ios_base::iostate original_exceptions = ostr.exceptions(); // 0 + + mSD = "test"; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify exceptions were removed + ensure_equals("exception bits should remain unchanged", + ostr.exceptions(), original_exceptions); + + // Verify precision was still restored even on failure + ensure_equals("precision should be restored even on stream failure", + ostr.precision(), (std::streamsize)5); + } + } + class TestLLSDSerializeData { public: diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index dd7d1a043ff..ea33178a1e9 100644 --- a/indra/llfilesystem/lldiskcache.cpp +++ b/indra/llfilesystem/lldiskcache.cpp @@ -64,9 +64,9 @@ LLDiskCache::LLDiskCache(const std::string& cache_dir, // WARNING: purge() is called by LLPurgeDiskCacheThread. As such it must // NOT touch any LLDiskCache data without introducing and locking a mutex! -// Interaction through the filesystem itself should be safe. Let’s say thread +// Interaction through the filesystem itself should be safe. Let's say thread // A is accessing the cache file for reading/writing and thread B is trimming -// the cache. Let’s also assume using llifstream to open a file and +// the cache. Let's also assume using llifstream to open a file and // boost::filesystem::remove are not atomic (which will be pretty much the // case). diff --git a/indra/llimagej2coj/llimagej2coj.cpp b/indra/llimagej2coj/llimagej2coj.cpp index 7cfadb889dd..8d544b360bd 100644 --- a/indra/llimagej2coj/llimagej2coj.cpp +++ b/indra/llimagej2coj/llimagej2coj.cpp @@ -227,11 +227,11 @@ static void opj_free_user_data_write(void * user_data) */ static U32 estimate_num_layers(U32 surface) { - if (surface <= 1024) return 2; // Tiny (≤32×32) - else if (surface <= 16384) return 3; // Small (≤128×128) - else if (surface <= 262144) return 4; // Medium (≤512×512) + if (surface <= 1024) return 2; // Tiny (<=32x32) + else if (surface <= 16384) return 3; // Small (<=128x128) + else if (surface <= 262144) return 4; // Medium (<=512x512) else if (surface <= 1048576) return 5; // Up to ~1MP - else return 6; // Up to ~1.5–2MP + else return 6; // Up to ~1.5-2MP } /** diff --git a/indra/llinventory/llinventory.cpp b/indra/llinventory/llinventory.cpp index f126accfb86..2f60e3bead4 100644 --- a/indra/llinventory/llinventory.cpp +++ b/indra/llinventory/llinventory.cpp @@ -33,6 +33,7 @@ #include "llxorcipher.h" #include "llsd.h" #include "llsdserialize.h" +#include "llstreamtools.h" #include "message.h" #include @@ -827,6 +828,42 @@ bool LLInventoryItem::importLegacyStream(std::istream& input_stream) } mDescription.assign(valuestr); + + // Currently server side doesn't handle escaped newline right + // and they end up unescaped. + // And even if we do copy the data correctly, moving the item + // from notecard using CopyInventoryFromNotecard drops the + // content after first the new line. + // + // Check if the description ends with | on this line + if (strchr(buffer, '|') == nullptr) + { + // No | found, continue reading lines until we find one + char next_buffer[MAX_STRING]; + while (input_stream.good()) + { + input_stream.getline(next_buffer, MAX_STRING); + + // Look for | delimiter + char* pipe_pos = strchr(next_buffer, '|'); + if (pipe_pos != nullptr) + { + // Found the delimiter, add text up to it + *pipe_pos = '\0'; // Terminate at the pipe + mDescription += '\n'; + mDescription += next_buffer; + break; + } + else + { + // No delimiter yet, add the whole line + mDescription += '\n'; + mDescription += next_buffer; + } + } + } + + unescape_string(mDescription); LLStringUtil::replaceNonstandardASCII(mDescription, ' '); /* TODO -- ask Ian about this code const char *donkey = mDescription.c_str(); @@ -920,7 +957,12 @@ bool LLInventoryItem::exportLegacyStream(std::ostream& output_stream, bool inclu output_stream << buffer; mSaleInfo.exportLegacyStream(output_stream); output_stream << "\t\tname\t" << mName.c_str() << "|\n"; - output_stream << "\t\tdesc\t" << mDescription.c_str() << "|\n"; + // Note: Currently server side doesn't handle newline right for notecards. + // Even if we escape or replace all newlines with spaces, notecard's + // import buffer still ends up with newlines. + std::string desc = mDescription; + escape_string(desc); + output_stream << "\t\tdesc\t" << desc.c_str() << "|\n"; output_stream << "\t\tcreation_date\t" << mCreationDate << "\n"; output_stream << "\t}\n"; return true; diff --git a/indra/llinventory/llsettingssky.cpp b/indra/llinventory/llsettingssky.cpp index 6f7510fef27..4957cf3c02d 100644 --- a/indra/llinventory/llsettingssky.cpp +++ b/indra/llinventory/llsettingssky.cpp @@ -653,39 +653,14 @@ void LLSettingsSky::blend(LLSettingsBase::ptr_t &end, F64 blendf) } } + mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_float(mHazeDensity, mLegacyHazeDensity, other->mHazeDensity, other->mLegacyHazeDensity, 0.7f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_float(mDistanceMultiplier, mLegacyDistanceMultiplier, other->mDistanceMultiplier, other->mLegacyDistanceMultiplier, 0.8f, (F32)blendf); + mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_color(mAmbientColor, mLegacyAmbientColor, other->mAmbientColor, other->mLegacyAmbientColor, LLColor3(0.25f, 0.25f, 0.25f), (F32)blendf); + mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); mHasLegacyHaze |= lerp_legacy_color(mBlueDensity, mLegacyBlueDensity, other->mBlueDensity, other->mLegacyBlueDensity, LLColor3(0.2447f, 0.4487f, 0.7599f), (F32)blendf); - if (mLegacyHazeHorizon == mLegacyDensityMultiplier == mLegacyBlueHorizon) - { - // mHazeHorizon coupled with mDensityMultiplier, mDistanceMultiplier and - // drastic blue horizon changes can result in a very bright sky during - // the transition. Purpose of this code is to calculate a 'fake level' - // and use it to even out brightness change. - // - // Example values that make lerp-based individual transition painfully bright: - // Start: 3 Haze Horiz, 0.1 Density, 6.54 Distance, white ambient, white blue horizon - // End: 0.03 Haze Horiz, 0.775 Density, 90.95 Distance, black ambient, black blue horizon - F32 strt_level = mHazeHorizon * mDensityMultiplier * mBlueHorizon.length(); - F32 end_level = other->mHazeHorizon * other->mDensityMultiplier * other->mBlueHorizon.length(); - mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); - mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); - mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); - - // lerp the fake level instead of density multiplier to avoid brightening the sky too much. - // This makes density multiplier non linear. - F32 new_level = lerp(strt_level, end_level, (F32)blendf); - mDensityMultiplier = new_level / (mHazeHorizon * mBlueHorizon.length()); - } - else - { - // default values are used, so we should lerp settings independently - mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); - mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); - mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); - } parammapping_t defaults = other->getParameterMap(); stringset_t skip = getSkipInterpolateKeys(); stringset_t slerps = getSlerpKeys(); diff --git a/indra/llkdu/llimagej2ckdu.cpp b/indra/llkdu/llimagej2ckdu.cpp index e7ac6bdb31e..21e5bc0ab43 100644 --- a/indra/llkdu/llimagej2ckdu.cpp +++ b/indra/llkdu/llimagej2ckdu.cpp @@ -158,6 +158,9 @@ class LLKDUDecodeState kdu_codestream* codestreamp); ~LLKDUDecodeState(); bool processTileDecode(F32 decode_time, bool limit_time = true); +#ifdef LL_WINDOWS + bool processTileDecodeSEH(F32 decode_time, bool limit_time = true); +#endif private: S32 mNumComponents; @@ -228,11 +231,11 @@ struct LLKDUMessageError : public LLKDUMessage { // According to the documentation nat found: // http://pirlwww.lpl.arizona.edu/resources/guide/software/Kakadu/html_pages/globals__kdu$mize_errors.html - // "If a kdu_error object is destroyed, handler→flush will be called with + // "If a kdu_error object is destroyed, handler->flush will be called with // an end_of_message argument equal to true and the process will // subsequently be terminated through exit. The termination may be // avoided, however, by throwing an exception from within the message - // terminating handler→flush call." + // terminating handler->flush call." // So throwing an exception here isn't arbitrary: we MUST throw an // exception if we want to recover from a KDU error. // Because this confused me: the above quote specifically refers to @@ -282,10 +285,16 @@ void LLImageJ2CKDU::setupCodeStream(LLImageJ2C &base, bool keep_codestream, ECod try { - S32 data_size = base.getDataSize(); S32 max_bytes = (base.getMaxBytes() ? base.getMaxBytes() : data_size); + // Data sanity checks + // 0 data size appears to be valid. + if (data_size < 0 || data_size > 128 * 1024 * 1024) // 128MB + { + LLTHROW(KDUError(STRINGIZE("Invalid data size: " << data_size))); + } + // // Initialization // @@ -385,7 +394,8 @@ void LLImageJ2CKDU::setupCodeStream(LLImageJ2C &base, bool keep_codestream, ECod catch (std::bad_alloc&) { // we are in a thread, can't show an 'out of memory' here, - // main thread will keep going + // as main thread will keep going + // Todo: should cause main thread to stop and show an error. throw; } catch (...) @@ -602,7 +612,11 @@ bool LLImageJ2CKDU::decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 deco // Do the actual processing F32 remaining_time = limit_time ? decode_time - decode_timer.getElapsedTimeF32().value() : 0.0f; // This is where we do the actual decode. If we run out of time, return false. +#ifdef LL_WINDOWS + if (mDecodeState->processTileDecodeSEH(remaining_time, limit_time)) +#else if (mDecodeState->processTileDecode(remaining_time, limit_time)) +#endif { mDecodeState.reset(); } @@ -1302,6 +1316,11 @@ all necessary level shifting, type conversion, rounding and truncation. */ LLKDUDecodeState::LLKDUDecodeState(kdu_tile tile, kdu_byte *buf, S32 row_gap, kdu_codestream* codestreamp) { + if (!buf) + { + LLTHROW(KDUError("Null buffer passed to LLKDUDecodeState")); + } + S32 c; mTile = tile; @@ -1311,6 +1330,11 @@ LLKDUDecodeState::LLKDUDecodeState(kdu_tile tile, kdu_byte *buf, S32 row_gap, mNumComponents = tile.get_num_components(); llassert(mNumComponents <= 4); + if (mNumComponents <= 0 || mNumComponents > 4) + { + LL_WARNS() << "Invalid component count: " << mNumComponents << ", clamping to valid range" << LL_ENDL; + mNumComponents = llclamp(mNumComponents, 1, 4); + } mUseYCC = tile.get_ycc(); for (c = 0; c < 4; ++c) @@ -1363,6 +1387,20 @@ LLKDUDecodeState::~LLKDUDecodeState() mTile.close(); } +#ifdef LL_WINDOWS +bool LLKDUDecodeState::processTileDecodeSEH(F32 decode_time, bool limit_time) +{ + __try + { + return processTileDecode(decode_time, limit_time); + } + __except (msc_exception_filter(GetExceptionCode(), GetExceptionInformation())) + { + return false; + } +} +#endif + bool LLKDUDecodeState::processTileDecode(F32 decode_time, bool limit_time) /* Decompresses a tile, writing the data into the supplied byte buffer. The buffer contains interleaved image components, if there are any. diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index c74ea3e42b4..8d7d201be15 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -2383,11 +2383,31 @@ bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) //copy out vertices U32 num_verts = static_cast(pos.size())/(3*2); + if (num_verts == 0) + { + LL_WARNS() << "Zero vertices for face index: " << i << LL_ENDL; + face.resizeIndices(3); + face.resizeVertices(1); + face.mPositions->clear(); + face.mNormals->clear(); + face.mTexCoords->setZero(); + memset(face.mIndices, 0, sizeof(U16) * 3); + continue; + } + + if (num_verts > 65535) // U16 indices + { + LL_WARNS() << "Invalid vertex count " << num_verts << " exceeds maximum for face index: " << i << LL_ENDL; + mVolumeFaces.clear(); + return false; + } + face.resizeVertices(num_verts); if (num_verts > 0 && !face.mPositions) { LL_WARNS() << "Failed to allocate " << num_verts << " vertices for face index: " << i << " Total: " << face_count << LL_ENDL; + face.resizeVertices(0); face.resizeIndices(0); continue; } @@ -5677,7 +5697,40 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) mOptimized = true; if (gen_tangents && mNormals && mTexCoords) - { // generate mikkt space tangents before cache optimizing since the index buffer may change + { + if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices <= 0) + { + LL_WARNS_ONCE("LLVolume") << "Invalid volume face data for tangent generation: " + << "mPositions=" << (void*)mPositions + << ", mIndices=" << (void*)mIndices + << ", mNumVertices=" << mNumVertices + << ", mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + if (mNumIndices % 3 != 0) + { + LL_WARNS_ONCE("LLVolume") << "Non-triangulated mesh, mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + for (S32 i = 0; i < mNumIndices; ++i) + { + if (mIndices[i] >= mNumVertices) + { + LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << i << "]=" + << mIndices[i] << " >= mNumVertices=" << mNumVertices << LL_ENDL; + return false; + } + } + + if (mNormalizedScale.mV[0] == 0.0f || mNormalizedScale.mV[1] == 0.0f || mNormalizedScale.mV[2] == 0.0f) + { + LL_WARNS_ONCE("LLVolume") << "Invalid normalized scale: " << mNormalizedScale << LL_ENDL; + return false; + } + + // generate mikkt space tangents before cache optimizing since the index buffer may change // a bit of a hack to do this here, but this function gets called exactly once for the lifetime of a mesh // and is executed on a background thread MikktData data(this); diff --git a/indra/llmessage/llcircuit.cpp b/indra/llmessage/llcircuit.cpp index c2b1a2f069a..eaee7b0112c 100644 --- a/indra/llmessage/llcircuit.cpp +++ b/indra/llmessage/llcircuit.cpp @@ -346,8 +346,7 @@ S32 LLCircuitData::resendUnackedPackets(const F64Seconds now) packetp->mBuffer[0] |= LL_RESENT_FLAG; // tag packet id as being a resend - gMessageSystem->mPacketRing.sendPacket(packetp->mSocket, - (char *)packetp->mBuffer, packetp->mBufferLength, + gMessageSystem->sendPacketToSocket((char *)packetp->mBuffer, packetp->mBufferLength, packetp->mHost); mThrottles.throttleOverflow(TC_RESEND, packetp->mBufferLength * 8.f); @@ -973,7 +972,7 @@ bool LLCircuitData::updateWatchDogTimers(LLMessageSystem *msgsys) { // let's call this one a loss! mPacketsLost++; - gMessageSystem->mDroppedPackets++; + gMessageSystem->mLostPackets++; if(gMessageSystem->mVerboseLog) { std::ostringstream str; diff --git a/indra/llmessage/llpacketbuffer.h b/indra/llmessage/llpacketbuffer.h index ac4012d330d..b0505e7afd3 100644 --- a/indra/llmessage/llpacketbuffer.h +++ b/indra/llmessage/llpacketbuffer.h @@ -38,6 +38,9 @@ class LLPacketBuffer LLPacketBuffer(S32 hSocket); // receive a packet ~LLPacketBuffer(); + LLPacketBuffer(const LLPacketBuffer&) = default; + LLPacketBuffer& operator=(const LLPacketBuffer&) = default; + S32 getSize() const { return mSize; } const char *getData() const { return mData; } LLHost getHost() const { return mHost; } diff --git a/indra/llmessage/llpacketring.cpp b/indra/llmessage/llpacketring.cpp index b8284334eaa..bade413e61e 100644 --- a/indra/llmessage/llpacketring.cpp +++ b/indra/llmessage/llpacketring.cpp @@ -28,344 +28,121 @@ #include "llpacketring.h" -#if LL_WINDOWS - #include -#else - #include - #include -#endif - -// linden library includes #include "llerror.h" -#include "lltimer.h" -#include "llproxy.h" -#include "llrand.h" -#include "message.h" -#include "u64.h" -constexpr S16 MAX_BUFFER_RING_SIZE = 1024; +constexpr S16 MAX_BUFFER_RING_SIZE = 8192; + +// DANGER: don't adjust DEFAULT_BUFFER_RING_SIZE unless you know what +// you're doing. Its value affects the "buffer load rate" which is used +// to supply backpressure to an overloaded nework queue. constexpr S16 DEFAULT_BUFFER_RING_SIZE = 256; -LLPacketRing::LLPacketRing () - : mPacketRing(DEFAULT_BUFFER_RING_SIZE, nullptr) +LLPacketRing::LLPacketRing() + : mRing(DEFAULT_BUFFER_RING_SIZE, nullptr) { LLHost invalid_host; - for (size_t i = 0; i < mPacketRing.size(); ++i) + for (size_t i = 0; i < mRing.size(); ++i) { - mPacketRing[i] = new LLPacketBuffer(invalid_host, nullptr, 0); + mRing[i] = new LLPacketBuffer(invalid_host, nullptr, 0); } } -LLPacketRing::~LLPacketRing () +LLPacketRing::~LLPacketRing() { - for (auto packet : mPacketRing) + for (auto* packet : mRing) { delete packet; } - mPacketRing.clear(); + mRing.clear(); mNumBufferedPackets = 0; mNumBufferedBytes = 0; mHeadIndex = 0; } -S32 LLPacketRing::receivePacket (S32 socket, char *datap) -{ - bool drop = computeDrop(); - return (mNumBufferedPackets > 0) ? - receiveOrDropBufferedPacket(datap, drop) : - receiveOrDropPacket(socket, datap, drop); -} - -bool send_packet_helper(int socket, const char * datap, S32 data_size, LLHost host) -{ - if (!LLProxy::isSOCKSProxyEnabled()) - { - return send_packet(socket, datap, data_size, host.getAddress(), host.getPort()); - } - - char headered_send_buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; - - proxywrap_t *socks_header = static_cast(static_cast(&headered_send_buffer)); - socks_header->rsv = 0; - socks_header->addr = host.getAddress(); - socks_header->port = htons(host.getPort()); - socks_header->atype = ADDRESS_IPV4; - socks_header->frag = 0; - - memcpy(headered_send_buffer + SOCKS_HEADER_SIZE, datap, data_size); - - return send_packet( socket, - headered_send_buffer, - data_size + SOCKS_HEADER_SIZE, - LLProxy::getInstance()->getUDPProxy().getAddress(), - LLProxy::getInstance()->getUDPProxy().getPort()); -} - -bool LLPacketRing::sendPacket(int socket, const char * datap, S32 data_size, LLHost host) +void LLPacketRing::pushPacket(const LLPacketBuffer& packet) { - mActualBytesOut += data_size; - return send_packet_helper(socket, datap, data_size, host); -} - -void LLPacketRing::dropPackets (U32 num_to_drop) -{ - mPacketsToDrop += num_to_drop; -} - -void LLPacketRing::setDropPercentage (F32 percent_to_drop) -{ - mDropPercentage = percent_to_drop; -} - -bool LLPacketRing::computeDrop() -{ - bool drop= (mDropPercentage > 0.0f && (ll_frand(100.f) < mDropPercentage)); - if (drop) - { - ++mPacketsToDrop; - } - if (mPacketsToDrop > 0) - { - --mPacketsToDrop; - drop = true; - } - return drop; -} - -S32 LLPacketRing::receiveOrDropPacket(S32 socket, char *datap, bool drop) -{ - S32 packet_size = 0; - - // pull straight from socket - if (LLProxy::isSOCKSProxyEnabled()) + S16 ring_size = (S16)mRing.size(); + if (mNumBufferedPackets >= ring_size && ring_size < MAX_BUFFER_RING_SIZE) { - char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ - packet_size = receive_packet(socket, buffer); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - } - - if (packet_size > SOCKS_HEADER_SIZE) - { - if (drop) - { - packet_size = 0; - } - else - { - // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) - packet_size -= SOCKS_HEADER_SIZE; // The unwrapped packet size - memcpy(datap, buffer + SOCKS_HEADER_SIZE, packet_size); - proxywrap_t * header = static_cast(static_cast(buffer)); - mLastSender.setAddress(header->addr); - mLastSender.setPort(ntohs(header->port)); - mLastReceivingIF = ::get_receiving_interface(); - } - } - else - { - packet_size = 0; - } - } - else - { - packet_size = receive_packet(socket, datap); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - if (drop) - { - packet_size = 0; - } - else - { - mLastSender = ::get_sender(); - mLastReceivingIF = ::get_receiving_interface(); - } - } + expandRing(); + ring_size = (S16)mRing.size(); } - return packet_size; -} -S32 LLPacketRing::receiveOrDropBufferedPacket(char *datap, bool drop) -{ - assert(mNumBufferedPackets > 0); - S32 packet_size = 0; + LLPacketBuffer* slot = mRing[mHeadIndex]; + S32 old_size = slot->getSize(); - S16 ring_size = (S16)(mPacketRing.size()); - S16 packet_index = (mHeadIndex + ring_size - mNumBufferedPackets) % ring_size; - LLPacketBuffer* packet = mPacketRing[packet_index]; - packet_size = packet->getSize(); - mLastSender = packet->getHost(); - mLastReceivingIF = packet->getReceivingInterface(); + *slot = packet; - --mNumBufferedPackets; - mNumBufferedBytes -= packet_size; - if (mNumBufferedPackets == 0) - { - assert(mNumBufferedBytes == 0); - } + mHeadIndex = (mHeadIndex + 1) % ring_size; - if (!drop) + if (mNumBufferedPackets < ring_size) { - if (packet_size > 0) - { - memcpy(datap, packet->getData(), packet_size); - } - else - { - assert(false); - } + ++mNumBufferedPackets; + mNumBufferedBytes += packet.getSize(); } else { - packet_size = 0; + // Ring is at maximum capacity; oldest packet was overwritten. + // This is VERY BAD because we've already ACKed the packet we're loosing + // (if it was "reliable"). + LL_WARNS("PacketRing") << "buffer overflow at " << mNumBufferedPackets << " packets" << LL_ENDL; + mNumBufferedBytes += packet.getSize() - old_size; } - return packet_size; } -S32 LLPacketRing::bufferInboundPacket(S32 socket) +bool LLPacketRing::popPacket(LLPacketBuffer& packet) { - if (mNumBufferedPackets == mPacketRing.size() && mNumBufferedPackets < MAX_BUFFER_RING_SIZE) + if (mNumBufferedPackets <= 0) { - expandRing(); + return false; } - LLPacketBuffer* packet = mPacketRing[mHeadIndex]; - S32 old_packet_size = packet->getSize(); - S32 packet_size = 0; - if (LLProxy::isSOCKSProxyEnabled()) - { - char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ - packet_size = receive_packet(socket, buffer); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - if (packet_size > SOCKS_HEADER_SIZE) - { - // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) + S16 ring_size = (S16)mRing.size(); + S16 tail_index = (mHeadIndex + ring_size - mNumBufferedPackets) % ring_size; - proxywrap_t * header = static_cast(static_cast(buffer)); - LLHost sender; - sender.setAddress(header->addr); - sender.setPort(ntohs(header->port)); + LLPacketBuffer* slot = mRing[tail_index]; + S32 packet_size = slot->getSize(); - packet_size -= SOCKS_HEADER_SIZE; // The unwrapped packet size - packet->init(buffer + SOCKS_HEADER_SIZE, packet_size, sender); + packet = *slot; - mHeadIndex = (mHeadIndex + 1) % (S16)(mPacketRing.size()); - if (mNumBufferedPackets < MAX_BUFFER_RING_SIZE) - { - ++mNumBufferedPackets; - mNumBufferedBytes += packet_size; - } - else - { - // we overwrote an older packet - mNumBufferedBytes += packet_size - old_packet_size; - } - } - else - { - packet_size = 0; - } - } - } - else - { - packet->init(socket); - packet_size = packet->getSize(); - if (packet_size > 0) - { - mActualBytesIn += packet_size; + --mNumBufferedPackets; + mNumBufferedBytes -= packet_size; - mHeadIndex = (mHeadIndex + 1) % (S16)(mPacketRing.size()); - if (mNumBufferedPackets < MAX_BUFFER_RING_SIZE) - { - ++mNumBufferedPackets; - mNumBufferedBytes += packet_size; - } - else - { - // we overwrote an older packet - mNumBufferedBytes += packet_size - old_packet_size; - } - } - } - return packet_size; -} + llassert(mNumBufferedPackets > 0 || mNumBufferedBytes == 0); -S32 LLPacketRing::drainSocket(S32 socket) -{ - // drain into buffer - S32 packet_size = 1; - S32 num_loops = 0; - S32 old_num_packets = mNumBufferedPackets; - while (packet_size > 0) - { - packet_size = bufferInboundPacket(socket); - ++num_loops; - } - S32 num_dropped_packets = (num_loops - 1 + old_num_packets) - mNumBufferedPackets; - if (num_dropped_packets > 0) - { - // It will eventually be accounted by mDroppedPackets - // and mPacketsLost, but track it here for logging purposes. - mNumDroppedPackets += num_dropped_packets; - } - return (S32)(mNumBufferedPackets); + return true; } bool LLPacketRing::expandRing() { - // compute larger size - constexpr S16 BUFFER_RING_EXPANSION = 256; - S16 old_size = (S16)(mPacketRing.size()); + constexpr S16 BUFFER_RING_EXPANSION = 512; + S16 old_size = (S16)mRing.size(); S16 new_size = llmin(old_size + BUFFER_RING_EXPANSION, MAX_BUFFER_RING_SIZE); if (new_size == old_size) { - // mPacketRing is already maxed out return false; } - // make a larger ring and copy packet pointers + // Lay existing entries out linearly in FIFO order starting at index 0. std::vector new_ring(new_size, nullptr); for (S16 i = 0; i < old_size; ++i) { S16 j = (mHeadIndex + i) % old_size; - new_ring[i] = mPacketRing[j]; + new_ring[i] = mRing[j]; } - // allocate new packets for the remainder of new_ring LLHost invalid_host; for (S16 i = old_size; i < new_size; ++i) { new_ring[i] = new LLPacketBuffer(invalid_host, nullptr, 0); } - // swap the rings and reset mHeadIndex - mPacketRing.swap(new_ring); + mRing.swap(new_ring); mHeadIndex = mNumBufferedPackets; return true; } F32 LLPacketRing::getBufferLoadRate() const { - // goes up to MAX_BUFFER_RING_SIZE return (F32)mNumBufferedPackets / (F32)DEFAULT_BUFFER_RING_SIZE; } - -void LLPacketRing::dumpPacketRingStats() -{ - mNumDroppedPacketsTotal += mNumDroppedPackets; - LL_INFOS("Messaging") << "Packet ring stats: " << std::endl - << "Buffered packets: " << mNumBufferedPackets << std::endl - << "Buffered bytes: " << mNumBufferedBytes << std::endl - << "Dropped packets current: " << mNumDroppedPackets << std::endl - << "Dropped packets total: " << mNumDroppedPacketsTotal << std::endl - << "Dropped packets percentage: " << mDropPercentage << "%" << std::endl - << "Actual in bytes: " << mActualBytesIn << std::endl - << "Actual out bytes: " << mActualBytesOut << LL_ENDL; - mNumDroppedPackets = 0; -} diff --git a/indra/llmessage/llpacketring.h b/indra/llmessage/llpacketring.h index 572dcbd271d..5315e5a5bda 100644 --- a/indra/llmessage/llpacketring.h +++ b/indra/llmessage/llpacketring.h @@ -1,7 +1,16 @@ /** * @file llpacketring.h - * @brief definition of LLPacketRing class for implementing a resend, - * drop, or delay in packet transmissions + * @brief LLPacketRing: a simple ring buffer for LLPacketBuffers. + * + * LLPacketRing stores incoming UDP packets that have already been received + * from the network socket. It has no socket or proxy awareness; callers + * push packets in with pushPacket() and retrieve them in FIFO order with + * popPacket(). + * + * The ring starts at DEFAULT_BUFFER_RING_SIZE slots and grows in increments + * of BUFFER_RING_EXPANSION up to MAX_BUFFER_RING_SIZE. Once at the ceiling, + * pushPacket() silently overwrites the oldest queued packet to make room for + * the incoming one. * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code @@ -29,9 +38,7 @@ #include -#include "llhost.h" #include "llpacketbuffer.h" -#include "llthrottle.h" class LLPacketRing @@ -40,71 +47,26 @@ class LLPacketRing LLPacketRing(); ~LLPacketRing(); - // receive one packet: either buffered or from the socket - S32 receivePacket (S32 socket, char *datap); - - // send one packet - bool sendPacket(int h_socket, const char * send_buffer, S32 buf_size, LLHost host); - - // drains packets from socket and returns final mNumBufferedPackets - S32 drainSocket(S32 socket); + // Copy 'packet' onto the tail of the ring, growing the ring or + // overwriting the oldest entry when the ring is at capacity. + void pushPacket(const LLPacketBuffer& packet); - void dropPackets(U32); - void setDropPercentage (F32 percent_to_drop); - - inline LLHost getLastSender() const; - inline LLHost getLastReceivingInterface() const; - - S32 getActualInBytes() const { return mActualBytesIn; } - S32 getActualOutBytes() const { return mActualBytesOut; } - S32 getAndResetActualInBits() { S32 bits = mActualBytesIn * 8; mActualBytesIn = 0; return bits;} - S32 getAndResetActualOutBits() { S32 bits = mActualBytesOut * 8; mActualBytesOut = 0; return bits;} + // Copy the head (oldest) packet into 'packet'. + // Returns true if a packet was available, false if the ring was empty. + bool popPacket(LLPacketBuffer& packet); S32 getNumBufferedPackets() const { return (S32)(mNumBufferedPackets); } - S32 getNumBufferedBytes() const { return mNumBufferedBytes; } - S32 getNumDroppedPackets() const { return mNumDroppedPacketsTotal + mNumDroppedPackets; } - - F32 getBufferLoadRate() const; // from 0 to 4 (0 - empty, 1 - default size is full) - void dumpPacketRingStats(); -protected: - // returns 'true' if we should intentionally drop a packet - bool computeDrop(); - - // returns packet_size of received packet, zero or less if no packet found - S32 receiveOrDropPacket(S32 socket, char *datap, bool drop); - S32 receiveOrDropBufferedPacket(char *datap, bool drop); + S32 getNumBufferedBytes() const { return mNumBufferedBytes; } - // returns packet_size of packet buffered - S32 bufferInboundPacket(S32 socket); + // Ratio of buffered packets to DEFAULT_BUFFER_RING_SIZE (0 = empty, 1 = nominal full). + F32 getBufferLoadRate() const; - // returns 'true' if ring was expanded +private: + // Returns true if the ring was expanded, false if already at the ceiling. bool expandRing(); -protected: - std::vector mPacketRing; - S16 mHeadIndex { 0 }; + std::vector mRing; + S16 mHeadIndex { 0 }; S16 mNumBufferedPackets { 0 }; - S32 mNumDroppedPackets { 0 }; - S32 mNumDroppedPacketsTotal { 0 }; - S32 mNumBufferedBytes { 0 }; - - S32 mActualBytesIn { 0 }; - S32 mActualBytesOut { 0 }; - F32 mDropPercentage { 0.0f }; // % of inbound packets to drop - U32 mPacketsToDrop { 0 }; // drop next inbound n packets - - // These are the sender and receiving_interface for the last packet delivered by receivePacket() - LLHost mLastSender; - LLHost mLastReceivingIF; + S32 mNumBufferedBytes { 0 }; }; - - -inline LLHost LLPacketRing::getLastSender() const -{ - return mLastSender; -} - -inline LLHost LLPacketRing::getLastReceivingInterface() const -{ - return mLastReceivingIF; -} diff --git a/indra/llmessage/llqueryflags.h b/indra/llmessage/llqueryflags.h index 227d28ba5c5..8dfb74aab08 100644 --- a/indra/llmessage/llqueryflags.h +++ b/indra/llmessage/llqueryflags.h @@ -56,8 +56,8 @@ const U32 DFQ_NAME_SORT = 0x1 << 19; const U32 DFQ_LIMIT_BY_PRICE = 0x1 << 20; const U32 DFQ_LIMIT_BY_AREA = 0x1 << 21; -const U32 DFQ_FILTER_MATURE = 0x1 << 22; -const U32 DFQ_PG_PARCELS_ONLY = 0x1 << 23; +const U32 DFQ_FILTER_MATURE = 0x1 << 22; // legacy, will not work with any from DFQ_INC_NEW_VIEWER +const U32 DFQ_PG_PARCELS_ONLY = 0x1 << 23; // legacy, will not work with any from DFQ_INC_NEW_VIEWER const U32 DFQ_INC_PG = 0x1 << 24; // Flags appear in 1.23 viewer or later const U32 DFQ_INC_MATURE = 0x1 << 25; diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp index e2937490bab..b11e4598e7c 100644 --- a/indra/llmessage/message.cpp +++ b/indra/llmessage/message.cpp @@ -78,6 +78,8 @@ #include "lltransfertargetvfile.h" #include "llcorehttputil.h" #include "llpounceable.h" +#include "llproxy.h" +#include "llrand.h" // Constants //const char* MESSAGE_LOG_FILENAME = "message.log"; @@ -173,7 +175,7 @@ void LLMessageSystem::init() mTotalBytesIn = 0; mTotalBytesOut = 0; - mDroppedPackets = 0; // total dropped packets in + mLostPackets = 0; // total lost packets out mResentPackets = 0; // total resent packets out mFailedResendPackets = 0; // total resend failure packets out mOffCircuitPackets = 0; // total # of off-circuit packets rejected @@ -184,6 +186,13 @@ void LLMessageSystem::init() mIncomingCompressedSize = 0; mCurrentRecvPacketID = 0; + mActualBytesIn = 0; + mActualBytesOut = 0; + mDropPercentage = 0.0f; + mPacketsToDrop = 0; + mNumDroppedPackets = 0; + mNumDroppedPacketsTotal = 0; + mMessageFileVersionNumber = 0.f; mTimingCallback = NULL; @@ -508,18 +517,16 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) bool recv_reliable = false; bool recv_resent = false; - S32 acks = 0; + S32 num_acks = 0; S32 true_rcv_size = 0; U8* buffer = mTrueReceiveBuffer; - mTrueReceiveSize = mPacketRing.receivePacket(mSocket, (char *)mTrueReceiveBuffer); + mTrueReceiveSize = receivePacketOrDrop((char *)mTrueReceiveBuffer); // If you want to dump all received packets into SecondLife.log, uncomment this //dumpPacketToLog(); receive_size = mTrueReceiveSize; - mLastSender = mPacketRing.getLastSender(); - mLastReceivingIF = mPacketRing.getLastReceivingInterface(); if (receive_size < (S32) LL_MINIMUM_VALID_PACKET_SIZE) { @@ -535,24 +542,23 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) } else { - LLHost host; - LLCircuitData* cdp; - - // note if packet acks are appended. + // handle any packet ACKs (for outbound messages) found at tail of inbound message if(buffer[0] & LL_ACK_FLAG) { - acks += buffer[--receive_size]; + // Note: these ACKs may have already been handled if this was a buffered message + // but it doesn't hurt to handle them again. + num_acks += buffer[--receive_size]; true_rcv_size = receive_size; - if(receive_size >= ((S32)(acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE))) + if(receive_size >= ((S32)(num_acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE))) { - receive_size -= acks * sizeof(TPACKETID); + receive_size -= num_acks * sizeof(TPACKETID); } else { // mal-formed packet. ignore it and continue with // the next one LL_WARNS("Messaging") << "Malformed packet received. Packet size " - << receive_size << " with invalid no. of acks " << acks + << receive_size << " with invalid no. of acks " << num_acks << LL_ENDL; valid_packet = false; continue; @@ -562,20 +568,20 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) // process the message as normal mIncomingCompressedSize = zeroCodeExpand(&buffer, &receive_size); mCurrentRecvPacketID = ntohl(*((U32*)(&buffer[1]))); - host = getSender(); + LLHost host = getSender(); const bool resetPacketId = true; - cdp = findCircuit(host, resetPacketId); + LLCircuitData* cdp = findCircuit(host, resetPacketId); // At this point, cdp is now a pointer to the circuit that // this message came in on if it's valid, and NULL if the // circuit was bogus. - if(cdp && (acks > 0) && ((S32)(acks * sizeof(TPACKETID)) < (true_rcv_size))) + if(cdp && (num_acks > 0) && ((S32)(num_acks * sizeof(TPACKETID)) < (true_rcv_size))) { TPACKETID packet_id; U32 mem_id=0; - for(S32 i = 0; i < acks; ++i) + for(S32 i = 0; i < num_acks; ++i) { true_rcv_size -= sizeof(TPACKETID); memcpy(&mem_id, &mTrueReceiveBuffer[true_rcv_size], /* Flawfinder: ignore*/ @@ -630,7 +636,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) str << tbuf << "(unknown)" << (recv_reliable ? " reliable" : "") << " resent " - << ((acks > 0) ? "acks" : "") + << ((num_acks > 0) ? "acks" : "") << " DISCARD DUPLICATE"; LL_INFOS("Messaging") << str.str() << LL_ENDL; } @@ -680,7 +686,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) if ( valid_packet ) { - logValidMsg(cdp, host, recv_reliable, recv_resent, acks>0 ); + logValidMsg(cdp, host, recv_reliable, recv_resent, num_acks>0 ); valid_packet = mTemplateMessageReader->readMessage(buffer, host); } @@ -724,7 +730,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count ) // Check to see if we need to print debug info if ((mt_sec - mCircuitPrintTime) > mCircuitPrintFreq) { - mPacketRing.dumpPacketRingStats(); + dumpPacketRingStats(); dumpCircuitInfo(); mCircuitPrintTime = mt_sec; } @@ -749,6 +755,10 @@ S32 LLMessageSystem::getReceiveBytes() const } } +F32 LLMessageSystem::getBufferLoadRate() const +{ + return llmax(mHighPriorityInbound.getBufferLoadRate(), mLowPriorityInbound.getBufferLoadRate()); +} void LLMessageSystem::processAcks(LockMessageChecker&, F32 collect_time) { @@ -822,7 +832,264 @@ void LLMessageSystem::processAcks(LockMessageChecker&, F32 collect_time) S32 LLMessageSystem::drainUdpSocket() { - return mPacketRing.drainSocket(mSocket); + S32 packet_size = 1; + S32 num_loops = 0; + S32 old_num_buffered_packets = getNumBufferedPackets(); + while (packet_size > 0) + { + packet_size = bufferInboundPacket(); + ++num_loops; + } + S32 num_dropped_packets = (num_loops - 1 + old_num_buffered_packets) - getNumBufferedPackets(); + if (num_dropped_packets > 0) + { + mNumDroppedPackets += num_dropped_packets; + } + return getNumBufferedPackets(); +} + +bool LLMessageSystem::computeDrop() +{ + bool drop = (mDropPercentage > 0.0f && (ll_frand(100.f) < mDropPercentage)); + if (drop) + { + ++mPacketsToDrop; + } + if (mPacketsToDrop > 0) + { + --mPacketsToDrop; + drop = true; + } + return drop; +} + +bool LLMessageSystem::isHighPriorityMessage(const LLPacketBuffer& pkt) const +{ + S32 size = pkt.getSize(); + if (size < LL_PACKET_ID_SIZE + 1) + { + return false; + } + + // We want to prioritize crucial messages use to establish viewer <--> simulator connection, + // which are all low-frequency. A simple approximation is to just prioritize all non high- + // frequency messages. + // + // High frequency messages use a single byte for message_id whereas all low- and medium- + // frequency messages have 255 at the first byte of the message_id (which is after the + // LL_PACKET_ID_SIZE bytes of packet_id). + return *((const U8*)pkt.getData() + LL_PACKET_ID_SIZE) == 255; +} + +void LLMessageSystem::dropPackets(U32 num_to_drop) +{ + mPacketsToDrop += num_to_drop; +} + +void LLMessageSystem::setDropPercentage(F32 percent_to_drop) +{ + mDropPercentage = percent_to_drop; +} + +S32 LLMessageSystem::receivePacketOrDrop(char* datap) +{ + if (getNumBufferedPackets() > 0) + { + LLHost invalid_host; + LLPacketBuffer pkt(invalid_host, nullptr, 0); + if (!mHighPriorityInbound.popPacket(pkt)) + { + mLowPriorityInbound.popPacket(pkt); + } + + S32 packet_size = pkt.getSize(); + mLastSender = pkt.getHost(); + mLastReceivingIF = pkt.getReceivingInterface(); + + if (packet_size > 0) + { + memcpy(datap, pkt.getData(), packet_size); + } + return packet_size; + } + + // Read directly from the socket. + bool drop = computeDrop(); + S32 packet_size = 0; + if (LLProxy::isSOCKSProxyEnabled()) + { + char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ + packet_size = receive_packet(mSocket, buffer); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + } + if (packet_size > SOCKS_HEADER_SIZE) + { + if (drop) + { + packet_size = 0; + } + else + { + // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) + packet_size -= SOCKS_HEADER_SIZE; + memcpy(datap, buffer + SOCKS_HEADER_SIZE, packet_size); + proxywrap_t* header = static_cast(static_cast(buffer)); + mLastSender.setAddress(header->addr); + mLastSender.setPort(ntohs(header->port)); + mLastReceivingIF = ::get_receiving_interface(); + } + } + else + { + packet_size = 0; + } + } + else + { + packet_size = receive_packet(mSocket, datap); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + if (drop) + { + packet_size = 0; + } + else + { + mLastSender = ::get_sender(); + mLastReceivingIF = ::get_receiving_interface(); + } + } + } + return packet_size; +} + +S32 LLMessageSystem::bufferInboundPacket() +{ + LLHost invalid_host; + LLPacketBuffer pkt(invalid_host, nullptr, 0); + S32 packet_size = 0; + + if (LLProxy::isSOCKSProxyEnabled()) + { + char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ + packet_size = receive_packet(mSocket, buffer); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + if (packet_size > SOCKS_HEADER_SIZE) + { + // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) + proxywrap_t* header = static_cast(static_cast(buffer)); + LLHost sender; + sender.setAddress(header->addr); + sender.setPort(ntohs(header->port)); + packet_size -= SOCKS_HEADER_SIZE; + pkt.init(buffer + SOCKS_HEADER_SIZE, packet_size, sender); + } + else + { + packet_size = 0; + } + } + } + else + { + pkt.init(mSocket); + packet_size = pkt.getSize(); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + } + } + + if (packet_size >= (S32)LL_MINIMUM_VALID_PACKET_SIZE && !computeDrop()) + { + const char* data = pkt.getData(); + LLCircuitData* cdp = mCircuitInfo.findCircuit(pkt.getHost()); + TPACKETID recv_packet_id = ntohl(*((U32*)(&data[1]))); + + // Harvest piggybacked ACKs for outbound messages from the packet tail of this inbound message + if (cdp && (data[0] & LL_ACK_FLAG)) + { + U8 num_acks = (U8)data[packet_size - 1]; + S32 true_rcv_size = packet_size - 1; + if (true_rcv_size >= (S32)(num_acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE)) + { + TPACKETID ack_id; + U32 mem_id = 0; + for (S32 i = 0; i < num_acks; ++i) + { + true_rcv_size -= sizeof(TPACKETID); + memcpy(&mem_id, &data[true_rcv_size], sizeof(TPACKETID)); /* Flawfinder: ignore */ + ack_id = ntohl(mem_id); + cdp->ackReliablePacket(ack_id); + } + if (!cdp->getUnackedPacketCount()) + { + mCircuitInfo.mUnackedCircuitMap.erase(cdp->mHost); + } + } + } + + // ACK inbound reliable packet ASAP + if (cdp && (data[0] & LL_RELIABLE_FLAG)) + { + cdp->collectRAck(recv_packet_id); + } + + if (isHighPriorityMessage(pkt)) + { + mHighPriorityInbound.pushPacket(pkt); + } + else + { + mLowPriorityInbound.pushPacket(pkt); + } + } + + return packet_size; +} + +bool LLMessageSystem::sendPacketToSocket(const char* datap, S32 data_size, LLHost host) +{ + mActualBytesOut += data_size; + if (!LLProxy::isSOCKSProxyEnabled()) + { + return send_packet(mSocket, datap, data_size, host.getAddress(), host.getPort()); + } + + char headered_send_buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; + + proxywrap_t* socks_header = static_cast(static_cast(&headered_send_buffer)); + socks_header->rsv = 0; + socks_header->addr = host.getAddress(); + socks_header->port = htons(host.getPort()); + socks_header->atype = ADDRESS_IPV4; + socks_header->frag = 0; + + memcpy(headered_send_buffer + SOCKS_HEADER_SIZE, datap, data_size); + + return send_packet(mSocket, + headered_send_buffer, + data_size + SOCKS_HEADER_SIZE, + LLProxy::getInstance()->getUDPProxy().getAddress(), + LLProxy::getInstance()->getUDPProxy().getPort()); +} + +void LLMessageSystem::dumpPacketRingStats() +{ + mNumDroppedPacketsTotal += mNumDroppedPackets; + LL_INFOS("Messaging") << "buffered_packets=" << getNumBufferedPackets() + << "buffered_bytes=" << (mHighPriorityInbound.getNumBufferedBytes() + mLowPriorityInbound.getNumBufferedBytes()) + << "recently_dropped=" << mNumDroppedPackets + << "total_dropped=" << mNumDroppedPacketsTotal + << "dropped_percentage=" << mDropPercentage << "%" + << "bytes_IN=" << mActualBytesIn + << "bytes_OUT=" << mActualBytesOut << LL_ENDL; + mNumDroppedPackets = 0; } void LLMessageSystem::copyMessageReceivedToSend() @@ -1277,7 +1544,7 @@ S32 LLMessageSystem::sendMessage(const LLHost &host) } bool success; - success = mPacketRing.sendPacket(mSocket, (char *)buf_ptr, buffer_length, host); + success = sendPacketToSocket((char *)buf_ptr, buffer_length, host); if (!success) { @@ -2607,7 +2874,7 @@ void LLMessageSystem::summarizeLogs(std::ostream& str) str << buffer << std::endl << std::endl; buffer = llformat( "SendPacket failures: %20d", mSendPacketFailureCount); str << buffer << std::endl; - buffer = llformat( "Dropped packets: %20d", mDroppedPackets); + buffer = llformat( "Dropped packets: %20d", getTotalNumDroppedPackets()); str << buffer << std::endl; buffer = llformat( "Resent packets: %20d", mResentPackets); str << buffer << std::endl; @@ -3333,7 +3600,7 @@ void LLMessageSystem::establishBidirectionalTrust(const LLHost &host, S64 frame_ void LLMessageSystem::dumpPacketToLog() { - LL_WARNS("Messaging") << "Packet Dump from:" << mPacketRing.getLastSender() << LL_ENDL; + LL_WARNS("Messaging") << "Packet Dump from:" << mLastSender << LL_ENDL; LL_WARNS("Messaging") << "Packet Size:" << mTrueReceiveSize << LL_ENDL; char line_buffer[256]; /* Flawfinder: ignore */ S32 i; diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h index 14cdc48a07d..e81cfe16a90 100644 --- a/indra/llmessage/message.h +++ b/indra/llmessage/message.h @@ -119,6 +119,49 @@ class LLMessageStringTable : public LLSingleton // Repeat for number of messages in file // +// UDP Packet Buffer Layout +// +// Every UDP message sent or received by LLMessageSystem uses the following +// on-wire layout. Offsets are defined in EPacketHeaderLayout below. +// +// Byte(s) Name Description +// ------- ---- ----------- +// 0 Flags Bit-field (see flag constants below): +// 0x80 LL_ZERO_CODE_FLAG – body is zero-run-length encoded +// 0x40 LL_RELIABLE_FLAG – sender expects a packet ACK +// 0x20 LL_RESENT_FLAG – this is a retransmission +// 0x10 LL_ACK_FLAG – piggybacked ACKs are appended +// at the tail of this packet +// 1-4 Packet ID Sequence number, U32 in network (big-endian) byte order. +// 5 Offset Byte offset from PHL_NAME to the start of the message +// body (past the message-ID bytes). Zero for most messages. +// 6+ Message ID Variable-length message type identifier: +// High-frequency (1 byte, values 0x01–0xFE) +// Medium-frequency (2 bytes, 0xFF hh) +// Low-frequency (4 bytes, 0xFF 0xFF hh ll) +// ... Body Message block data, described by the message template. +// Present only when LL_ZERO_CODE_FLAG is clear; otherwise +// the body (everything after byte 5) is zero-coded (see +// below). The 6-byte header is never zero-coded. +// +// Optional ACK tail (present when LL_ACK_FLAG is set in the flags byte): +// +// ... ACK IDs N packet-sequence IDs being acknowledged, each a U32 in +// network byte order, packed contiguously immediately before +// the ACK count byte. Read in reverse: walk backwards from +// just before the count byte, 4 bytes at a time. +// last ACK Count U8 giving N, the number of appended ACK IDs (max 255). +// This is the very last byte of the UDP payload. +// +// Zero-coding (applied when LL_ZERO_CODE_FLAG is set): +// +// Runs of zero bytes in the body are replaced by a two-byte token: +// 0x00 N – represents (N + 1) zero bytes, for N in 1..254 +// 0x00 0x00 N – represents (256 + N) zero bytes (wrap/overflow case) +// A literal 0x00 byte that starts no run is encoded as 0x00 0x00 0x00. +// The six-byte packet header is excluded from zero-coding and is always +// transmitted as-is. + // Constants const S32 MAX_MESSAGE_INTERNAL_NAME_SIZE = 255; const S32 MAX_BUFFER_SIZE = NET_BUFFER_SIZE; @@ -291,8 +334,11 @@ class LLMessageSystem : public LLMessageSenderInterface bool mBlockUntrustedInterface; LLHost mUntrustedInterface; + protected: + LLPacketRing mHighPriorityInbound; + LLPacketRing mLowPriorityInbound; + public: - LLPacketRing mPacketRing; LLReliablePacketParams mReliablePacketParams; // Set this flag to true when you want *very* verbose logs. @@ -334,7 +380,7 @@ class LLMessageSystem : public LLMessageSenderInterface U32 mReliablePacketsIn; // total reliable packets in U32 mReliablePacketsOut; // total reliable packets out - U32 mDroppedPackets; // total dropped packets in + U32 mLostPackets; // total reliable outbound packets declared lost U32 mResentPackets; // total resent packets out U32 mFailedResendPackets; // total resend failure packets out U32 mOffCircuitPackets; // total # of off-circuit packets rejected @@ -420,6 +466,25 @@ class LLMessageSystem : public LLMessageSenderInterface // returns total number of buffered packets after the drain S32 drainUdpSocket(); + // Inbound Packet-loss simulation controls + void dropPackets(U32 num_to_drop); + void setDropPercentage(F32 percent_to_drop); + + // UDP byte-accounting + S32 getActualInBytes() const { return mActualBytesIn; } + S32 getActualOutBytes() const { return mActualBytesOut; } + S32 getAndResetActualInBits() { S32 bits = mActualBytesIn * 8; mActualBytesIn = 0; return bits; } + S32 getAndResetActualOutBits() { S32 bits = mActualBytesOut * 8; mActualBytesOut = 0; return bits; } + + // Get number of "dropped" inbound packets + S32 getTotalNumDroppedPackets() const { return mNumDroppedPacketsTotal + mNumDroppedPackets; } + + S32 getNumBufferedPackets() const { return mHighPriorityInbound.getNumBufferedPackets() + mLowPriorityInbound.getNumBufferedPackets(); } + void dumpPacketRingStats(); + + // Send datap to host via mSocket (with SOCKS proxy support if enabled). + bool sendPacketToSocket(const char* datap, S32 data_size, LLHost host); + bool isMessageFast(const char *msg); bool isMessage(const char *msg) { @@ -754,7 +819,7 @@ class LLMessageSystem : public LLMessageSenderInterface S32 getReceiveBytes() const; S32 getUnackedListSize() const { return mUnackedListSize; } - F32 getBufferLoadRate() const { return mPacketRing.getBufferLoadRate(); } + F32 getBufferLoadRate() const; //const char* getCurrentSMessageName() const { return mCurrentSMessageName; } //const char* getCurrentSBlockName() const { return mCurrentSBlockName; } @@ -898,6 +963,32 @@ class LLMessageSystem : public LLMessageSenderInterface S32 mIncomingCompressedSize; // original size of compressed msg (0 if uncomp.) TPACKETID mCurrentRecvPacketID; // packet ID of current receive packet (for reporting) + // Socket I/O helpers + + // Receive one packet: pop from ring if buffered, else read from mSocket. + // Sets mLastSender and mLastReceivingIF. + // Returns packet_size, or 0 if no packet or packet was dropped. + S32 receivePacketOrDrop(char* datap); + + // Read one raw packet from mSocket into inbound message queues + // Returns packet_size (0 if no packet was available). + S32 bufferInboundPacket(); + + // Returns true if the next inbound packet should be intentionally dropped. + bool computeDrop(); + + // Returns true if pkt carries a high-priority message and should be queued + // in mHighPriorityInbound. + bool isHighPriorityMessage(const LLPacketBuffer& pkt) const; + + // Packet-loss simulation and byte-accounting state + S32 mActualBytesIn; + S32 mActualBytesOut; + F32 mDropPercentage; // % of inbound packets to drop + U32 mPacketsToDrop; // drop next N inbound packets + S32 mNumDroppedPackets; // inbound + S32 mNumDroppedPacketsTotal;// inbound + LLMessageBuilder* mMessageBuilder; LLTemplateMessageBuilder* mTemplateMessageBuilder; LLSDMessageBuilder* mLLSDMessageBuilder; diff --git a/indra/llmessage/net.cpp b/indra/llmessage/net.cpp index 2be5a9e5b63..0df38d040af 100644 --- a/indra/llmessage/net.cpp +++ b/indra/llmessage/net.cpp @@ -326,7 +326,7 @@ S32 receive_packet(int hSocket, char * receiveBuffer) return 0; if (WSAECONNRESET == WSAGetLastError()) return 0; - LL_INFOS() << "receivePacket() failed, Error: " << WSAGetLastError() << LL_ENDL; + LL_INFOS() << "receive_packet() failed, Error: " << WSAGetLastError() << LL_ENDL; } return nRet; diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index 075c496ec54..3c5682dfc9b 100644 --- a/indra/llrender/llfontfreetype.cpp +++ b/indra/llrender/llfontfreetype.cpp @@ -720,7 +720,50 @@ void LLFontFreetype::renderGlyph(EFontGlyphType bitmap_type, U32 glyph_index, ll llassert_always_msg(FT_Err_Ok == error, message.c_str()); } - llassert_always(! FT_Render_Glyph(mFTFace->glyph, gFontRenderMode) ); + // TODO: Make this more sturdy, make asserts/ll_errs conditional + // to non-critical characters. + // Temporarily leaving them for data gathering, but unicode chars + // like emojis should not cause the app to crash and should either + // fallback to some predetermined bitmap or simply return. + + // Verify glyph slot is valid + if (!mFTFace->glyph) + { + LL_ERRS() << "FT_Load_Glyph succeeded but glyph slot is null for wchar " << llformat("U+%xu", U32(wch)) << LL_ENDL; + return; + } + + // Check if bitmap buffer is already allocated + // It can potentially be preallocated for: + // 1. SVG/color glyphs rendered by FreeType's SVG_RendererHooks + // 2. Embedded bitmap fonts + // 3. Some Color emoji that use FT_LOAD_COLOR + if (!mFTFace->glyph->bitmap.buffer) + { + error = FT_Render_Glyph(mFTFace->glyph, gFontRenderMode); + if (error != FT_Err_Ok) + { + std::string render_message = llformat( + "Error %d (%s) rendering wchar %u glyph %u: format=%lu, pixel_mode=%d, render_mode=%d", + error, FT_Error_String(error), wch, glyph_index, + (unsigned long)mFTFace->glyph->format, mFTFace->glyph->bitmap.pixel_mode, gFontRenderMode); + + // Try with FT_RENDER_MODE_NORMAL as fallback + if (gFontRenderMode != FT_RENDER_MODE_NORMAL) + { + LL_WARNS_ONCE() << render_message << LL_ENDL; + error = FT_Render_Glyph(mFTFace->glyph, FT_RENDER_MODE_NORMAL); + if (error != FT_Err_Ok) + { + LL_ERRS() << "Fallback to FT_RENDER_MODE_NORMAL failed. " << render_message << LL_ENDL; + } + } + else + { + LL_ERRS() << render_message << LL_ENDL; + } + } + } mRenderGlyphCount++; } diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index b8545b3ed9c..c77a7ff8f18 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1142,34 +1142,55 @@ bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader) { std::string in_path = gDirUtilp->add(mShaderCacheDir, shader->mShaderHash.asString() + ".shaderbin"); auto& shader_info = binary_iter->second; - if (shader_info.mBinaryLength > 0) - { - std::vector in_data; - in_data.resize(shader_info.mBinaryLength); - LLUniqueFile filep = LLFile::fopen(in_path, "rb"); - if (filep) + try + { + constexpr GLsizei MAX_SHADER_BINARY_SIZE = 1024 * 1024; // 1 MB, normally around 10KB + if (shader_info.mBinaryLength > 0 && shader_info.mBinaryLength <= MAX_SHADER_BINARY_SIZE) { - size_t result = fread(in_data.data(), sizeof(U8), in_data.size(), filep); - filep.close(); + std::vector in_data; + in_data.resize(shader_info.mBinaryLength); - if (result == in_data.size()) + LLUniqueFile filep = LLFile::fopen(in_path, "rb"); + if (filep) { - GLenum error = glGetError(); // Clear current error - glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + size_t result = fread(in_data.data(), sizeof(U8), in_data.size(), filep); + filep.close(); - error = glGetError(); - GLint success = GL_TRUE; - glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); - if (error == GL_NO_ERROR && success == GL_TRUE) + if (result == in_data.size()) { - binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); - LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; - return true; + GLenum error = glGetError(); // Clear current error + glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + + error = glGetError(); + GLint success = GL_TRUE; + glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); + if (error == GL_NO_ERROR && success == GL_TRUE) + { + binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); + LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; + return true; + } + } + else + { + LL_WARNS("ShaderMgr") << "Incomplete read of shader binary. Expected: " + << in_data.size() << ", read: " << result << LL_ENDL; } } } } + catch (const std::bad_alloc&) + { + LL_WARNS("ShaderMgr") << "Failed to allocate memory for shader binary (" + << shader_info.mBinaryLength << " bytes) for: " + << shader->mName << LL_ENDL; + } + catch (const std::exception& err) + { + LL_WARNS("ShaderMgr") << "Caught exception " << err.what() << " while loading shader binary for: " << shader->mName << LL_ENDL; + } + //an error occured, normally we would print log but in this case it means the shader needs recompiling. LL_INFOS() << "Failed to load cached binary for shader: " << shader->mName << " falling back to compilation" << LL_ENDL; LLFile::remove(in_path); diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index 908e94b24c4..1c9a16ba413 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -41,6 +41,7 @@ set(llui_SOURCE_FILES llfloaterreglistener.cpp llflyoutbutton.cpp llfocusmgr.cpp + llgestureautocompletehelper.cpp llfolderview.cpp llfolderviewitem.cpp llfolderviewmodel.cpp @@ -154,6 +155,7 @@ set(llui_HEADER_FILES llfloaterreglistener.h llflyoutbutton.h llfocusmgr.h + llgestureautocompletehelper.h llfolderview.h llfolderviewitem.h llfolderviewmodel.h diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp index 01463b49629..db6c5c291fe 100644 --- a/indra/llui/llcombobox.cpp +++ b/indra/llui/llcombobox.cpp @@ -521,7 +521,7 @@ bool LLComboBox::setCurrentByIndex(S32 index) if (item->getEnabled()) { mList->selectItem(item, -1, true); - LLSD::String label = item->getColumn(0)->getValue().asString(); + LLSD::String label = getSelectedItemLabel(); if (mTextEntry) { mTextEntry->setText(label); diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp index 34eb1ea3fc6..64e47cea6e7 100644 --- a/indra/llui/llflatlistview.cpp +++ b/indra/llui/llflatlistview.cpp @@ -31,6 +31,14 @@ #include "llflatlistview.h" +#include "llfocusmgr.h" +#include "llrender2dutils.h" +#include "llui.h" +#include "lluicolortable.h" +#include "llwindow.h" + +#include + static const LLDefaultChildRegistry::Register flat_list_view("flat_list_view"); const LLSD SELECTED_EVENT = LLSD().with("selected", true); @@ -45,17 +53,28 @@ LLFlatListView::Params::Params() multi_select("multi_select"), keep_one_selected("keep_one_selected"), keep_selection_visible_on_reshape("keep_selection_visible_on_reshape",false), + allow_reorder("allow_reorder", false), + drag_indicator_color("drag_indicator_color"), no_items_text("no_items_text") {}; void LLFlatListView::reshape(S32 width, S32 height, bool called_from_parent /* = true */) { S32 delta = height - getRect().getHeight(); + LLRect visible_rc; + LLRect selected_rc = getLastSelectedItemRect(); + bool keep_selection_visible = false; + if (delta != 0 && mKeepSelectionVisibleOnReshape && selected_rc.isValid()) + { + visible_rc = getVisibleContentRect(); + keep_selection_visible = visible_rc.overlaps(selected_rc); + } + LLScrollContainer::reshape(width, height, called_from_parent); setItemsNoScrollWidth(width); rearrangeItems(); - if(delta!= 0 && mKeepSelectionVisibleOnReshape) + if(keep_selection_visible) { ensureSelectedVisible(); } @@ -91,7 +110,7 @@ bool LLFlatListView::addItem(LLPanel * item, const LLSD& value /*= LLUUID::null* } //_4 is for MASK - item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); item->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); // Children don't accept the focus @@ -142,7 +161,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr mItemsPanel->addChild(panel); //_4 is for MASK - panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); // Children don't accept the focus panel->setTabStop(false); @@ -165,7 +184,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr mItemsPanel->addChild(panel); //_4 is for MASK - panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4)); + panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4, CLICK_LEFT)); panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, item_pair, _4)); // Children don't accept the focus panel->setTabStop(false); @@ -217,7 +236,7 @@ bool LLFlatListView::insertItemAfter(LLPanel* after_item, LLPanel* item_to_add, } //_4 is for MASK - item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); item_to_add->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); rearrangeItems(); @@ -396,6 +415,11 @@ U32 LLFlatListView::size(const bool only_visible_items) const void LLFlatListView::clear() { + if (mReorderDragPair) + { + cancelReorderDrag(); + } + // This will clear mSelectedItemPairs, calling all appropriate callbacks. resetSelection(); @@ -460,6 +484,16 @@ LLFlatListView::LLFlatListView(const LLFlatListView::Params& p) , mIsConsecutiveSelection(false) , mKeepSelectionVisibleOnReshape(p.keep_selection_visible_on_reshape) , mFocusOnItemClicked(true) + , mAllowReorder(p.allow_reorder) + , mIsReordering(false) + , mReorderDragPair(NULL) + , mDeferredSelectPair(NULL) + , mReorderMouseDownX(0) + , mReorderMouseDownY(0) + , mReorderInsertIndex(-1) + , mDragIndicatorColor(p.drag_indicator_color.isProvided() + ? p.drag_indicator_color() + : LLUIColorTable::instance().getColor("EmphasisColor")) { mBorderThickness = getBorderWidth(); @@ -520,12 +554,29 @@ LLFlatListView::~LLFlatListView() // virtual void LLFlatListView::draw() { + // Keep scrolling while a reorder drag rests over the top/bottom edge, so the + // user does not have to drop, scroll, and grab again. + if (mIsReordering) + { + S32 local_x, local_y; + LLUI::getInstance()->getMousePositionLocal(this, &local_x, &local_y); + if (autoScroll(local_x, local_y)) + { + mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(local_x, local_y)); + } + } + // Highlight border if a child of this container has keyboard focus if( mSelectedItemsBorder->getVisible() ) { mSelectedItemsBorder->setKeyboardFocusHighlight( hasFocus() ); } LLScrollContainer::draw(); + + if (mIsReordering && mReorderInsertIndex >= 0) + { + drawReorderIndicator(); + } } // virtual @@ -602,7 +653,7 @@ void LLFlatListView::rearrangeItems() mSelectedItemsBorder->setRect(getLastSelectedItemRect().stretch(-1)); } -void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) +void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype) { if (!item_pair) return; @@ -617,6 +668,11 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) setFocus(true); } + if (clicktype == CLICK_LEFT && !(mask & (MASK_SHIFT | MASK_CONTROL))) + { + armReorderDrag(item_pair); + } + bool select_item = !isSelected(item_pair); //*TODO find a better place for that enforcing stuff @@ -695,6 +751,16 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) return; } + // When a reorderable list has a multi-selection, a plain click on one of the + // selected rows must not collapse the selection yet: the user may be about to + // drag the whole selection. Defer collapsing to mouse-up if no drag happens. + if (mAllowReorder && mMultipleSelection && !(mask & (MASK_CONTROL | MASK_SHIFT)) + && isSelected(item_pair) && numSelected() > 1) + { + mDeferredSelectPair = item_pair; + return; + } + //no need to do additional commit on selection reset if (!(mask & MASK_CONTROL) || !mMultipleSelection) resetSelection(true); @@ -715,8 +781,364 @@ void LLFlatListView::onItemRightMouseClick(item_pair_t* item_pair, MASK mask) if ( !(mask & MASK_CONTROL) && mMultipleSelection && isSelected(item_pair) ) return; - // else got same behavior as at onItemMouseClick - onItemMouseClick(item_pair, mask); + // else got same behavior as at onItemMouseClick, but a right click must never start a drag + onItemMouseClick(item_pair, mask, CLICK_RIGHT); +} + +static const S32 REORDER_DRAG_THRESHOLD = 5; + +void LLFlatListView::armReorderDrag(item_pair_t* item_pair) +{ + if (!mAllowReorder) + { + return; + } + + if (!item_pair || !item_pair->first) + { + return; + } + + if (size() < 2) + { + return; // nothing to reorder against + } + + if (gFocusMgr.getMouseCapture() && !hasMouseCapture()) + { + return; + } + + mReorderDragPair = item_pair; + mReorderDragGroup.clear(); + mDeferredSelectPair = NULL; + mIsReordering = false; + mReorderInsertIndex = -1; + + LLUI::getInstance()->getMousePositionLocal(this, &mReorderMouseDownX, &mReorderMouseDownY); + gFocusMgr.setMouseCapture(this); +} + +void LLFlatListView::updateReorderDrag(S32 x, S32 y) +{ + if (!mReorderDragPair) return; + + if (!mIsReordering) + { + if (abs(y - mReorderMouseDownY) < REORDER_DRAG_THRESHOLD + && abs(x - mReorderMouseDownX) < REORDER_DRAG_THRESHOLD) + { + return; // still within the click slop, not a drag yet + } + mIsReordering = true; + buildReorderGroup(); + } + + mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(x, y)); +} + +void LLFlatListView::buildReorderGroup() +{ + mReorderDragGroup.clear(); + + bool drag_selection = mMultipleSelection && isSelected(mReorderDragPair) && numSelected() > 1; + + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) continue; + + if (pair == mReorderDragPair) + { + mReorderDragGroup.push_back(pair); + } + else if (drag_selection && isSelected(pair) + && (!mReorderValidateCallback || mReorderValidateCallback(mReorderDragPair->second, pair->second))) + { + // only carry along selected rows that belong to the grabbed row's group + mReorderDragGroup.push_back(pair); + } + } +} + +bool LLFlatListView::isInReorderGroup(item_pair_t* item_pair) const +{ + return std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end(); +} + +void LLFlatListView::getReorderRemaining(pairs_list_t& remaining) const +{ + remaining.clear(); + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible() || isInReorderGroup(pair)) continue; + remaining.push_back(pair); + } +} + +void LLFlatListView::finishReorderDrag() +{ + if (mReorderDragPair && mIsReordering && mReorderInsertIndex >= 0) + { + pairs_list_t remaining; + getReorderRemaining(remaining); + + // Resolve the drop boundary to the remaining row it lands before (NULL = append). + item_pair_t* anchor = NULL; + S32 cur = 0; + for (item_pair_t* pair : remaining) + { + if (cur == mReorderInsertIndex) { anchor = pair; break; } + ++cur; + } + + LLSD moved_value = mReorderDragPair->second; + + for (item_pair_t* pair : mReorderDragGroup) + { + mItemPairs.remove(pair); + } + + pairs_iterator_t it = (anchor != NULL) + ? std::find(mItemPairs.begin(), mItemPairs.end(), anchor) + : mItemPairs.end(); + for (item_pair_t* pair : mReorderDragGroup) + { + mItemPairs.insert(it, pair); // preserves group order; it stays before anchor + } + + rearrangeItems(); + + if (mReorderCallback) + { + S32 visible_index = 0; + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) continue; + if (pair == mReorderDragPair) break; + ++visible_index; + } + + mReorderCallback(moved_value, visible_index); + } + } + + cancelReorderDrag(); +} + +void LLFlatListView::cancelReorderDrag() +{ + if (hasMouseCapture()) + { + gFocusMgr.setMouseCapture(NULL); + } + + clearReorderDragState(); +} + +void LLFlatListView::clearReorderDragState() +{ + if (mIsReordering) + { + getWindow()->setCursor(UI_CURSOR_ARROW); + } + + bool was_armed = (mReorderDragPair != NULL); + + mReorderDragPair = NULL; + mReorderDragGroup.clear(); + mDeferredSelectPair = NULL; + mIsReordering = false; + mReorderInsertIndex = -1; + + if (was_armed && mReorderEndedCallback) + { + mReorderEndedCallback(); + } +} + +void LLFlatListView::onMouseCaptureLost() +{ + clearReorderDragState(); +} + +void LLFlatListView::onVisibilityChange(bool new_visibility) +{ + if (!new_visibility && mReorderDragPair) + { + cancelReorderDrag(); + } + + LLScrollContainer::onVisibilityChange(new_visibility); +} + +S32 LLFlatListView::getInsertIndexAt(S32 x, S32 y) const +{ + S32 panel_x, panel_y; + localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel); + + // The drop boundary among the remaining rows is the count of remaining rows + // whose vertical centre sits above the cursor. + S32 index = 0; + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible() || isInReorderGroup(pair)) continue; + + if (pair->first->getRect().getCenterY() > panel_y) + { + ++index; + } + } + return index; +} + +LLFlatListView::item_pair_t* LLFlatListView::getReorderPairAt(S32 x, S32 y) const +{ + S32 panel_x, panel_y; + localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel); + + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) continue; + + // Claim the padding above each row so gap presses still resolve to a row. + LLRect rc = pair->first->getRect(); + rc.mTop += mItemPad; + if (rc.pointInRect(panel_x, panel_y)) + { + return pair; + } + } + return NULL; +} + +S32 LLFlatListView::constrainInsertIndex(S32 dest_index) const +{ + if (!mReorderValidateCallback) return dest_index; + + // Clamp the boundary to the contiguous run of remaining rows that share the + // grabbed row's group, so a drag can't leave its group. + pairs_list_t remaining; + getReorderRemaining(remaining); + + const LLSD& dragged = mReorderDragPair->second; + S32 first_valid = -1; + S32 last_valid = -1; + S32 i = 0; + for (item_pair_t* pair : remaining) + { + if (mReorderValidateCallback(dragged, pair->second)) + { + if (first_valid < 0) first_valid = i; + last_valid = i; + } + ++i; + } + + if (first_valid < 0) return -1; // no valid neighbour (whole group is being dragged) + + if (dest_index < first_valid) return first_valid; + if (dest_index > last_valid + 1) return last_valid + 1; + return dest_index; +} + +void LLFlatListView::drawReorderIndicator() +{ + pairs_list_t remaining; + getReorderRemaining(remaining); + if (remaining.empty()) return; + + const LLRect& panel_rc = mItemsPanel->getRect(); + const LLColor4& color = mDragIndicatorColor.get(); + + // faint highlight on each row being moved + for (item_pair_t* pair : mReorderDragGroup) + { + const LLRect& dr = pair->first->getRect(); + gl_rect_2d(panel_rc.mLeft + dr.mLeft, panel_rc.mBottom + dr.mTop, + panel_rc.mLeft + dr.mRight, panel_rc.mBottom + dr.mBottom, + color % 0.15f, true); + } + + // insertion line at the drop boundary among the remaining rows + S32 count = (S32)remaining.size(); + bool at_end = mReorderInsertIndex >= count; + S32 anchor_idx = at_end ? count - 1 : mReorderInsertIndex; + + item_pair_t* anchor = NULL; + S32 cur = 0; + for (item_pair_t* pair : remaining) + { + if (cur == anchor_idx) { anchor = pair; break; } + ++cur; + } + if (!anchor) return; + + const LLRect& item_rc = anchor->first->getRect(); + S32 left = panel_rc.mLeft + item_rc.mLeft; + S32 right = panel_rc.mLeft + item_rc.mRight; + S32 line_y = panel_rc.mBottom + (at_end ? item_rc.mBottom : item_rc.mTop); + + if (line_y < 0 || line_y > getRect().getHeight()) return; + + gl_rect_2d(left, line_y, right, line_y - 1, color, true); +} + +bool LLFlatListView::handleHover(S32 x, S32 y, MASK mask) +{ + if (mAllowReorder && mReorderDragPair && hasMouseCapture()) + { + updateReorderDrag(x, y); + if (mIsReordering) + { + getWindow()->setCursor(UI_CURSOR_ARROWDRAG); + return true; + } + } + return LLScrollContainer::handleHover(x, y, mask); +} + +bool LLFlatListView::handleMouseDown(S32 x, S32 y, MASK mask) +{ + bool handled = LLScrollContainer::handleMouseDown(x, y, mask); + + // A press in the padding between rows misses every item, so the per-item + // mouse-down never arms a reorder. Map it to the nearest row and arm there; + // armReorderDrag's capture guard ignores presses a child control already took. + if (mAllowReorder && !mReorderDragPair && !(mask & (MASK_CONTROL | MASK_SHIFT))) + { + if (item_pair_t* pair = getReorderPairAt(x, y)) + { + onItemMouseClick(pair, mask, CLICK_LEFT); + } + } + + return handled || (mReorderDragPair != NULL); +} + +bool LLFlatListView::handleMouseUp(S32 x, S32 y, MASK mask) +{ + if (mReorderDragPair && hasMouseCapture()) + { + bool was_reordering = mIsReordering; + item_pair_t* deferred = mDeferredSelectPair; + mDeferredSelectPair = NULL; + + finishReorderDrag(); + + if (was_reordering) + { + return true; // a drag happened: keep the (multi-)selection on the moved rows + } + + // No drag: apply the selection collapse that was deferred on mouse-down. + if (deferred) + { + resetSelection(true); + selectItemPair(deferred, true); + return true; + } + } + return LLScrollContainer::handleMouseUp(x, y, mask); } bool LLFlatListView::handleKeyHere(KEY key, MASK mask) @@ -1087,6 +1509,18 @@ bool LLFlatListView::removeItemPair(item_pair_t* item_pair, bool rearrange) { llassert(item_pair); + bool removing_reorder_pair = item_pair == mReorderDragPair + || std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end(); + + if (removing_reorder_pair) + { + cancelReorderDrag(); + } + if (item_pair == mDeferredSelectPair) + { + mDeferredSelectPair = NULL; + } + bool deleted = false; bool selection_changed = false; for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it) diff --git a/indra/llui/llflatlistview.h b/indra/llui/llflatlistview.h index 39afa33be82..ae20a4b1eb2 100644 --- a/indra/llui/llflatlistview.h +++ b/indra/llui/llflatlistview.h @@ -30,6 +30,7 @@ #include "llpanel.h" #include "llscrollcontainer.h" #include "lltextbox.h" +#include "lluicolor.h" /** @@ -106,12 +107,28 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler /** padding between items */ Optional item_pad; + /** allow items to be reordered by dragging them with the mouse */ + Optional allow_reorder; + + /** colour of the insertion indicator drawn while reordering */ + Optional drag_indicator_color; + /** textbox with info message when list is empty*/ Optional no_items_text; Params(); }; + /** Fired after the user drags an item to a new position: (moved value, new visible index). */ + typedef boost::function reorder_signal_t; + + /** + * Returns true if the dragged item is allowed to move past the given neighbour. + * Lets owners constrain reordering (e.g. keep items within a group). Optional; + * when unset any reorder is permitted. + */ + typedef boost::function reorder_validate_signal_t; + // disable traversal when finding widget to hand focus off to /*virtual*/ bool canFocusChildren() const override { return false; } @@ -259,6 +276,18 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler /** Turn on/off selection support */ void setAllowSelection(bool can_select) { mAllowSelection = can_select; } + /** Turn on/off drag-to-reorder support */ + void setAllowReorder(bool allow) { mAllowReorder = allow; } + + void setReorderCallback(reorder_signal_t cb) { mReorderCallback = cb; } + void setReorderValidateCallback(reorder_validate_signal_t cb) { mReorderValidateCallback = cb; } + + /** Fired when a reorder grab is released, whether or not it moved anything */ + void setReorderEndedCallback(boost::function cb) { mReorderEndedCallback = cb; } + + /** True while an item is grabbed for a drag-to-reorder (before or during the drag) */ + bool isReorderActive() const { return mReorderDragPair != NULL; } + /** Sets flag whether onCommit should be fired if selection was changed */ // FIXME: this should really be a separate signal, since "Commit" implies explicit user action, and selection changes can happen more indirectly. void setCommitOnSelectionChange(bool b) { mCommitOnSelectionChange = b; } @@ -330,7 +359,7 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler LLFlatListView(const LLFlatListView::Params& p); /** Manage selection on mouse events */ - void onItemMouseClick(item_pair_t* item_pair, MASK mask); + void onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype); void onItemRightMouseClick(item_pair_t* item_pair, MASK mask); @@ -368,6 +397,16 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler virtual bool handleKeyHere(KEY key, MASK mask) override; + virtual bool handleHover(S32 x, S32 y, MASK mask) override; + + virtual bool handleMouseDown(S32 x, S32 y, MASK mask) override; + + virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override; + + virtual void onMouseCaptureLost() override; + + virtual void onVisibilityChange(bool new_visibility) override; + virtual bool postBuild() override; virtual void onFocusReceived() override; @@ -384,6 +423,29 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler private: + // Drag-to-reorder helpers. + void armReorderDrag(item_pair_t* item_pair); + void updateReorderDrag(S32 x, S32 y); + void finishReorderDrag(); + void cancelReorderDrag(); + void clearReorderDragState(); + + // Builds the set of pairs being dragged (the whole selection when the grabbed + // row is part of a multi-selection, constrained to the validator's group). + void buildReorderGroup(); + bool isInReorderGroup(item_pair_t* item_pair) const; + + // The remaining visible pairs (those not being dragged), in visual order. + void getReorderRemaining(pairs_list_t& remaining) const; + // Number of leading non-dragged items whose centre sits above (x, y). + S32 getInsertIndexAt(S32 x, S32 y) const; + // Row under (x, y), counting the padding above each row, so presses in the + // inter-row gaps still map to a row instead of falling through. + item_pair_t* getReorderPairAt(S32 x, S32 y) const; + // Clamps an insertion boundary to the validator's contiguous group. + S32 constrainInsertIndex(S32 dest_index) const; + void drawReorderIndicator(); + void setItemsNoScrollWidth(S32 new_width) {mItemsNoScrollWidth = new_width - 2 * mBorderThickness;} void setNoItemsCommentVisible(bool visible) const; @@ -427,6 +489,20 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler bool mFocusOnItemClicked; + /** Drag-to-reorder state */ + bool mAllowReorder; + bool mIsReordering; // a drag is currently in progress + item_pair_t* mReorderDragPair; // the grabbed pair (drag anchor) + pairs_list_t mReorderDragGroup; // all pairs being dragged, in visual order + item_pair_t* mDeferredSelectPair; // collapse selection to this on mouse-up if no drag + S32 mReorderMouseDownX; + S32 mReorderMouseDownY; + S32 mReorderInsertIndex; // current drop boundary among the remaining (non-dragged) items + LLUIColor mDragIndicatorColor; + reorder_signal_t mReorderCallback; + reorder_validate_signal_t mReorderValidateCallback; + boost::function mReorderEndedCallback; + /** All pairs of the list */ pairs_list_t mItemPairs; diff --git a/indra/llui/llfolderview.cpp b/indra/llui/llfolderview.cpp index db4ab8487e5..758b4bb1765 100644 --- a/indra/llui/llfolderview.cpp +++ b/indra/llui/llfolderview.cpp @@ -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 @@ -1423,6 +1423,7 @@ bool LLFolderView::search(LLFolderViewItem* first_item, const std::string &searc } } + // Note: for inventory getSearchableName should already be 'upper' case. std::string current_item_label(search_item->getViewModelItem()->getSearchableName()); LLStringUtil::toUpper(current_item_label); auto search_string_length = llmin(upper_case_string.size(), current_item_label.size()); diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index fcc1964bd6d..c8ef4216778 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -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 @@ -190,7 +190,7 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) mItemHeight(p.item_height), mControlLabelRotation(0.f), mDragAndDropTarget(false), - mLabel(utf8str_to_wstring(p.name)), + mLabel(utf8str_to_wstring(p.name)), // will be immediately reset in postBuild() mRoot(p.root), mViewModelItem(p.listener), mIsMouseOverTitle(false), @@ -244,21 +244,19 @@ bool LLFolderViewItem::postBuild() llassert(vmi); // not supposed to happen, if happens, find out why and fix if (vmi) { - // getDisplayName() is expensive (due to internal getLabelSuffix() and name building) - // it also sets search strings so it requires a filter reset + // First getDisplayName() is expensive due to internal + // lazy getLabelSuffix(), it is however needed as it sets + // search string, which can later determine visibility. + // Refreshing a search string also requires a filter reset. mLabel = utf8str_to_wstring(vmi->getDisplayName()); mIsFavorite = vmi->isFavorite() && !vmi->isItemInTrash(); - setToolTip(vmi->getName()); // Dirty the filter flag of the model from the view (CHUI-849) vmi->dirtyFilter(); } - // Don't do full refresh on constructor if it is possible to avoid + // Don't do full refresh on constructor if it is possible to avoid, // it significantly slows down bulk view creation. - // Todo: Ideally we need to move getDisplayName() out of constructor as well. - // Like: make a logic that will let filter update search string, - // while LLFolderViewItem::arrange() updates visual part mSuffixNeedsRefresh = true; mLabelWidthDirty = true; return true; @@ -363,7 +361,6 @@ void LLFolderViewItem::refresh() mLabel = utf8str_to_wstring(vmi.getDisplayName()); mLabelFontBuffer.reset(); mIsFavorite = vmi.isFavorite() && !vmi.isItemInTrash(); - setToolTip(vmi.getName()); // icons are slightly expensive to get, can be optimized // see LLInventoryIcon::getIcon() mIcon = vmi.getIcon(); @@ -623,6 +620,19 @@ const std::string& LLFolderViewItem::getName( void ) const return getViewModelItem() ? getViewModelItem()->getName() : noName; } +const std::string LLFolderViewItem::getToolTip() const +{ + // Return the item name as tooltip without storing it + if (!LLView::sDebugUnicode) + { + if (const LLFolderViewModelItem* vmi = getViewModelItem()) + { + return vmi->getName(); + } + } + return LLView::getToolTip(); +} + // LLView functionality bool LLFolderViewItem::handleRightMouseDown( S32 x, S32 y, MASK mask ) { diff --git a/indra/llui/llfolderviewitem.h b/indra/llui/llfolderviewitem.h index 258a806b913..8d6de2fee30 100644 --- a/indra/llui/llfolderviewitem.h +++ b/indra/llui/llfolderviewitem.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 @@ -253,6 +253,11 @@ class LLFolderViewItem : public LLView // viewed. This method will ask the viewed object itself. const std::string& getName( void ) const; + // Override to provide lazy tooltip generation without memory overhead + // Inventory can consist of millions of items, yet most stay invisible, + // much less need to show a tooltip, so avoid storing tooltips. + virtual const std::string getToolTip() const; + // This method returns the label displayed on the view. This // method was primarily added to allow sorting on the folder // contents possible before the entire view has been constructed. diff --git a/indra/llui/llgestureautocompletehelper.cpp b/indra/llui/llgestureautocompletehelper.cpp new file mode 100644 index 00000000000..ca976fff10e --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.cpp @@ -0,0 +1,168 @@ +/** + * @file llgestureautocompletehelper.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * 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 + * 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 "llgestureautocompletehelper.h" + +#include "llfloater.h" +#include "llfloaterreg.h" +#include "llfocusmgr.h" +#include "lluictrl.h" + +constexpr char GESTURE_AUTOCOMPLETE_FLOATER[] = "gesture_autocomplete_picker"; + +bool LLGestureAutocompleteHelper::isActive(const LLUICtrl* ctrl) const +{ + return mHostHandle.get() == ctrl; +} + +void LLGestureAutocompleteHelper::showHelper( + LLUICtrl* host_ctrl, + const std::vector& rows, + size_t total, + const std::string& empty_text, + std::function commit_cb) +{ + if (mHelperHandle.isDead()) + { + LLFloater* helper_floater = LLFloaterReg::getInstance(GESTURE_AUTOCOMPLETE_FLOATER); + mHelperHandle = helper_floater->getHandle(); + mHelperCommitConn = helper_floater->setCommitCallback( + [this](LLUICtrl*, const LLSD& param) { onCommitGesture(param.asString()); }); + } + + setHostCtrl(host_ctrl); + mRows = rows; + mTotal = total; + mEmptyText = empty_text; + mGestureCommitCb = commit_cb; + + S32 floater_x, floater_y; + LLRect host_rect = host_ctrl->getRect(); + if (!host_ctrl->localPointToOtherView(0, host_rect.getHeight(), &floater_x, &floater_y, gFloaterView)) + { + LL_WARNS() << "Cannot show gesture autocomplete helper for non-floater controls." << LL_ENDL; + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + LLRect rect = helper_floater->getRect(); + rect.setLeftTopAndSize(floater_x, floater_y + rect.getHeight(), rect.getWidth(), rect.getHeight()); + helper_floater->setRect(rect); + + refreshPicker(); +} + +void LLGestureAutocompleteHelper::hideHelper(const LLUICtrl* ctrl) +{ + if (ctrl && !isActive(ctrl)) + { + return; + } + + setHostCtrl(nullptr); +} + +bool LLGestureAutocompleteHelper::handleKey(const LLUICtrl* ctrl, KEY key, MASK mask) +{ + if (mHelperHandle.isDead() || !isActive(ctrl)) + { + return false; + } + + return mHelperHandle.get()->handleKey(key, mask, true); +} + +void LLGestureAutocompleteHelper::onCommitGesture(const std::string& trigger) +{ + if (!mHostHandle.isDead() && mGestureCommitCb) + { + mGestureCommitCb(trigger); + } + + hideHelper(getHostCtrl()); +} + +void LLGestureAutocompleteHelper::refreshPicker() +{ + if (mHelperHandle.isDead()) + { + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + + if (helper_floater->isShown()) + { + helper_floater->onOpen(LLSD()); + } + else + { + helper_floater->openFloater(LLSD()); + } +} + +void LLGestureAutocompleteHelper::setHostCtrl(LLUICtrl* host_ctrl) +{ + const LLUICtrl* cur_host_ctrl = mHostHandle.get(); + + if (cur_host_ctrl != host_ctrl) + { + mHostCtrlFocusLostConn.disconnect(); + mHostHandle.markDead(); + mGestureCommitCb = {}; + mRows.clear(); + mEmptyText.clear(); + mTotal = 0; + + if (!mHelperHandle.isDead()) + { + mHelperHandle.get()->closeFloater(); + } + + if (host_ctrl) + { + mHostHandle = host_ctrl->getHandle(); + mHostCtrlFocusLostConn = host_ctrl->setFocusLostCallback( + [this](auto*) + { + // Scroll list grabs focus on click. + // Keep focus on the host when the click was ours. + LLFloater* helper_floater = mHelperHandle.get(); + if (helper_floater && gFocusMgr.childHasKeyboardFocus(helper_floater)) + { + if (LLUICtrl* host = getHostCtrl()) + { + host->setFocus(true); + } + return; + } + + hideHelper(getHostCtrl()); + }); + } + } +} diff --git a/indra/llui/llgestureautocompletehelper.h b/indra/llui/llgestureautocompletehelper.h new file mode 100644 index 00000000000..53000c0829e --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.h @@ -0,0 +1,83 @@ +/** + * @file llgestureautocompletehelper.h + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * 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 + * 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$ + */ + +#pragma once + +#include "llhandle.h" +#include "llsingleton.h" + +#include +#include +#include +#include + +class LLFloater; +class LLUICtrl; + +class LLGestureAutocompleteHelper : public LLSingleton +{ + LLSINGLETON(LLGestureAutocompleteHelper) {} + ~LLGestureAutocompleteHelper() override {} + +public: + struct Row + { + std::string value; + std::string trigger; + std::string name; + }; + + bool isActive(const LLUICtrl* ctrl) const; + void showHelper( + LLUICtrl* host_ctrl, + const std::vector& rows, + size_t total, + const std::string& empty_text, + std::function commit_cb); + void hideHelper(const LLUICtrl* ctrl = nullptr); + bool handleKey(const LLUICtrl* ctrl, KEY key, MASK mask); + void onCommitGesture(const std::string& trigger); + + const std::vector& rows() const { return mRows; } + size_t total() const { return mTotal; } + const std::string& emptyText() const { return mEmptyText; } + +protected: + void setHostCtrl(LLUICtrl* host_ctrl); + LLUICtrl* getHostCtrl() const { return mHostHandle.get(); } + +private: + void refreshPicker(); + + LLHandle mHostHandle; + LLHandle mHelperHandle; + boost::signals2::connection mHostCtrlFocusLostConn; + boost::signals2::connection mHelperCommitConn; + std::function mGestureCommitCb; + + std::vector mRows; + std::string mEmptyText; + size_t mTotal = 0; +}; diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index 9a88083a5d8..404e394ace5 100644 --- a/indra/llui/lllineeditor.cpp +++ b/indra/llui/lllineeditor.cpp @@ -493,12 +493,13 @@ void LLLineEditor::setCursor( S32 pos ) S32 pixels_after_scroll = findPixelNearestPos(); if( pixels_after_scroll > mTextRightEdge ) { - S32 width_chars_to_left = mGLFont->getWidth(mText.getWString().c_str(), 0, mScrollHPos); - S32 last_visible_char = mGLFont->maxDrawableChars(mText.getWString().c_str(), llmax(0.f, (F32)(mTextRightEdge - mTextLeftEdge + width_chars_to_left))); + const LLWString& wtext = mText.getWString(); + S32 width_chars_to_left = mGLFont->getWidth(wtext.c_str(), 0, mScrollHPos); + S32 last_visible_char = mGLFont->maxDrawableChars(wtext.c_str(), llmax(0.f, (F32)(mTextRightEdge - mTextLeftEdge + width_chars_to_left))); // character immediately to left of cursor should be last one visible (SCROLL_INCREMENT_ADD will scroll in more characters) // or first character if cursor is at beginning S32 new_last_visible_char = llmax(0, getCursor() - 1); - S32 min_scroll = mGLFont->firstDrawableChar(mText.getWString().c_str(), (F32)(mTextRightEdge - mTextLeftEdge), mText.length(), new_last_visible_char); + S32 min_scroll = mGLFont->firstDrawableChar(wtext.c_str(), (F32)(mTextRightEdge - mTextLeftEdge), mText.length(), new_last_visible_char); if (old_cursor_pos == last_visible_char) { mScrollHPos = llmin(mText.length(), llmax(min_scroll, mScrollHPos + SCROLL_INCREMENT_ADD)); diff --git a/indra/llui/llpanel.cpp b/indra/llui/llpanel.cpp index 2100b23783b..d09fc3e22df 100644 --- a/indra/llui/llpanel.cpp +++ b/indra/llui/llpanel.cpp @@ -489,58 +489,70 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu LL_RECORD_BLOCK_TIME(FTM_PANEL_SETUP); LLXMLNodePtr referenced_xml; - std::string xml_filename = mXMLFilename; + const std::string& xml_filename = mXMLFilename; // if the panel didn't provide a filename, check the node if (xml_filename.empty()) { - node->getAttributeString("filename", xml_filename); - setXMLFilename(xml_filename); + std::string temp_filename; + node->getAttributeString("filename", temp_filename); + setXMLFilename(temp_filename); } + // Cache singleton and filename to avoid repeated calls + LLUICtrlFactory* factory = LLUICtrlFactory::getInstance(); + + // Cache node name pointer to avoid repeated dereferencing + const LLStringTableEntry* node_name = node->getName(); + + // Cache registry to avoid repeated singleton access + const child_registry_t& registry = child_registry_t::instance(); + LLXUIParser parser; - if (!xml_filename.empty()) + if (!mXMLFilename.empty()) { if (output_node) { //if we are exporting, we want to export the current xml //not the referenced xml - parser.readXUI(node, params, LLUICtrlFactory::getInstance()->getCurFileName()); + parser.readXUI(node, params, factory->getCurFileName()); Params output_params(params); setupParamsForExport(output_params, parent); - output_node->setName(node->getName()->mString); + output_node->setName(node_name->mString); parser.writeXUI(output_node, output_params, LLInitParam::default_parse_rules(), &default_params); return true; } - LLUICtrlFactory::instance().pushFileName(xml_filename); + factory->pushFileName(mXMLFilename); LL_RECORD_BLOCK_TIME(FTM_EXTERNAL_PANEL_LOAD); - if (!LLUICtrlFactory::getLayeredXMLNode(xml_filename, referenced_xml)) + if (!LLUICtrlFactory::getLayeredXMLNode(mXMLFilename, referenced_xml)) { - LL_WARNS() << "Couldn't parse panel from: " << xml_filename << LL_ENDL; + LL_WARNS() << "Couldn't parse panel from: " << mXMLFilename << LL_ENDL; return false; } - parser.readXUI(referenced_xml, params, LLUICtrlFactory::getInstance()->getCurFileName()); + // Get filename after pushFileName + const std::string& updated_filename = factory->getCurFileName(); + parser.readXUI(referenced_xml, params, updated_filename); // add children using dimensions from referenced xml for consistent layout setShape(params.rect); - LLUICtrlFactory::createChildren(this, referenced_xml, child_registry_t::instance()); + LLUICtrlFactory::createChildren(this, referenced_xml, registry); - LLUICtrlFactory::instance().popFileName(); + factory->popFileName(); } // ask LLUICtrlFactory for filename, since xml_filename might be empty - parser.readXUI(node, params, LLUICtrlFactory::getInstance()->getCurFileName()); + parser.readXUI(node, params, factory->getCurFileName()); if (output_node) { Params output_params(params); setupParamsForExport(output_params, parent); - output_node->setName(node->getName()->mString); + output_node->setName(node_name->mString); parser.writeXUI(output_node, output_params, LLInitParam::default_parse_rules(), &default_params); } @@ -552,7 +564,7 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu } // add children - LLUICtrlFactory::createChildren(this, node, child_registry_t::instance(), output_node); + LLUICtrlFactory::createChildren(this, node, registry, output_node); // Connect to parent after children are built, because tab containers // do a reshape() on their child panels, which requires that the children diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp index e36fd45bb48..ad2f58db697 100644 --- a/indra/llui/llscrollcontainer.cpp +++ b/indra/llui/llscrollcontainer.cpp @@ -75,6 +75,7 @@ LLScrollContainer::Params::Params() max_auto_scroll_rate("max_auto_scroll_rate", 1000), max_auto_scroll_zone("max_auto_scroll_zone", 16), reserve_scroll_corner("reserve_scroll_corner", false), + keep_scroll_pos("keep_scroll_pos", false), size("size", -1) {} @@ -93,8 +94,12 @@ LLScrollContainer::LLScrollContainer(const LLScrollContainer::Params& p) mMaxAutoScrollRate(p.max_auto_scroll_rate), mMaxAutoScrollZone(p.max_auto_scroll_zone), mScrolledView(NULL), + mKeepScrollPos(p.keep_scroll_pos), mSize(p.size) { + mStoredDocPos[VERTICAL] = 0; + mStoredDocPos[HORIZONTAL] = 0; + static LLUICachedControl scrollbar_size_control ("UIScrollbarSize", 0); S32 scrollbar_size = (mSize == -1 ? scrollbar_size_control : mSize); @@ -605,6 +610,22 @@ void LLScrollContainer::updateScroll() calcVisibleSize( &visible_width, &visible_height, &show_h_scrollbar, &show_v_scrollbar ); S32 border_width = getBorderWidth(); + + // Remember the position only while the scrollbar is genuinely showing scrollable + // content, so a transient empty pass (e.g. a list clearing before it repopulates) + // cannot overwrite it with 0. + if (mKeepScrollPos) + { + if (show_v_scrollbar && mScrollbar[VERTICAL]->getVisible()) + { + mStoredDocPos[VERTICAL] = mScrollbar[VERTICAL]->getDocPos(); + } + if (show_h_scrollbar && mScrollbar[HORIZONTAL]->getVisible()) + { + mStoredDocPos[HORIZONTAL] = mScrollbar[HORIZONTAL]->getDocPos(); + } + } + if( show_v_scrollbar ) { if( doc_rect.mTop < getRect().getHeight() - border_width ) @@ -673,6 +694,18 @@ void LLScrollContainer::updateScroll() mScrollbar[VERTICAL]->setDocSize( doc_height ); mScrollbar[VERTICAL]->setPageSize( visible_height ); + + if (mKeepScrollPos) + { + if (show_v_scrollbar) + { + mScrollbar[VERTICAL]->setDocPos(mStoredDocPos[VERTICAL]); + } + if (show_h_scrollbar) + { + mScrollbar[HORIZONTAL]->setDocPos(mStoredDocPos[HORIZONTAL]); + } + } } // end updateScroll void LLScrollContainer::setBorderVisible(bool b) diff --git a/indra/llui/llscrollcontainer.h b/indra/llui/llscrollcontainer.h index 859750d71c1..7761824dbb0 100644 --- a/indra/llui/llscrollcontainer.h +++ b/indra/llui/llscrollcontainer.h @@ -64,7 +64,8 @@ class LLScrollContainer : public LLUICtrl reserve_scroll_corner, border_visible, hide_scrollbar, - ignore_arrow_keys; + ignore_arrow_keys, + keep_scroll_pos; Optional min_auto_scroll_rate, max_auto_scroll_rate; Optional max_auto_scroll_zone; @@ -138,6 +139,8 @@ class LLScrollContainer : public LLUICtrl void calcVisibleSize( S32 *visible_width, S32 *visible_height, bool* show_h_scrollbar, bool* show_v_scrollbar ) const; LLScrollbar* mScrollbar[ORIENTATION_COUNT]; + S32 mStoredDocPos[ORIENTATION_COUNT]; + bool mKeepScrollPos; S32 mSize; bool mIsOpaque; LLUIColor mBackgroundColor; diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 0521853b02a..802367720fa 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -2288,6 +2288,7 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_url) registrar.add("Url.AddFriend", boost::bind(&LLUrlAction::addFriend, url)); registrar.add("Url.RemoveFriend", boost::bind(&LLUrlAction::removeFriend, url)); registrar.add("Url.ReportAbuse", boost::bind(&LLUrlAction::reportAbuse, url)); + registrar.add("Url.ReportAbuseObj", boost::bind(&LLUrlAction::reportAbuseObj, url)); registrar.add("Url.SendIM", boost::bind(&LLUrlAction::sendIM, url)); registrar.add("Url.ZoomInObject", boost::bind(&LLUrlAction::zoomInObject, url)); registrar.add("Url.ShowOnMap", boost::bind(&LLUrlAction::showLocationOnMap, url)); diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index bc39d9732c5..62f984b8fc9 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -327,6 +327,7 @@ class LLTextBase public: friend class LLTextSegment; friend class LLNormalTextSegment; + friend class LLEmbeddedItemSegment; friend class LLUICtrlFactory; typedef boost::signals2::signal is_friend_signal_t; diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 7689b933741..5f1302df88a 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -61,6 +61,7 @@ #include "lltooltip.h" #include "llmenugl.h" #include "llchatmentionhelper.h" +#include "llgestureautocompletehelper.h" #include #include "llcombobox.h" @@ -1950,7 +1951,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) // not handled and let the parent take care of field movement. if (KEY_TAB == key && mTabsToNextField) { - return mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask); + return (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) + || LLGestureAutocompleteHelper::instance().handleKey(this, key, mask); } if (mReadOnly && mScroller) @@ -1964,7 +1966,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) if (!mReadOnly) { if ((mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) || - (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask))) + (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) || + LLGestureAutocompleteHelper::instance().handleKey(this, key, mask)) { return true; } diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index d9742db34d6..43f47a404c4 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -208,6 +208,7 @@ class LLTextEditor : void setShowContextMenu(bool show) { mShowContextMenu = show; } bool getShowContextMenu() const { return mShowContextMenu; } + void showContextMenu(S32 x, S32 y); void showEmojiHelper(); void hideEmojiHelper(); @@ -219,7 +220,6 @@ class LLTextEditor : LLWString getConvertedText() const; protected: - void showContextMenu(S32 x, S32 y); void drawPreeditMarker(); void removeCharOrTab(); diff --git a/indra/llui/lltextvalidate.cpp b/indra/llui/lltextvalidate.cpp index 9a087d82307..1f2e7e65dbf 100644 --- a/indra/llui/lltextvalidate.cpp +++ b/indra/llui/lltextvalidate.cpp @@ -434,7 +434,7 @@ class ValidatorASCIINoLeadingSpace : public ValidatorASCII } validatorASCIINoLeadingSpaceImpl; Validator validateASCIINoLeadingSpace(validatorASCIINoLeadingSpaceImpl); -class ValidatorASCIIWithNewLine : public ValidatorImpl +class ValidatorASCIIWithNewLineNoPipe : public ValidatorImpl { // Used for multiline text stored on the server. // Example is landmark description in Places SP. @@ -446,9 +446,9 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl { CHAR ch = str[len]; - if ((ch < 0x20 && ch != 0xA) || ch > 0x7f) + if ((ch < 0x20 && ch != 0xA) || ch > 0x7f || ch == '|') { - return setError("Validator_ShouldBeNewLineOrASCII", LLSD().with("NR", len + 1).with("CH", llsd(ch))); + return setError("Validator_ShouldBeNewLineOrASCIINoPipe", LLSD().with("NR", len + 1).with("CH", llsd(ch))); } } @@ -458,8 +458,8 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl public: /*virtual*/ bool validate(const std::string& str) override { return validate(str); } /*virtual*/ bool validate(const LLWString& str) override { return validate(str); } -} validatorASCIIWithNewLineImpl; -Validator validateASCIIWithNewLine(validatorASCIIWithNewLineImpl); +} validatorASCIIWithNewLineNoPipeImpl; +Validator validateASCIIWithNewLineNoPipe(validatorASCIIWithNewLineNoPipeImpl); void Validators::declareValues() { @@ -472,7 +472,7 @@ void Validators::declareValues() declare("alpha_num_space", validateAlphaNumSpace); declare("ascii_printable_no_pipe", validateASCIIPrintableNoPipe); declare("ascii_printable_no_space", validateASCIIPrintableNoSpace); - declare("ascii_with_newline", validateASCIIWithNewLine); + declare("ascii_with_newline_no_pipe", validateASCIIWithNewLineNoPipe); } } // namespace LLTextValidate diff --git a/indra/llui/lltextvalidate.h b/indra/llui/lltextvalidate.h index 096c28b4481..a95f79bfcb9 100644 --- a/indra/llui/lltextvalidate.h +++ b/indra/llui/lltextvalidate.h @@ -88,7 +88,7 @@ namespace LLTextValidate extern Validator validateASCIIPrintableNoSpace; extern Validator validateASCII; extern Validator validateASCIINoLeadingSpace; - extern Validator validateASCIIWithNewLine; + extern Validator validateASCIIWithNewLineNoPipe; // Add available validators to the internal map struct Validators : public LLInitParam::TypeValuesHelper diff --git a/indra/llui/llurlaction.cpp b/indra/llui/llurlaction.cpp index 8b320b59cc3..61a529c5d23 100644 --- a/indra/llui/llurlaction.cpp +++ b/indra/llui/llurlaction.cpp @@ -269,13 +269,22 @@ void LLUrlAction::reportAbuse(std::string url) } } +void LLUrlAction::reportAbuseObj(std::string url) +{ + std::string object_id = getObjectId(url); + if (LLUUID::validate(object_id)) + { + executeSLURL("secondlife:///app/object/" + object_id + "/reportAbuse"); + } +} + void LLUrlAction::blockObject(std::string url) { std::string object_id = getObjectId(url); std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/block/" + LLURI::escape(object_name)); + executeSLURL("secondlife:///app/object/" + object_id + "/block/" + LLURI::escape(object_name)); } } @@ -285,6 +294,6 @@ void LLUrlAction::unblockObject(std::string url) std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/unblock/" + object_name); + executeSLURL("secondlife:///app/object/" + object_id + "/unblock/" + LLURI::escape(object_name)); } } diff --git a/indra/llui/llurlaction.h b/indra/llui/llurlaction.h index c4cfd0f3fb1..d110fe56d37 100644 --- a/indra/llui/llurlaction.h +++ b/indra/llui/llurlaction.h @@ -89,6 +89,7 @@ class LLUrlAction static void addFriend(std::string url); static void removeFriend(std::string url); static void reportAbuse(std::string url); + static void reportAbuseObj(std::string url); static void blockObject(std::string url); static void unblockObject(std::string url); diff --git a/indra/llui/llxyvector.cpp b/indra/llui/llxyvector.cpp index 1521823ce2c..b3c38bf82d2 100644 --- a/indra/llui/llxyvector.cpp +++ b/indra/llui/llxyvector.cpp @@ -211,7 +211,16 @@ void LLXYVector::draw() mGhostY = pointY; } - if (abs(mValueX) >= mIncrementX || abs(mValueY) >= mIncrementY) + S32 dx = abs(pointX - centerX); + S32 dy = abs(pointY - centerY); + bool draw_arrow = + (abs(mValueX) >= mIncrementX || abs(mValueY) >= mIncrementY) + && (dx >= 1 || dy >= 1); // At least 1 pixel displacement + + // Todo: Arrow doesn't display well with small values. + // Ex: (0.1, 0.05) will point to the right, as if it is (0.1, 0.0) + // as position will be offset by a single pixes on X, and 0 pixels on Y. + if (draw_arrow) { // draw the vector arrow drawArrow(centerX, centerY, pointX, pointY, mArrowColor); diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index a286f75f424..6c809f2743c 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" @@ -134,7 +134,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 +252,47 @@ 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) + { + // 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. +} + // // LLWebRTCImpl implementation // LLWebRTCImpl::LLWebRTCImpl(LLWebRTCLogCallback* logCallback) : + mEnv(webrtc::CreateEnvironment(webrtc::CreateDefaultTaskQueueFactory())), mLogSink(new LLWebRTCLogSink(logCallback)), mPeerCustomProcessor(nullptr), mMute(true), + mVoiceEnabled(false), mTuningMode(false), mDevicesDeploying(0), - mGain(0.0f) + mGain(0.0f), + mBuiltinNS(false), + mBuiltinAGC(false), + mBuiltinAEC(false) { } @@ -273,8 +305,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 +320,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 +340,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 +387,6 @@ void LLWebRTCImpl::init() { if (mDeviceModule) { - mDeviceModule->EnableBuiltInAEC(false); updateDevices(); } }); @@ -379,10 +421,9 @@ void LLWebRTCImpl::terminate() { if (mDeviceModule) { - mDeviceModule->Terminate(); + mDeviceModule->ForceTerminate(); } mDeviceModule = nullptr; - mTaskQueueFactory = nullptr; }); // In case peer connections still somehow have jobs in workers, @@ -395,47 +436,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,20 +528,25 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer) } } -// must be run in the worker thread. -void LLWebRTCImpl::workerDeployDevices() +// 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) { @@ -484,8 +562,6 @@ void LLWebRTCImpl::workerDeployDevices() } } - mDeviceModule->StopPlayout(); - mDeviceModule->ForceStopRecording(); #if WEBRTC_WIN if (recordingDevice < 0) { @@ -500,13 +576,32 @@ void LLWebRTCImpl::workerDeployDevices() #endif mDeviceModule->InitMicrophone(); mDeviceModule->SetStereoRecording(false); + // 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(); +} + +// 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()) + { + 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) { @@ -537,16 +632,29 @@ void LLWebRTCImpl::workerDeployDevices() mDeviceModule->InitSpeaker(); mDeviceModule->SetStereoPlayout(true); mDeviceModule->InitPlayout(); + mDeviceModule->StartPlayout(); +} - if ((!mMute && mPeerConnections.size()) || mTuningMode) +// 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) { - mDeviceModule->ForceStartRecording(); + return; } - if (!mTuningMode) - { - mDeviceModule->StartPlayout(); - } + // 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] { @@ -588,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() { @@ -636,6 +773,8 @@ void LLWebRTCImpl::updateDevices() { observer->OnDevicesChanged(mPlayoutDeviceList, mRecordingDeviceList); } + + deployDevices(); } void LLWebRTCImpl::OnDevicesUpdated() @@ -658,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] { @@ -729,39 +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) - { - mWorkerThread->PostDelayedTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - mDeviceModule->ForceStopRecording(); - } - }, - webrtc::TimeDelta::Millis(delay_ms)); - } - else - { - mWorkerThread->PostTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - mDeviceModule->InitRecording(); - mDeviceModule->ForceStartRecording(); - } - }); - } } // @@ -770,8 +893,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 +901,13 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() } mPeerConnections.emplace_back(peerConnection); + // 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(); return peerConnection.get(); @@ -795,10 +924,45 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn if (mPeerConnections.empty()) { intSetMute(true); + // 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->StopPlayout(); + if (!mVoiceEnabled) + { + mDeviceModule->ForceStopRecording(); + } + } + }); } } } +void LLWebRTCImpl::startPlayout() +{ + // 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]() + { + workerStartPlayout(); + }); +} + // // LLWebRTCPeerConnectionImpl implementation. @@ -806,7 +970,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), @@ -841,22 +1006,23 @@ void LLWebRTCPeerConnectionImpl::init(LLWebRTCImpl * webrtc_impl) void LLWebRTCPeerConnectionImpl::terminate() { mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this]() + [self]() { - if (mPeerConnection) + if (self->mPeerConnection) { - if (mDataChannel) + if (self->mDataChannel) { { - mDataChannel->Close(); - mDataChannel = nullptr; + self->mDataChannel->Close(); + self->mDataChannel = nullptr; } } // to remove 'Secondlife is recording' icon from taskbar // if user was speaking - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto& sender : senders) { auto track = sender->track(); @@ -866,24 +1032,24 @@ void LLWebRTCPeerConnectionImpl::terminate() } } - mPeerConnection->Close(); - if (mLocalStream) + self->mPeerConnection->Close(); + if (self->mLocalStream) { - auto tracks = mLocalStream->GetAudioTracks(); + auto tracks = self->mLocalStream->GetAudioTracks(); for (auto& track : tracks) { - mLocalStream->RemoveTrack(track); + self->mLocalStream->RemoveTrack(track); } - mLocalStream = nullptr; + self->mLocalStream = nullptr; } - mPeerConnection = nullptr; + self->mPeerConnection = nullptr; - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnPeerConnectionClosed(); } } - mPendingJobs--; + self->mPendingJobs--; }); } @@ -906,8 +1072,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti mAnswerReceived = false; mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this,options]() + [self,options]() { webrtc::PeerConnectionInterface::RTCConfiguration config; for (auto server : options.mServers) @@ -926,42 +1093,42 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti config.set_min_port(60000); config.set_max_port(60100); - webrtc::PeerConnectionDependencies pc_dependencies(this); + webrtc::PeerConnectionDependencies pc_dependencies(self.get()); // Other thread manages mPeerConnectionFactory's lifetime and it can be reset // at any momment, create own scoped_refptr (atomic). - webrtc::scoped_refptr peer_connection_factory = mPeerConnectionFactory; + webrtc::scoped_refptr peer_connection_factory = self->mPeerConnectionFactory; if (peer_connection_factory == nullptr) { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection, factory doesn't exist"; // Too early? - mPendingJobs--; + self->mPendingJobs--; return; } auto error_or_peer_connection = peer_connection_factory->CreatePeerConnectionOrError(config, std::move(pc_dependencies)); if (error_or_peer_connection.ok()) { - mPeerConnection = std::move(error_or_peer_connection.value()); + self->mPeerConnection = std::move(error_or_peer_connection.value()); } else { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection: " << error_or_peer_connection.error().message(); - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnRenegotiationNeeded(); } - mPendingJobs--; + self->mPendingJobs--; return; } webrtc::DataChannelInit init; init.ordered = true; - auto data_channel_or_error = mPeerConnection->CreateDataChannelOrError("SLData", &init); + auto data_channel_or_error = self->mPeerConnection->CreateDataChannelOrError("SLData", &init); if (data_channel_or_error.ok()) { - mDataChannel = std::move(data_channel_or_error.value()); + self->mDataChannel = std::move(data_channel_or_error.value()); - mDataChannel->RegisterObserver(this); + self->mDataChannel->RegisterObserver(self.get()); } webrtc::AudioOptions audioOptions; @@ -970,16 +1137,16 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti audioOptions.noise_suppression = true; audioOptions.init_recording_on_send = false; - mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); + self->mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); webrtc::scoped_refptr audio_track( peer_connection_factory->CreateAudioTrack("SLAudio", peer_connection_factory->CreateAudioSource(audioOptions).get())); audio_track->set_enabled(false); - mLocalStream->AddTrack(audio_track); + self->mLocalStream->AddTrack(audio_track); - mPeerConnection->AddTrack(audio_track, {"SLStream"}); + self->mPeerConnection->AddTrack(audio_track, {"SLStream"}); - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto &sender : senders) { @@ -995,7 +1162,7 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti sender->SetParameters(params); } - auto receivers = mPeerConnection->GetReceivers(); + auto receivers = self->mPeerConnection->GetReceivers(); for (auto &receiver : receivers) { webrtc::RtpParameters params; @@ -1011,9 +1178,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti } webrtc::PeerConnectionInterface::RTCOfferAnswerOptions offerOptions; - this->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. - mPeerConnection->CreateOffer(this, offerOptions); - mPendingJobs--; + self->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. + self->mPeerConnection->CreateOffer(self.get(), offerOptions); + self->mPendingJobs--; }); return true; @@ -1090,14 +1257,15 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this, force_reset, enable]() + [self, force_reset, enable]() { - if (mPeerConnection) + if (self->mPeerConnection) { - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); - RTC_LOG(LS_INFO) << __FUNCTION__ << (mMute ? "disabling" : "enabling") << " streams count " << senders.size(); + RTC_LOG(LS_INFO) << __FUNCTION__ << (self->mMute ? "disabling" : "enabling") << " streams count " << senders.size(); for (auto &sender : senders) { auto track = sender->track(); @@ -1113,7 +1281,7 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) track->set_enabled(enable); } } - mPendingJobs--; + self->mPendingJobs--; } }); } @@ -1252,16 +1420,25 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf { case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { + // 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++; - mWebRTCImpl->PostWorkerTask([this]() { - for (auto &observer : mSignalingObserverList) + webrtc::scoped_refptr self(this); + mWebRTCImpl->PostWorkerTask([self]() + { + for (auto &observer : self->mSignalingObserverList) { - observer->OnAudioEstablished(this); + observer->OnAudioEstablished(self.get()); } - mPendingJobs--; + self->mPendingJobs--; }); break; } + case webrtc::PeerConnectionInterface::PeerConnectionState::kFailed: { for (auto &observer : mSignalingObserverList) diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h index e76e708f0ce..821400cfe89 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 bd7a2e0bcfc..28d25b8d515 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -180,7 +180,7 @@ class LLWebRTCAudioTransport : public webrtc::AudioTransport 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 @@ class LLWebRTCAudioDeviceModule : public webrtc::AudioDeviceModule } 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(); } @@ -323,30 +329,8 @@ class LLWebRTCAudioDeviceModule : public webrtc::AudioDeviceModule // 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 +420,6 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO // void setAudioConfig(LLWebRTCDeviceInterface::AudioConfig config = LLWebRTCDeviceInterface::AudioConfig()) override; - void refreshDevices() override; void setDevicesObserver(LLWebRTCDevicesObserver *observer) override; @@ -445,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; @@ -522,9 +507,23 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCPeerConnectionInterface* newPeerConnection(); void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection); + // 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 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. + void workerDisableBuiltInAudioProcessing(); + + LLWebRTCLogSink* mLogSink; // The native webrtc threads @@ -537,10 +536,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 +543,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; @@ -557,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; @@ -580,7 +581,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, { public: - LLWebRTCPeerConnectionImpl(); + LLWebRTCPeerConnectionImpl(const webrtc::Environment& env); ~LLWebRTCPeerConnectionImpl(); void init(LLWebRTCImpl * webrtc_impl); @@ -659,7 +660,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void gatherConnectionStats() override; protected: - + const webrtc::Environment mEnv; LLWebRTCImpl * mWebRTCImpl; webrtc::scoped_refptr mPeerConnectionFactory; diff --git a/indra/llwindow/CMakeLists.txt b/indra/llwindow/CMakeLists.txt index 08b3df87abe..3dae2d1067c 100644 --- a/indra/llwindow/CMakeLists.txt +++ b/indra/llwindow/CMakeLists.txt @@ -48,6 +48,47 @@ set(viewer_HEADER_FILES llmousehandler.h ) +# Platform-specific files for IDE visibility (excluded from build on other platforms) +set(macosx_SOURCE_FILES + llkeyboardmacosx.cpp + llwindowmacosx.cpp + llwindowmacosx-objc.mm + llopenglview-objc.mm + ) + +set(macosx_HEADER_FILES + llkeyboardmacosx.h + llwindowmacosx.h + llwindowmacosx-objc.h + llwindowmacosx_iokit.h + llopenglview-objc.h + llappdelegate-objc.h + ) + +set(linux_SOURCE_FILES + llkeyboardsdl.cpp + llwindowsdl.cpp + ) + +set(linux_HEADER_FILES + llkeyboardsdl.h + llwindowsdl.h + ) + +set(windows_SOURCE_FILES + llwindowwin32.cpp + lldxhardware.cpp + llkeyboardwin32.cpp + lldragdropwin32.cpp + ) + +set(windows_HEADER_FILES + llwindowwin32.h + lldxhardware.h + llkeyboardwin32.h + lldragdropwin32.h + ) + set(llwindow_LINK_LIBRARIES llcommon llimage @@ -63,14 +104,8 @@ set(llwindow_LINK_LIBRARIES # Libraries on which this library depends, needed for Linux builds # Sort by high-level to low-level if (LINUX) - list(APPEND viewer_SOURCE_FILES - llkeyboardsdl.cpp - llwindowsdl.cpp - ) - list(APPEND viewer_HEADER_FILES - llkeyboardsdl.h - llwindowsdl.h - ) + list(APPEND viewer_SOURCE_FILES ${linux_SOURCE_FILES}) + list(APPEND viewer_HEADER_FILES ${linux_HEADER_FILES}) if (BUILD_HEADLESS) set(llwindowheadless_LINK_LIBRARIES @@ -88,19 +123,8 @@ if (LINUX) endif (LINUX) if (DARWIN) - list(APPEND llwindow_SOURCE_FILES - llkeyboardmacosx.cpp - llwindowmacosx.cpp - llwindowmacosx-objc.mm - llopenglview-objc.mm - ) - list(APPEND llwindow_HEADER_FILES - llkeyboardmacosx.h - llwindowmacosx.h - llwindowmacosx-objc.h - llopenglview-objc.h - llappdelegate-objc.h - ) + list(APPEND llwindow_SOURCE_FILES ${macosx_SOURCE_FILES}) + list(APPEND llwindow_HEADER_FILES ${macosx_HEADER_FILES}) # We use a bunch of deprecated system APIs. set_source_files_properties( @@ -113,18 +137,8 @@ endif (DARWIN) if (WINDOWS) - list(APPEND llwindow_SOURCE_FILES - llwindowwin32.cpp - lldxhardware.cpp - llkeyboardwin32.cpp - lldragdropwin32.cpp - ) - list(APPEND llwindow_HEADER_FILES - llwindowwin32.h - lldxhardware.h - llkeyboardwin32.h - lldragdropwin32.h - ) + list(APPEND llwindow_SOURCE_FILES ${windows_SOURCE_FILES}) + list(APPEND llwindow_HEADER_FILES ${windows_HEADER_FILES}) list(APPEND llwindow_LINK_LIBRARIES comdlg32 # Common Dialogs for ChooseColor ole32 @@ -167,10 +181,48 @@ endif (llwindow_HEADER_FILES) list(APPEND viewer_SOURCE_FILES ${viewer_HEADER_FILES}) + # Collect all platform files for IDE visibility, excluding already-added current platform + set(ide_visibility_files "") + if (NOT DARWIN) + list(APPEND ide_visibility_files ${macosx_SOURCE_FILES} ${macosx_HEADER_FILES}) + endif() + if (NOT LINUX) + list(APPEND ide_visibility_files ${linux_SOURCE_FILES} ${linux_HEADER_FILES}) + endif() + if (NOT WINDOWS) + list(APPEND ide_visibility_files ${windows_SOURCE_FILES} ${windows_HEADER_FILES}) + endif() + add_library (llwindow ${llwindow_SOURCE_FILES} ${viewer_SOURCE_FILES} + # Add non-platform files for IDE visibility only + ${ide_visibility_files} + ) + +# Mark non-platform files as excluded from build +if (WINDOWS) + set_source_files_properties( + ${macosx_SOURCE_FILES} + ${linux_SOURCE_FILES} + PROPERTIES + HEADER_FILE_ONLY TRUE + ) +elseif (DARWIN) + set_source_files_properties( + ${windows_SOURCE_FILES} + ${linux_SOURCE_FILES} + PROPERTIES + HEADER_FILE_ONLY TRUE + ) +elseif (LINUX) + set_source_files_properties( + ${macosx_SOURCE_FILES} + ${windows_SOURCE_FILES} + PROPERTIES + HEADER_FILE_ONLY TRUE ) +endif() if (SDL_FOUND) set_property(TARGET llwindow diff --git a/indra/llwindow/llwindowcallbacks.cpp b/indra/llwindow/llwindowcallbacks.cpp index 7331f50ba0c..8d1eebe33d4 100644 --- a/indra/llwindow/llwindowcallbacks.cpp +++ b/indra/llwindow/llwindowcallbacks.cpp @@ -68,6 +68,18 @@ void LLWindowCallbacks::handleMouseLeave(LLWindow *window) return; } +void LLWindowCallbacks::handlePreCloseRequest() +{ +} + +void LLWindowCallbacks::handleCloseRequestCanceled() +{ +} + +void LLWindowCallbacks::handleSuspendRequest() +{ +} + bool LLWindowCallbacks::handleCloseRequest(LLWindow *window, bool from_user) { //allow the window to close diff --git a/indra/llwindow/llwindowcallbacks.h b/indra/llwindow/llwindowcallbacks.h index 59dcdd3adee..390e3ff93a5 100644 --- a/indra/llwindow/llwindowcallbacks.h +++ b/indra/llwindow/llwindowcallbacks.h @@ -41,6 +41,10 @@ class LLWindowCallbacks virtual bool handleMouseDown(LLWindow *window, LLCoordGL pos, MASK mask); virtual bool handleMouseUp(LLWindow *window, LLCoordGL pos, MASK mask); virtual void handleMouseLeave(LLWindow *window); + // Called before close request is processed (ex: to create marker file in case OS is about to kill app). + virtual void handlePreCloseRequest(); + virtual void handleCloseRequestCanceled(); + virtual void handleSuspendRequest(); // return true to allow window to close, which will then cause handleQuit to be called virtual bool handleCloseRequest(LLWindow *window, bool from_user); virtual bool handleSessionExit(LLWindow* window); diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp index f8920318d31..1dd795cd3a4 100644 --- a/indra/llwindow/llwindowmacosx.cpp +++ b/indra/llwindow/llwindowmacosx.cpp @@ -44,7 +44,7 @@ #include #include -#include +#include "llwindowmacosx_iokit.h" #include #include #include @@ -2379,7 +2379,7 @@ bool LLWindowMacOSX::getInputDevices(U32 device_type_filter, io_iterator_t io_iter = 0; // create an IO object iterator - result = IOServiceGetMatchingServices( kIOMasterPortDefault, device_dict_ref, &io_iter ); + result = IOServiceGetMatchingServices( kLLIOMainPort, device_dict_ref, &io_iter ); if ( kIOReturnSuccess != result ) { LL_WARNS("Joystick") << "IOServiceGetMatchingServices failed" << LL_ENDL; diff --git a/indra/llwindow/llwindowmacosx_iokit.h b/indra/llwindow/llwindowmacosx_iokit.h new file mode 100644 index 00000000000..a6be2c86ef4 --- /dev/null +++ b/indra/llwindow/llwindowmacosx_iokit.h @@ -0,0 +1,35 @@ +/** + * @file llwindowmacosx_iokit.h + * @brief IOKit compatibility for macOS deployment target differences + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, 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$ + */ + +#pragma once +#include + +// kIOMainPortDefault is the macOS 12+ rename of kIOMasterPortDefault. +#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000 +static const mach_port_t kLLIOMainPort = kIOMainPortDefault; +#else +static const mach_port_t kLLIOMainPort = kIOMasterPortDefault; +#endif diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index c185fc6c4ac..77023b6ca6d 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -508,6 +508,7 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, : LLWindow(callbacks, fullscreen, flags), mAbsoluteCursorPosition(false), + mReceivedSCClose(false), mMaxGLVersion(max_gl_version), mMaxCores(max_cores) { @@ -1420,7 +1421,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo catch (...) { LOG_UNHANDLED_EXCEPTION("ChoosePixelFormat"); - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1431,7 +1432,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!DescribePixelFormat(mhDC, pixel_format, sizeof(PIXELFORMATDESCRIPTOR), &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1469,7 +1470,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!SetPixelFormat(mhDC, pixel_format, &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1477,14 +1478,14 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!(mhRC = SafeCreateContext(mhDC))) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } if (!wglMakeCurrent(mhDC, mhRC)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1690,14 +1691,14 @@ const S32 max_format = (S32)num_formats - 1; if (!mhDC) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBDevContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBDevContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } if (!SetPixelFormat(mhDC, pixel_format, &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1729,7 +1730,7 @@ const S32 max_format = (S32)num_formats - 1; { LL_WARNS("Window") << "No wgl_ARB_pixel_format extension!" << LL_ENDL; // cannot proceed without wgl_ARB_pixel_format extension, shutdown same as any other gGLManager.initGL() failure - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1738,7 +1739,7 @@ const S32 max_format = (S32)num_formats - 1; if (!DescribePixelFormat(mhDC, pixel_format, sizeof(PIXELFORMATDESCRIPTOR), &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1760,14 +1761,14 @@ const S32 max_format = (S32)num_formats - 1; if (!wglMakeCurrent(mhDC, mhRC)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } if (!gGLManager.initGL()) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1975,7 +1976,7 @@ void* LLWindowWin32::createSharedContext() if (!rc && !(rc = wglCreateContext(mhDC))) { close(); - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); } return rc; @@ -2433,6 +2434,15 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ update_width, update_height)); break; } + case WM_ERASEBKGND: + { + RECT client_rect; + if (GetClientRect(h_wnd, &client_rect)) + { + FillRect((HDC)w_param, &client_rect, (HBRUSH)GetStockObject(BLACK_BRUSH)); + } + return 1; + } case WM_PARENTNOTIFY: { break; @@ -2524,8 +2534,15 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_SYSCOMMAND: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_SYSCOMMAND"); - switch (w_param) + switch (w_param & 0xFFF0) { + case SC_CLOSE: + // User clicked close from system menu/taskbar or 'end process' from task manager + // Do nothing, will cause WM_CLOSE. + // If we don't get this message before WM_CLOSE, we are likely getting + // a kill from some external program. Win11 task manager Does cause SC_CLOSE. + window_imp->mReceivedSCClose = true; + break; case SC_KEYMENU: // Disallow the ALT key from triggering the default system menu. return 0; @@ -2540,9 +2557,30 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_CLOSE: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_CLOSE"); - // todo: WM_CLOSE can be caused by user and by task manager, - // distinguish these cases. - // For now assume it is always user. + + window_imp->mCallbacks->handlePreCloseRequest(); // mark app as potentially closing + if (!window_imp->mReceivedSCClose) + { + // Some external program is trying to close the app. + // Assume that it's going to destroy process if it fails + // and try to fast-quit without confirmation or cleanup. + window_imp->post([=]() + { + // Check if app needs cleanup or can be closed immediately. + if (window_imp->mCallbacks->handleSessionExit(window_imp)) + { + // Get the app to initiate cleanup. + window_imp->mCallbacks->handleQuit(window_imp); + } + }); + return 0; + } + window_imp->mReceivedSCClose = false; + + // There is no way to tell the difference between a user issued + // WM_CLOSE or task manager's WM_CLOSE. + // Assume it is a user and ask for confirmation, but create a marker file. + // If App keeps doing something after a second, or gets 'destroy' message clear the marker. window_imp->post([=]() { // Will the app allow the window to close? @@ -2564,6 +2602,16 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ } return 0; } + case WM_NCDESTROY: + LL_INFOS("Window") << "Received WM_NCDESTROY" << LL_ENDL; + break; + case WM_WTSSESSION_CHANGE: + { + // Detects Remote Desktop disconnects, fast user switching, session logoff + // w_param: WTS_CONSOLE_CONNECT, WTS_CONSOLE_DISCONNECT, WTS_SESSION_LOGOFF, etc. + LL_INFOS("Window") << "Received WM_WTSSESSION_CHANGE with wParam: " << (U32)w_param << LL_ENDL; + break; + } case WM_QUERYENDSESSION: { // Generally means that OS is going to shut down or user is going to log off. @@ -2585,8 +2633,10 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ || (end_session_flags & ENDSESSION_CRITICAL) // will shutdown regardless of app state || (end_session_flags & ENDSESSION_LOGOFF)) // logoff, can delay shutdown { + window_imp->mCallbacks->handlePreCloseRequest(); // mark app as closing window_imp->post([=]() { + LL_INFOS("Window") << "Shutting down due to session terminating" << LL_ENDL; // Check if app needs cleanup or can be closed immediately. if (window_imp->mCallbacks->handleSessionExit(window_imp)) { @@ -2605,6 +2655,192 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ // if session is ending OS is going to take care of it. return 0; } + case WM_POWERBROADCAST: + { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POWERBROADCAST"); + switch (w_param) + { + case PBT_APMSUSPEND: + LL_INFOS("Window") << "System is suspending (sleep/hibernate)" << LL_ENDL; + // System is about to enter sleep or hibernation + // Viewer can't function in hibernation, try to shut down. + // The system allows approximately two seconds for an + // application to handle this notification. + + // Mark app as potentially closing, to minimize issues if OS does not recover. + window_imp->mCallbacks->handlePreCloseRequest(); + window_imp->post([=]() + { + window_imp->mCallbacks->handleSuspendRequest(); + }); + // Window thread normally doesn't block main thread, but OS can suspend + // immediately if we don't wait. + // Keep OS from suspending to give a chance to send stats. + ms_sleep(1000); + return TRUE; + + case PBT_APMRESUMESUSPEND: + LL_INFOS("Window") << "System is resuming from suspend" << LL_ENDL; + window_imp->mCallbacks->handleCloseRequestCanceled(); + return TRUE; + + case PBT_APMPOWERSTATUSCHANGE: + LL_INFOS("Window") << "Power status has changed" << LL_ENDL; + // Power status change (AC/battery) + // Viewer requires high performance, not much we can do. + // about it, but log for diagnostic purposes (example: + // OS trying to throw viewer at an iGPU after this message) + return TRUE; + + default: + break; + } + break; + } + case WM_POST_UNINSTALL_: + { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POST_UNINSTALL_"); + // Other instance, likely velopack, requested we quit. + // Don't trust PID alone (can be spoofed), verify the + // path for security purposes before processing. + // Verifying path isn't a strong varranty, if this turns + // up to be a risk, we will want something more secure. + // See sendShutdownToOtherInstances for the sender. + + // LPARAM contains message type. + DWORD message_type = static_cast(l_param); + if (message_type == WM_POST_UNINSTALL_MSG_SHUTDOWN || message_type == WM_POST_UNINSTALL_MSG_UPDATE) + { + DWORD sender_process_id = static_cast(w_param); + + // Make sure something didn't just send us our own process + DWORD our_process_id = GetCurrentProcessId(); + if (our_process_id == sender_process_id) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ from our own process, ignoring" << LL_ENDL; + break; + } + + if (sender_process_id == 0) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't get sender process ID" << LL_ENDL; + break; + } + + // Open the existing sender process to verify its executable path + HANDLE hSenderProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, sender_process_id); + if (!hSenderProcess) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't open sender process" << LL_ENDL; + break; + } + + // Get the actual executable path of the sender + wchar_t sender_exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + bool got_sender_path = QueryFullProcessImageNameW(hSenderProcess, 0, sender_exe_path, &size) != 0; + CloseHandle(hSenderProcess); + + if (!got_sender_path) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't query sender executable path" << LL_ENDL; + break; + } + + // Extract directory from sender's executable path + wchar_t sender_dir[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(sender_exe_path, MAX_PATH, sender_dir, &file_part); + + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize sender executable path" << LL_ENDL; + break; + } + + // Remove the filename to get just directory + if (file_part) + { + *file_part = L'\0'; + } + + // Remove trailing backslash + size_t sender_dir_len = wcslen(sender_dir); + if (sender_dir_len > 0 && sender_dir[sender_dir_len - 1] == L'\\') + { + sender_dir[sender_dir_len - 1] = L'\0'; + sender_dir_len--; + } + + // Remove "\current" suffix from sender's path if present + const std::wstring current_suffix = L"\\current"; + std::wstring sender_normalized_str(sender_dir); + if (sender_normalized_str.length() >= current_suffix.length() && + _wcsicmp(sender_normalized_str.c_str() + sender_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + sender_normalized_str.resize(sender_normalized_str.length() - current_suffix.length()); + } + + // Get our executable directory for comparison + std::wstring our_wide = ll_convert(gDirUtilp->getExecutableDir()); + + // Normalize our path + wchar_t our_normalized[MAX_PATH]; + file_part = nullptr; + + DWORD result2 = GetFullPathNameW(our_wide.c_str(), MAX_PATH, our_normalized, &file_part); + + if (result2 == 0 || result2 >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize our executable path" << LL_ENDL; + break; + } + + // Remove trailing backslash + size_t our_len = wcslen(our_normalized); + if (our_len > 0 && our_normalized[our_len - 1] == L'\\') + { + our_normalized[our_len - 1] = L'\0'; + our_len--; + } + + // Remove "\current" suffix from our path if present + std::wstring our_normalized_str(our_normalized); + if (our_normalized_str.length() >= current_suffix.length() && + _wcsicmp(our_normalized_str.c_str() + our_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + our_normalized_str.resize(our_normalized_str.length() - current_suffix.length()); + } + + // Compare the normalized base installation paths (case-insensitive) + if (_wcsicmp(sender_normalized_str.c_str(), our_normalized_str.c_str()) == 0) + { + window_imp->post([=]() + { + LL_INFOS("Window") << "Received valid shutdown request from verified same installation directory" << LL_ENDL; + // Check if app needs cleanup or can be closed immediately. + if (window_imp->mCallbacks->handleCloseRequest(window_imp, false)) + { + // Get the app to initiate cleanup. + window_imp->mCallbacks->handleQuit(window_imp); + } + }); + } + else + { + LL_WARNS("Window") << "Rejected shutdown request - sender not from our installation directory. " + << "Sender: " << ll_convert_wide_to_string(sender_normalized_str) + << " Our: " << ll_convert_wide_to_string(our_normalized_str) << LL_ENDL; + } + } + else + { + LL_WARNS("Window") << "Received invalid WM_POST_UNINSTALL_ message" << LL_ENDL; + } + break; + } case WM_COMMAND: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_COMMAND"); @@ -3132,7 +3368,18 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_DISPLAYCHANGE: { - WINDOW_IMP_POST(window_imp->mCallbacks->handleDisplayChanged()); + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_DISPLAYCHANGE"); + window_imp->post([=]() { + window_imp->mCallbacks->handleDisplayChanged(); + // Note: WM_DISPLAYCHANGE was passing to WM_SETFOCUS + // which might have been unintended and was messing with zones. + // handleFocus was copied over and return 0 added, but + // handleFocus might be not needed here. + // handleFocus resets mouse, closes popups and keys, which + // we probablt should do on 'display change'. + window_imp->mCallbacks->handleFocus(window_imp); + }); + return 0; } case WM_SETFOCUS: diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index aab2635a34f..defa90d1a32 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -40,6 +40,12 @@ // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) +// For requesting shutdown on uninstall, +// make sure it does not conflict with messages like WM_DUMMY_ +inline constexpr UINT WM_POST_UNINSTALL_ = WM_USER + 0x0019; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_SHUTDOWN = 1; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_UPDATE = 2; + typedef void (*LLW32MsgCallback)(const MSG &msg); class LLWindowWin32 : public LLWindow @@ -232,6 +238,7 @@ class LLWindowWin32 : public LLWindow LPWSTR mIconResource; LPWSTR mIconSmallResource; bool mInputProcessingPaused; + bool mReceivedSCClose; // received SC_CLOSE and expecting WM_CLOSE // The following variables are for Language Text Input control. // They are all static, since one context is shared by all LLWindowWin32 diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index c2bcd20c85f..c2a7c22bbcc 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -326,7 +326,7 @@ class LLControlCache : public LLRefCount, public LLInstanceTracker - RecentJumpThresholdSecs Comment - Seconds after a jump input during which finish-anim is suppressed to avoid interrupting rapid successive jumps. + Seconds after jump input during which landing finish-anim is suppressed to avoid interrupting rapid successive jumps. Persist 1 Type @@ -3445,17 +3445,6 @@ Value https://viewer-help.secondlife.com/[LANGUAGE]/[CHANNEL]/[VERSION]/[TOPIC][DEBUG_MODE] - HowToHelpURL - - Comment - URL for How To help content - Persist - 1 - Type - String - Value - https://lecs-viewer-web-components.s3.amazonaws.com/v3.0/[GRID_LOWERCASE]/howto/index.html - HomeSidePanelURL Comment @@ -3478,17 +3467,6 @@ Value https://search.[GRID]/viewer/?query_term=[QUERY]&search_type=[TYPE][COLLECTION]&maturity=[MATURITY]&lang=[LANGUAGE]&g=[GODLIKE]&sid=[SESSION_ID]&rid=[REGION_ID]&pid=[PARCEL_ID]&channel=[CHANNEL]&version=[VERSION]&major=[VERSION_MAJOR]&minor=[VERSION_MINOR]&patch=[VERSION_PATCH]&build=[VERSION_BUILD] - GuidebookURL - - Comment - URL for Guidebook content - Persist - 1 - Type - String - Value - http://guidebooks.secondlife.io/welcome/index.html - HighResSnapshot Comment @@ -9063,6 +9041,17 @@ Value 0 + NametagOverWater + + Comment + Render name tag over the transparent water while camera is above the water + Persist + 1 + Type + Boolean + Value + 1 + RenderInitError Comment @@ -13300,6 +13289,17 @@ Value 0 + SwitchToSharedEnvAfterTeleport + + Comment + Switch to Shared Environment after teleport + Persist + 1 + Type + Boolean + Value + 1 + PreferredBrowserBehavior Comment @@ -14416,7 +14416,7 @@ Type Boolean Value - 0 + 1 OutfitOperationsTimeout @@ -14429,6 +14429,17 @@ Value 180 + OSHibernationMode + + Comment + Whether to prevent OS from hibernating. 0 - can hibernate; 1 - can't hibernate, can turn screen off; 2 - can't hibernate, can't turn screen off + Persist + 1 + Type + S32 + Value + 0 + HeightUnits Comment diff --git a/indra/newview/app_settings/toolbars.xml b/indra/newview/app_settings/toolbars.xml index a1c9d6d9ee5..f22c25f3a23 100644 --- a/indra/newview/app_settings/toolbars.xml +++ b/indra/newview/app_settings/toolbars.xml @@ -10,7 +10,6 @@ - diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp index 5a94a2c6c68..54ad8c894b1 100644 --- a/indra/newview/gltf/llgltfloader.cpp +++ b/indra/newview/gltf/llgltfloader.cpp @@ -1398,6 +1398,24 @@ void LLGLTFLoader::populateJointsFromSkin(S32 skin_idx) buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, ident, ident); } + // Precompute the source bind pose in viewer coordinate space so each + // joint can later be remapped against its own GLTF rest matrix. This + // keeps bind-pose deltas local to the joint hierarchy. Without this, + // a source skeleton whose parent joints carry bind-pose rotations that + // the viewer skeleton does not have can leak those rotations into + // child joints with different local bases. + joint_node_mat4_map_t rotated_bind_matrices; + joint_node_mat4_map_t converted_bind_matrices; + if (inverse_count > 0) + { + for (S32 i = 0; i < joint_count && i < inverse_count; i++) + { + S32 joint = skin.mJoints[i]; + glm::mat4 original_bind_matrix = glm::inverse(skin.mInverseBindMatricesData[i]); + rotated_bind_matrices[joint] = rotateGltfMatrixToViewerSpace(original_bind_matrix); + } + } + for (S32 i = 0; i < joint_count; i++) { S32 joint = skin.mJoints[i]; @@ -1430,12 +1448,19 @@ void LLGLTFLoader::populateJointsFromSkin(S32 skin_idx) } else if (inverse_count > i) { - // Transalte existing bind matrix to viewer's overriden skeleton - glm::mat4 original_bind_matrix = glm::inverse(skin.mInverseBindMatricesData[i]); - glm::mat4 rotated_original = coord_system_rotation * original_bind_matrix; - glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint, legal_name); - glm::mat4 tranlated_original = skeleton_transform * rotated_original; - glm::mat4 final_inverse_bind_matrix = glm::inverse(tranlated_original); + // Translate existing bind matrices to the viewer skeleton by + // preserving each joint's bind-pose delta from its own GLTF rest + // matrix. The previous world-space remap applied one skeleton + // transform directly to each bind matrix; when a source parent has + // bind-pose rotation that the viewer parent does not, that rotation + // could get baked into child inverse binds. Keeping the delta local + // fixes the resulting child joint twists. + glm::mat4 translated_original = computeViewerBindMatrix( + joints_data, + rotated_bind_matrices, + joint, + converted_bind_matrices); + glm::mat4 final_inverse_bind_matrix = glm::inverse(translated_original); LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(final_inverse_bind_matrix)); LL_DEBUGS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Translated val: " << gltf_transform << LL_ENDL; @@ -1551,6 +1576,12 @@ void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map glm::quat rotation; glm::decompose(translated_joint, scale, rotation, translation_override, skew, perspective); + glm::mat4 viewer_rotation_scale(1.0f); + viewer_rotation_scale = glm::rotate(viewer_rotation_scale, glm::radians(viewer_data.mRotation[0]), glm::vec3(1, 0, 0)); + viewer_rotation_scale = glm::rotate(viewer_rotation_scale, glm::radians(viewer_data.mRotation[1]), glm::vec3(0, 1, 0)); + viewer_rotation_scale = glm::rotate(viewer_rotation_scale, glm::radians(viewer_data.mRotation[2]), glm::vec3(0, 0, 1)); + viewer_rotation_scale = glm::scale(viewer_rotation_scale, viewer_data.mScale); + // Viewer allows overrides, which are base joint with applied translation override. // fortunately normal bones use only translation, without rotation or scale node.mOverrideMatrix = glm::recompose(glm::vec3(1, 1, 1), glm::identity(), translation_override, glm::vec3(0, 0, 0), glm::vec4(0, 0, 0, 1)); @@ -1566,13 +1597,14 @@ void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map } else { - // This is likely incomplete or even wrong. - // Viewer Collision bones specify rotation and scale. - // Importer should apply rotation and scale to this matrix and save as needed - // then subsctruct them from bind matrix - // Todo: get models that use collision bones, made by different programs - - overriden_joint = glm::scale(overriden_joint, viewer_data.mScale); + // Collision volumes need the imported translation override, but + // their local rotation-scale basis must come from the raw viewer + // skeleton XML values. For non-uniform torso volumes, matching DAE + // requires viewer rotation followed by viewer scale. + overriden_joint = viewer_rotation_scale; + overriden_joint[3][0] = translation_override.x; + overriden_joint[3][1] = translation_override.y; + overriden_joint[3][2] = translation_override.z; node.mOverrideRestMatrix = parent_support_rest * overriden_joint; } } @@ -1651,6 +1683,69 @@ glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const joints_d return data.mGltfMatrix; } +// Convert a GLTF-space transform into the viewer coordinate space used by +// skeleton override and bind-pose calculations. Keeping this conversion in +// one helper ensures bind and rest matrices use the same rotation path, +// including the optional XY rotation applied for compatible uploads. +glm::mat4 LLGLTFLoader::rotateGltfMatrixToViewerSpace(const glm::mat4& gltf_matrix) const +{ + glm::mat4 rotated = coord_system_rotation * gltf_matrix; + if (mApplyXYRotation) + { + rotated = coord_system_rotationxy * rotated; + } + return rotated; +} + +// Convert one GLTF bind matrix to the matching viewer bind matrix. The +// function derives the joint-local bind delta from the GLTF bind and rest +// matrices, then applies that same delta to the viewer override rest matrix. +// Results are cached because child calculations can request the same joint +// more than once while preserving local hierarchy behavior. +glm::mat4 LLGLTFLoader::computeViewerBindMatrix( + const joints_data_map_t& joints_data_map, + const joint_node_mat4_map_t& rotated_bind_matrices, + S32 gltf_node_index, + joint_node_mat4_map_t& converted_bind_matrices) const +{ + auto cached = converted_bind_matrices.find(gltf_node_index); + if (cached != converted_bind_matrices.end()) + { + return cached->second; + } + + auto node_iter = joints_data_map.find(gltf_node_index); + if (node_iter == joints_data_map.end()) + { + return glm::mat4(1.f); + } + + const JointNodeData& node_data = node_iter->second; + if (!node_data.mIsOverrideValid) + { + // No valid override, falling back to rest matrix. + glm::mat4 fallback_bind = rotateGltfMatrixToViewerSpace(node_data.mGltfRestMatrix); + converted_bind_matrices[gltf_node_index] = fallback_bind; + return fallback_bind; + } + + auto bind_iter = rotated_bind_matrices.find(gltf_node_index); + if (bind_iter == rotated_bind_matrices.end()) + { + converted_bind_matrices[gltf_node_index] = node_data.mOverrideRestMatrix; + return node_data.mOverrideRestMatrix; + } + + glm::mat4 gltf_joint_node = rotateGltfMatrixToViewerSpace(node_data.mGltfRestMatrix); + glm::mat4 gltf_joint_bind = bind_iter->second; + glm::mat4 viewer_joint_node = node_data.mOverrideRestMatrix; + glm::mat4 bind_delta = gltf_joint_bind * glm::inverse(gltf_joint_node); + glm::mat4 viewer_joint_bind = bind_delta * viewer_joint_node; + + converted_bind_matrices[gltf_node_index] = viewer_joint_bind; + return viewer_joint_bind; +} + // This function computes the transformation matrix needed to convert from GLTF skeleton space // to viewer skeleton space for a specific joint @@ -1903,4 +1998,3 @@ std::string LLGLTFLoader::getLodlessLabel(const LL::GLTF::Node& node) } return node.mName; } - diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index a847e567a62..d8aad798836 100644 --- a/indra/newview/gltf/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -160,6 +160,8 @@ class LLGLTFLoader : public LLModelLoader void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& support_rest) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const; + glm::mat4 rotateGltfMatrixToViewerSpace(const glm::mat4& gltf_matrix) const; + glm::mat4 computeViewerBindMatrix(const joints_data_map_t& joints_data_map, const joint_node_mat4_map_t& rotated_bind_matrices, S32 gltf_node_index, joint_node_mat4_map_t& converted_bind_matrices) const; glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const; bool checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx); void checkForXYrotation(const LL::GLTF::Skin& gltf_skin); diff --git a/indra/newview/licenses-linux.txt b/indra/newview/licenses-linux.txt index a199ed410c6..8a410d22a33 100644 --- a/indra/newview/licenses-linux.txt +++ b/indra/newview/licenses-linux.txt @@ -632,3 +632,373 @@ supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. +================= +Vivox SDK License +================= + +RSA Data Security, Inc. MD5 Message-Digest Algorithm + +Audio coding: Polycom(R) Siren14TM (ITU-T Rec. G.722.1 Annex C) + +Open Source Software Licensing +Each open source software component utilized by this product is subject to its own copyright and licensing terms, as listed below. + +************************************************************* +************************************************************* + +/** + * OpenAL cross platform audio library + * Copyright (C) 1999-2000 by authors. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +************************************************************* +************************************************************* +RTP code under Lesser General Public License + +/* + The oRTP library is an RTP (Realtime Transport Protocol - rfc3550) stack. + Copyright (C) 2001 Simon MORLAT simon.morlat@linphone.org + + 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; either + version 2.1 of the License, or (at your option) any later version. + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +************************************************************ +************************************************************* + +/* + * The Vovida Software License, Version 1.0 + * + * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The names "VOCAL", "Vovida Open Communication Application Library", + * and "Vovida Open Communication Application Library (VOCAL)" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact vocal@vovida.org. + * + * 4. Products derived from this software may not be called "VOCAL", nor + * may "VOCAL" appear in their name, without prior written + * permission of Vovida Networks, Inc. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA + * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES + * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * + * This software consists of voluntary contributions made by Vovida + * Networks, Inc. and many individuals on behalf of Vovida Networks, + * Inc. For more information on Vovida Networks, Inc., please see + * + * + */ +************************************************************* +************************************************************* + +Internet Software Consortium code + +/* This is from the BIND 4.9.4 release, modified to compile by itself */ +/* Copyright (c) 1996 by Internet Software Consortium. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS + * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE + * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR + * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + */ + +************************************************************* + +************************************************************* + +************************************************************ + +http://tinyxpath.sourceforge.net/ + +TinyXPath is covered by the zlib license : + + www.sourceforge.net/projects/tinyxpath + Copyright (c) 2002-2006 Yves Berquin (yvesb@users.sourceforge.net) + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any + damages arising from the use of this software. + + Permission is granted to anyone to use this software for any + purpose, including commercial applications, and to alter it and + redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product documentation + would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + +************************************************************ +************************************************************ + +THE FREE SOFTWARE FOUNDATION + +Any customer may request the source code for all open source portions of this product which are covered by the Free Software Foundation's General Public License (GPL), for a period of three years from purchase. Please contact the vendor from whom you obtained this product for instructions. A fee equivalent to the cost of making the code available may be charged. Alternatively, customers may choose to download desired GPL components directly from their original vendors. Specifically, this product contains the following GPL-licensed components: + + +From Vivox: + - Assorted software components. To request source, contact Vivox at: + Vivox, Inc. + Attn: customer support + 40 Speen Street Suite 402 + Framingham, MA 01701 + +DejaVu Fonts +MIT License + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +@@ -41,17 +42,17 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +@@ -91,9 +92,96 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +Twemoji Fonts +MIT License + +Applies to "EmojiOne SVGinOT Font" code only +Copyright (c) 2022 Brad Erickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +WebRTC + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Google fonts +SIL OPEN FONT LICENSE Version 1.1 + +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/indra/newview/licenses-mac.txt b/indra/newview/licenses-mac.txt index 0bd9ebc5f1f..c2f52e3b900 100644 --- a/indra/newview/licenses-mac.txt +++ b/indra/newview/licenses-mac.txt @@ -593,3 +593,205 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +============== +DejaVu Fonts +============== +MIT License + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +@@ -41,17 +42,17 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +@@ -91,9 +92,96 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +============== +Twemoji Fonts +============== +MIT License + +Applies to "EmojiOne SVGinOT Font" code only +Copyright (c) 2022 Brad Erickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +============== +WebRTC +============== + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============== +Google fonts +============== +SIL OPEN FONT LICENSE Version 1.1 + +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + diff --git a/indra/newview/licenses-win32.txt b/indra/newview/licenses-win32.txt index b56f1bf2eb7..2d4bd8a4b63 100644 --- a/indra/newview/licenses-win32.txt +++ b/indra/newview/licenses-win32.txt @@ -667,3 +667,205 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +============== +DejaVu Fonts +============== +MIT License + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +@@ -41,17 +42,17 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +@@ -91,9 +92,96 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +============== +Twemoji Fonts +============== +MIT License + +Applies to "EmojiOne SVGinOT Font" code only +Copyright (c) 2022 Brad Erickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +============== +WebRTC +============== + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============== +Google fonts +============== +SIL OPEN FONT LICENSE Version 1.1 + +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 3ab87cac137..35d60e6595e 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -44,6 +44,7 @@ #include "llchicletbar.h" #include "llconsole.h" #include "lldonotdisturbnotificationstorage.h" +#include "llenvironment.h" #include "llfirstuse.h" #include "llfloatercamera.h" #include "llfloaterimcontainer.h" @@ -1551,6 +1552,8 @@ void LLAgent::setAFK() setControlFlags(AGENT_CONTROL_AWAY | AGENT_CONTROL_STOP); gAwayTimer.start(); } + + LLAppViewer::instance()->setPermitOSHibernation(true); } //----------------------------------------------------------------------------- @@ -1569,6 +1572,13 @@ void LLAgent::clearAFK() sendAnimationRequest(ANIM_AGENT_AWAY, ANIM_REQUEST_STOP); clearControlFlags(AGENT_CONTROL_AWAY); } + + if (isAgentAvatarValid()) + { + // Only set this if agent is inworld, login screen + // shouldn't prevent hibernation. + LLAppViewer::instance()->setPermitOSHibernation(false); + } } //----------------------------------------------------------------------------- @@ -2669,17 +2679,20 @@ void LLAgent::onAnimStop(const LLUUID& id) else if (id == ANIM_AGENT_PRE_JUMP || id == ANIM_AGENT_LAND || id == ANIM_AGENT_MEDIUM_LAND) { // FIRE-34049/FIRE-34273/https://github.com/secondlife/viewer/issues/4218 - // Avoid forcing AGENT_CONTROL_FINISH_ANIM, which can short-circuit the next pre-jump - // during rapid successive jumps. + // Avoid forcing AGENT_CONTROL_FINISH_ANIM on landing, which can short-circuit the + // next pre-jump during rapid successive jumps. + // Do not suppress pre-jump finish, otherwise a quick tap from standing can stall. // TODO: a more robust fix would require knowing which specific animation finished, // information that is not currently provided by the simulator. + const bool is_landing_anim = (id == ANIM_AGENT_LAND || id == ANIM_AGENT_MEDIUM_LAND); const bool up_pos = (mControlFlags & AGENT_CONTROL_UP_POS) != 0; const F64 now = LLTimer::getTotalSeconds(); const F64 elapsed = now - mLastJumpInputTime; - static LLCachedControl recent_jump_threshold_secs(gSavedSettings, "RecentJumpThresholdSecs"); + static LLCachedControl recent_jump_threshold_secs(gSavedSettings, "RecentJumpThresholdSecs", 1.0); const bool recent_jump = (mLastJumpInputTime > 0.0) && (elapsed < recent_jump_threshold_secs); + const bool suppress_finish = is_landing_anim && recent_jump; - if (!up_pos && !recent_jump) + if (!up_pos && !suppress_finish) { setControlFlags(AGENT_CONTROL_FINISH_ANIM); } @@ -4169,6 +4182,11 @@ void LLAgent::handleTeleportFinished() mRegionp->setCapabilitiesReceivedCallback(boost::bind(&LLAgent::onCapabilitiesReceivedAfterTeleport)); } } + static LLCachedControl shared_env_on_teleport(gSavedSettings, "SwitchToSharedEnvAfterTeleport", true); + if (shared_env_on_teleport) + { + LLEnvironment::instance().setSharedEnvironment(); + } LLPerfStats::tunables.autoTuneTimeout = true; } diff --git a/indra/newview/llagentlistener.cpp b/indra/newview/llagentlistener.cpp index 5ddb87558ac..73368c1bcc6 100644 --- a/indra/newview/llagentlistener.cpp +++ b/indra/newview/llagentlistener.cpp @@ -43,6 +43,7 @@ #include "llviewernetwork.h" #include "llviewerobject.h" #include "llviewerobjectlist.h" +#include "llviewerjointattachment.h" #include "llviewerregion.h" #include "llvoavatarself.h" #include "llsdutil.h" @@ -195,6 +196,12 @@ LLAgentListener::LLAgentListener(LLAgent &agent) &LLAgentListener::getNearbyObjectsList, llsd::map("reply", LLSD())); + add("getAttachedObjectsList", + "Return attached objects list with information about each object\n" + "reply contains \"attachments\" result key", + &LLAgentListener::getAttachedObjectsList, + llsd::map("reply", LLSD())); + add("getAgentScreenPos", "Return screen position of the [\"avatar_id\"] avatar or own avatar if not specified\n" "reply contains \"x\", \"y\" coordinates and \"onscreen\" flag to indicate if it's actually in within the current window\n" @@ -772,6 +779,41 @@ void LLAgentListener::getNearbyObjectsList(LLSD const& event_data) } } +void LLAgentListener::getAttachedObjectsList(LLSD const& event_data) +{ + Response response(LLSD(), event_data); + response["attachments"] = LLSD::emptyArray(); + + if (!isAgentAvatarValid()) + { + return; + } + + for (const auto& [attachment_point_index, attachment_point] : gAgentAvatarp->mAttachmentPoints) + { + if (!attachment_point) + { + continue; + } + + for (const auto& attachment_object_ptr : attachment_point->mAttachedObjects) + { + if (LLViewerObject* attachment_object = attachment_object_ptr.get()) + { + response["attachments"].append(llsd::map( + "object_id", attachment_object->getID(), + "inventory_item_id", attachment_object->getAttachmentItemID(), + "name", attachment_object->getAttachmentItemName(), + "attachment_point", attachment_point->getName(), + "attachment_point_index", attachment_point_index, + "position", ll_sd_from_vector3(attachment_object->getPosition()), + "rotation", ll_sd_from_quaternion(attachment_object->getRotation()), + "is_temporary", attachment_object->isTempAttachment())); + } + } + } +} + void LLAgentListener::getAgentScreenPos(LLSD const& event_data) { Response response(LLSD(), event_data); diff --git a/indra/newview/llagentlistener.h b/indra/newview/llagentlistener.h index b5bea8c0bde..031fc75e463 100644 --- a/indra/newview/llagentlistener.h +++ b/indra/newview/llagentlistener.h @@ -68,6 +68,7 @@ class LLAgentListener : public LLEventAPI void getID(LLSD const& event_data); void getNearbyAvatarsList(LLSD const& event_data); void getNearbyObjectsList(LLSD const& event_data); + void getAttachedObjectsList(LLSD const& event_data); void getAgentScreenPos(LLSD const& event_data); LLViewerObject * findObjectClosestTo( const LLVector3 & position, bool sit_target = false ) const; diff --git a/indra/newview/llagentwearables.cpp b/indra/newview/llagentwearables.cpp index a075b6f004d..edd08c19e5d 100644 --- a/indra/newview/llagentwearables.cpp +++ b/indra/newview/llagentwearables.cpp @@ -547,14 +547,23 @@ const S32 LLAgentWearables::getWearableIdxFromItem(const LLViewerInventoryItem* U32 wearable_count = getWearableCount(type); if (0 == wearable_count) return -1; - const LLUUID& asset_id = item->getAssetUUID(); + // Match by linked inventory item id so the correct copy is found when several + // worn wearables of this type share the same asset. + const LLUUID item_id = gInventory.getLinkedItemID(item->getUUID()); + for (U32 i = 0; i < wearable_count; ++i) + { + const LLViewerWearable* wearable = getViewerWearable(type, i); + if (!wearable) continue; + if (wearable->getItemID() == item_id) return i; + } + // Fall back to asset id for items whose inventory id can't be resolved. + const LLUUID& asset_id = item->getAssetUUID(); for (U32 i = 0; i < wearable_count; ++i) { const LLViewerWearable* wearable = getViewerWearable(type, i); if (!wearable) continue; - if (wearable->getAssetID() != asset_id) continue; - return i; + if (wearable->getAssetID() == asset_id) return i; } return -1; @@ -1529,6 +1538,37 @@ bool LLAgentWearables::moveWearable(const LLViewerInventoryItem* item, bool clos return false; } +bool LLAgentWearables::moveWearableToIndex(const LLViewerInventoryItem* item, U32 new_index) +{ + if (!item) return false; + if (!item->isWearableType()) return false; + + LLWearableType::EType type = item->getWearableType(); + U32 wearable_count = getWearableCount(type); + if (wearable_count < 2) return false; + + if (new_index >= wearable_count) new_index = wearable_count - 1; + + S32 cur = getWearableIdxFromItem(item); + if (cur < 0) return false; + if ((U32)cur == new_index) return true; // already in place + + // step the wearable to its new slot one swap at a time + U32 pos = (U32)cur; + while (pos < new_index) + { + if (!swapWearables(type, pos, pos + 1)) return false; + ++pos; + } + while (pos > new_index) + { + if (!swapWearables(type, pos, pos - 1)) return false; + --pos; + } + + return true; +} + // static void LLAgentWearables::createWearable(LLWearableType::EType type, bool wear, const LLUUID& parent_id, std::function created_cb) { diff --git a/indra/newview/llagentwearables.h b/indra/newview/llagentwearables.h index 70da86805cb..3c9ffa7443d 100644 --- a/indra/newview/llagentwearables.h +++ b/indra/newview/llagentwearables.h @@ -129,6 +129,7 @@ class LLAgentWearables : public LLInitClass, public LLWearable static void createWearable(LLWearableType::EType type, bool wear = false, const LLUUID& parent_id = LLUUID::null, std::function created_cb = nullptr); static void editWearable(const LLUUID& item_id); bool moveWearable(const LLViewerInventoryItem* item, bool closer_to_body); + bool moveWearableToIndex(const LLViewerInventoryItem* item, U32 new_index); void requestEditingWearable(const LLUUID& item_id); void editWearableIfRequested(const LLUUID& item_id); diff --git a/indra/newview/llaisapi.cpp b/indra/newview/llaisapi.cpp index f67f2688a16..1c5530916f3 100644 --- a/indra/newview/llaisapi.cpp +++ b/indra/newview/llaisapi.cpp @@ -1758,35 +1758,41 @@ void AISUpdate::doUpdate() const LLUUID id = ucv_it->first; S32 version = static_cast(ucv_it->second); LLViewerInventoryCategory *cat = gInventory.getCategory(id); - LL_DEBUGS("Inventory") << "cat version update " << cat->getName() << " to version " << cat->getVersion() << LL_ENDL; - if (cat->getVersion() != version) - { - // the AIS version should be considered the true version. Adjust - // our local category model to reflect this version number. Otherwise - // it becomes possible to get stuck with the viewer being out of - // sync with the inventory system. Under normal circumstances - // inventory COF is maintained on the viewer through calls to - // LLInventoryModel::accountForUpdate when a changing operation - // is performed. This occasionally gets out of sync however. - if (version != LLViewerInventoryCategory::VERSION_UNKNOWN) - { - LL_WARNS() << "Possible version mismatch for category " << cat->getName() - << ", viewer version " << cat->getVersion() - << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL; - cat->setVersion(version); - } - else + // Update can be rather large and take time to process. + // By the time update gets to the category, it could + // could have been removed by the user + if (cat) + { + LL_DEBUGS("Inventory") << "cat " << cat->getName() << " version update from " << cat->getVersion() << " to AIS version " << version << LL_ENDL; + if (cat->getVersion() != version) { - // We do not account for update if version is UNKNOWN, so we shouldn't rise version - // either or viewer will get stuck on descendants count -1, try to refetch folder instead - // - // Todo: proper backoff? - - LL_WARNS() << "Possible version mismatch for category " << cat->getName() - << ", viewer version " << cat->getVersion() - << " AIS version " << version << " !!!Rerequesting category!!!" << LL_ENDL; - const S32 LONG_EXPIRY = 360; - cat->fetch(LONG_EXPIRY); + // the AIS version should be considered the true version. Adjust + // our local category model to reflect this version number. Otherwise + // it becomes possible to get stuck with the viewer being out of + // sync with the inventory system. Under normal circumstances + // inventory COF is maintained on the viewer through calls to + // LLInventoryModel::accountForUpdate when a changing operation + // is performed. This occasionally gets out of sync however. + if (version != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + LL_WARNS() << "Possible version mismatch for category " << cat->getName() + << ", viewer version " << cat->getVersion() + << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL; + cat->setVersion(version); + } + else + { + // We do not account for update if version is UNKNOWN, so we shouldn't riase version + // either or viewer will get stuck on descendants count -1, try to refetch folder instead + // + // Todo: proper backoff? + + LL_WARNS() << "Possible version mismatch for category " << cat->getName() + << ", viewer version " << cat->getVersion() + << " AIS version " << version << " !!!Rerequesting category!!!" << LL_ENDL; + const S32 LONG_EXPIRY = 360; + cat->fetch(LONG_EXPIRY); + } } } } diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index 8aa28e1b091..80a33449795 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -980,7 +980,9 @@ void recovered_item_link_cb(const LLUUID& item_id, LLWearableType::EType type, L if (!holder->isMostRecent()) { LL_WARNS() << "HP " << holder->index() << " skipping because LLWearableHolding pattern is invalid (superceded by later outfit request)" << LL_ENDL; - // runway skip here? + + // If we were signalled to stop then we shouldn't do anything else except poll for when it's safe to delete ourselves + return; } LL_INFOS("Avatar") << "HP " << holder->index() << " recovered item link for type " << type << LL_ENDL; @@ -1020,7 +1022,6 @@ void recovered_item_cb(const LLUUID& item_id, LLWearableType::EType type, LLView { if (!holder->isMostRecent()) { - // runway skip here? LL_WARNS() << self_av_string() << "skipping because LLWearableHolding pattern is invalid (superceded by later outfit request)" << LL_ENDL; // If we were signalled to stop then we shouldn't do anything else except poll for when it's safe to delete ourselves @@ -4268,77 +4269,110 @@ bool LLAppearanceMgr::moveWearable(LLViewerInventoryItem* item, bool closer_to_b { if (!item || !item->isWearableType()) return false; if (item->getType() != LLAssetType::AT_CLOTHING) return false; - if (!gInventory.isObjectDescendentOf(item->getUUID(), getCOF())) return false; S32 pos = gAgentWearables.getWearableIdxFromItem(item); if (pos < 0) return false; // Not found + if (closer_to_body && pos == 0) return false; // already closest to the body - U32 count = gAgentWearables.getWearableCount(item->getWearableType()); - if (count < 2) return false; // Nothing to swap with - if (closer_to_body) + return reorderWearable(item, closer_to_body ? (U32)(pos - 1) : (U32)(pos + 1)); +} + +bool LLAppearanceMgr::reorderWearable(LLViewerInventoryItem* item, U32 new_index) +{ + if (!item || !item->isWearableType()) return false; + if (item->getType() != LLAssetType::AT_CLOTHING) return false; + if (!gInventory.isObjectDescendentOf(item->getUUID(), getCOF())) return false; + + LLWearableType::EType type = item->getWearableType(); + U32 count = gAgentWearables.getWearableCount(type); + if (count < 2) return false; // nothing to reorder against + + if (new_index >= count) new_index = count - 1; + + S32 cur = gAgentWearables.getWearableIdxFromItem(item); + if (cur < 0) return false; + if ((U32)cur == new_index) return false; // already in place + + // Update the live layer order first; bail before touching inventory if it fails. + if (!gAgentWearables.moveWearableToIndex(item, new_index)) return false; + + persistWearableOrder(type); + return true; +} + +bool LLAppearanceMgr::reorderWearableGroup(LLWearableType::EType type, const uuid_vec_t& ordered_link_ids) +{ + U32 count = gAgentWearables.getWearableCount(type); + if (count < 2) return false; + if (ordered_link_ids.size() != count) return false; // order must cover the whole group + + // Validate everything before mutating, so a bad link can't leave it half-reordered. + for (const LLUUID& link_id : ordered_link_ids) { - if (pos == 0) return false; // already first + LLViewerInventoryItem* link = gInventory.getItem(link_id); + if (!link || link->getWearableType() != type) return false; } - else + + // ordered_link_ids runs furthest-to-closest; body index 0 is closest to the body. + // Place each target at its body index, leaving already-placed lower indices untouched. + for (U32 body_index = 0; body_index < count; ++body_index) { - if (pos == count - 1) return false; // already last + const LLUUID& link_id = ordered_link_ids[count - 1 - body_index]; + LLViewerInventoryItem* link = gInventory.getItem(link_id); + if (!link || link->getWearableType() != type) return false; + if (!gAgentWearables.moveWearableToIndex(link, body_index)) return false; } - U32 old_pos = (U32)pos; - U32 swap_with = closer_to_body ? old_pos - 1 : old_pos + 1; - LLUUID swap_item_id = gAgentWearables.getWearableItemID(item->getWearableType(), swap_with); + persistWearableOrder(type); + return true; +} + +void LLAppearanceMgr::persistWearableOrder(LLWearableType::EType type) +{ + U32 count = gAgentWearables.getWearableCount(type); - // Find link item from item id. + // Rewrite the sort-index descriptions for the whole type group in one pass so + // the order survives relog, trusting gAgentWearables over existing descriptions. LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; - LLFindWearablesOfType filter_wearables_of_type(item->getWearableType()); + LLFindWearablesOfType filter_wearables_of_type(type); gInventory.collectDescendentsIf(getCOF(), cats, items, true, filter_wearables_of_type); - if (items.empty()) return false; - LLViewerInventoryItem* swap_item = nullptr; - for (auto iter : items) + for (U32 i = 0; i < count; ++i) { - if (iter->getLinkedUUID() == swap_item_id) + LLUUID linked_id = gAgentWearables.getWearableItemID(type, i); + if (linked_id.isNull()) continue; + + LLViewerInventoryItem* link = nullptr; + for (auto iter : items) { - swap_item = iter.get(); - break; + if (iter->getLinkedUUID() == linked_id) + { + link = iter.get(); + break; + } } - } - if (!swap_item) - { - return false; - } - - // Description is supposed to hold sort index, but user could have changed - // order rapidly and there might be a state mismatch between description - // and gAgentWearables, trust gAgentWearables over description. - // Generate new description. - std::string new_desc = build_order_string(item->getWearableType(), old_pos); - swap_item->setDescription(new_desc); - new_desc = build_order_string(item->getWearableType(), swap_with); - item->setDescription(new_desc); + if (!link) continue; - item->setComplete(true); - item->updateServer(false); - gInventory.updateItem(item); + std::string new_desc = build_order_string(type, i); + if (new_desc == link->getActualDescription()) continue; - swap_item->setComplete(true); - swap_item->updateServer(false); - gInventory.updateItem(swap_item); + // Keep the local cache consistent immediately (so the COF list does not + // flicker back on a refresh), and persist durably via AISv3, matching + // updateClothingOrderingInfo() rather than the legacy UDP updateServer(). + link->setDescription(new_desc); + LLSD updates; + updates["desc"] = new_desc; + update_inventory_item(link->getUUID(), updates, NULL); + } - //to cause appearance of the agent to be updated - bool result = false; - if ((result = gAgentWearables.moveWearable(item, closer_to_body))) + if (isAgentAvatarValid()) { - gAgentAvatarp->wearableUpdated(item->getWearableType()); + gAgentAvatarp->wearableUpdated(type); } setOutfitDirty(true); - - //*TODO do we need to notify observers here in such a way? gInventory.notifyObservers(); - - return result; } //static diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h index 9d90be00045..77146ba0bed 100644 --- a/indra/newview/llappearancemgr.h +++ b/indra/newview/llappearancemgr.h @@ -210,6 +210,15 @@ class LLAppearanceMgr: public LLSingleton bool moveWearable(LLViewerInventoryItem* item, bool closer_to_body); + // Move a clothing item to an absolute layer index within its wearable type + // (0 == closest to the body). Persists the new order to the COF link descriptions. + bool reorderWearable(LLViewerInventoryItem* item, U32 new_index); + + // Apply a complete layer order for one wearable type, persisting it to the + // COF link descriptions. ordered_link_ids lists the type's COF link items + // furthest-to-closest. + bool reorderWearableGroup(LLWearableType::EType type, const uuid_vec_t& ordered_link_ids); + static void sortItemsByActualDescription(LLInventoryModel::item_array_t& items); //Divvy items into arrays by wearable type @@ -250,6 +259,10 @@ class LLAppearanceMgr: public LLSingleton private: + // Rewrite COF sort-index descriptions for one wearable type to match the + // current in-memory layer order, then trigger a single appearance update. + void persistWearableOrder(LLWearableType::EType type); + void filterWearableItems(LLInventoryModel::item_array_t& items, S32 max_per_type, S32 max_total, bool skip_bodyparts = false); void getDescendentsOfAssetType(const LLUUID& category, diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 59056381e1c..58e3aea9b79 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2012, 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 @@ -66,6 +66,7 @@ #include "llconversationlog.h" #if LL_WINDOWS #include "lldxhardware.h" +#include #endif #include "lltexturestats.h" #include "lltrace.h" @@ -383,6 +384,8 @@ const std::string START_MARKER_FILE_NAME("SecondLife.start_marker"); const std::string ERROR_MARKER_FILE_NAME("SecondLife.error_marker"); const std::string LOGOUT_MARKER_FILE_NAME("SecondLife.logout_marker"); const std::string WATCHDOG_MARKER_FILE_NAME("SecondLife.watchdog_marker"); +const std::string INITED_MARKER_FILE_NAME("SecondLife.inited_marker"); +const std::string CLOSE_EVENT_MARKER_FILE_NAME("SecondLife.close_marker"); static std::string gLaunchFileOnQuit; //---------------------------------------------------------------------------- @@ -630,6 +633,12 @@ bool LLAppViewer::sendURLToOtherInstance(const std::string& url) return false; } +//virtual +void LLAppViewer::setOSHibernationMode(eHibernationMode mode) +{ + // See OS specific files +} + //---------------------------------------------------------------------------- // LLAppViewer definition @@ -688,6 +697,12 @@ LLAppViewer::LLAppViewer() gLoggedInTime.stop(); + // Locking this early is needed to prevent multiple instances and + // to log, but it also means that early paths such as SLURL handling + // can invoke processMarkerFiles() and potentially clear markers + // from previous runs before those stats are reported. + // Todo: improve this. Perhaps store stats 'permanently' to be reported + // on next login and only login cleans stats up? processMarkerFiles(); // // OK to write stuff to logs now, we've now crash reported if necessary @@ -938,6 +953,16 @@ bool LLAppViewer::init() LL_WARNS("InitInfo") << "initHardwareTest() failed." << LL_ENDL; // quit immediately LL_PROFILER_FRAME_END; + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Stats from previous session will likely be lost, but this should + // be fine as this is likely first run for this version. + // Todo: Might be smarter to have an exit code for a cleaner shutdown + removeMarkerFiles(); + } return false; } LL_INFOS("InitInfo") << "Hardware test initialization done." << LL_ENDL ; @@ -2133,6 +2158,7 @@ bool LLAppViewer::cleanup() LLWorld::deleteSingleton(); LLVoiceClient::deleteSingleton(); LLUI::deleteSingleton(); + LLWatchdog::deleteSingleton(); // It's not at first obvious where, in this long sequence, a generic cleanup // call OUGHT to go. So let's say this: as we migrate cleanup from @@ -2277,6 +2303,9 @@ void errorHandler(const std::string& title_string, const std::string& message_st case LLError::LLUserWarningMsg::ERROR_MISSING_FILES: LLAppViewer::instance()->createErrorMarker(LAST_EXEC_MISSING_FILES); break; + case LLError::LLUserWarningMsg::ERROR_INIT_FAILED: + LLAppViewer::instance()->createErrorMarker(LAST_EXEC_INIT); + break; default: break; } @@ -2301,6 +2330,12 @@ void errorHandler(const std::string& title_string, const std::string& message_st } } +namespace +{ + std::string getStartupLogFileName(); + std::string getOldLogFileName(const std::string& log_file); +} + void LLAppViewer::initLoggingAndGetLastDuration() { // @@ -2326,13 +2361,10 @@ void LLAppViewer::initLoggingAndGetLastDuration() else { // Remove the last ".old" log file. - std::string old_log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, - "SecondLife.old"); + std::string log_file = getStartupLogFileName(); + std::string old_log_file = getOldLogFileName(log_file); LLFile::remove(old_log_file); - // Get name of the log file - std::string log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, - "SecondLife.log"); /* * Before touching any log files, compute the duration of the last run * by comparing the ctime of the previous start marker file with the ctime @@ -2379,7 +2411,7 @@ void LLAppViewer::initLoggingAndGetLastDuration() // Rename current log file to ".old" LLFile::rename(log_file, old_log_file); - // Set the log file to SecondLife.log + // Set the log file. LLError::logToFile(log_file); LL_INFOS() << "Started logging to " << log_file << LL_ENDL; if (!duration_log_msg.empty()) @@ -2518,6 +2550,76 @@ namespace LLStringUtil::null, OSMB_OK); } + + std::string getStartupLogFileName() + { + if (LLControlVariable* user_log_file = gSavedSettings.getControl("UserLogFile")) + { + std::string log_file = user_log_file->getValue().asString(); + if (!log_file.empty()) + { + return log_file; + } + } + +#if LL_WINDOWS + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (argv) + { + std::string log_file; + for (int i = 1; i < argc; ++i) + { + std::string option = ll_convert_wide_to_string(argv[i]); + if ((option == "--logfile" || option == "-logfile" || option == "/logfile") && + i + 1 < argc) + { + log_file = ll_convert_wide_to_string(argv[i + 1]); + } + else if (option.compare(0, 10, "--logfile=") == 0) + { + log_file = option.substr(10); + } + else if (option.compare(0, 9, "-logfile=") == 0) + { + log_file = option.substr(9); + } + else if (option.compare(0, 9, "/logfile:") == 0) + { + log_file = option.substr(9); + } + } + LocalFree(argv); + + if (!log_file.empty()) + { + return log_file; + } + } +#endif + + return gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "SecondLife.log"); + } + + std::string getOldLogFileName(const std::string& log_file) + { + std::string old_log_file = log_file; + size_t separator = old_log_file.find_last_of("/\\"); + size_t basename_start = (separator == std::string::npos) ? 0 : separator + 1; + size_t extension = old_log_file.find_last_of('.'); + + if (extension != std::string::npos && + extension > basename_start) + { + old_log_file.replace(extension, std::string::npos, ".old"); + } + else + { + old_log_file += ".old"; + } + + return old_log_file; + } } // anonymous namespace // Set a named control temporarily for this session, as when set via the command line --set option. @@ -2932,7 +3034,23 @@ bool LLAppViewer::initConfiguration() { if (sendURLToOtherInstance(start_slurl.getSLURLString())) { - // successfully handed off URL to existing instance, exit + // Successfully handed off URL to existing instance. + // Returning 'false' gets treated as a failure to init, + // without cleanup, so instead clear markers and app here. + // Do not save settings. + // Might be smarter to have an exit code for a more reliable + // "early exit, needs cleanup" case. + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Todo: Unfortunately, if we are doing this, stats and + // markers from previous session were already processed, + // cleared yet haven't been reported and will be lost. + // Consider a way to save those. + removeMarkerFiles(); + } return false; } } @@ -2979,6 +3097,11 @@ bool LLAppViewer::initConfiguration() LLTrans::getString("MBAlreadyRunning"), LLStringUtil::null, OSMB_OK); + + // Since returning 'false' is basically an error without cleanup, + // do cleanup here. No need to worry about marker files here. + LLSingletonBase::deleteAll(); + cleanupConsole(); return false; } @@ -3498,10 +3621,11 @@ LLSD LLAppViewer::getViewerInfo() const info["LIBVLC_VERSION"] = "Undefined"; #endif - S32 packets_in = (S32)LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_IN); + LLTrace::Recording& recording = LLViewerStats::instance().getRecording(); + S32 packets_in = (S32)recording.getSum(LLStatViewer::PACKETS_IN); if (packets_in > 0) { - info["PACKETS_LOST"] = LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_LOST); + info["PACKETS_LOST"] = recording.getSum(LLStatViewer::PACKETS_LOST); info["PACKETS_IN"] = packets_in; info["PACKETS_PCT"] = 100.f*info["PACKETS_LOST"].asReal() / info["PACKETS_IN"].asReal(); } @@ -3936,7 +4060,12 @@ void LLAppViewer::processMarkerFiles() // - Freeze (SecondLife.exec_marker present, not locked) // - LLError Crash (SecondLife.llerror_marker present) // - Other Crash (SecondLife.error_marker present) - // These checks should also remove these files for the last 2 cases if they currently exist + // - Watchdog freeze (SecondLife.watchdog_marker present) + // - Failed to initialize (SecondLife.inited_marker not present) + // - Potentially killed by task manager or computer + // didn't recover from hibernation (SecondLife.close_marker present) + // These checks should also remove these files for the last 2 cases + // if they currently exist std::ostringstream marker_log_stream; bool marker_is_same_version = true; @@ -4048,6 +4177,8 @@ void LLAppViewer::processMarkerFiles() // Bugsplat will set correct state in bugsplatSendLog. std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ERROR_MARKER_FILE_NAME); std::string watchdog_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); + std::string inited_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + std::string close_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); if(LLAPRFile::isExist(error_marker_file, NULL, LL_APR_RB)) { S32 marker_code = getMarkerErrorCode(error_marker_file); @@ -4086,11 +4217,13 @@ void LLAppViewer::processMarkerFiles() } else { - // so only check watchdog marker if there is no error marker. - if (LLAPRFile::isExist(watchdog_marker_file, NULL, LL_APR_RB)) + if (LAST_EXEC_UNKNOWN == gLastExecEvent + || LAST_EXEC_LOGOUT_UNKNOWN == gLastExecEvent) { - if (LAST_EXEC_UNKNOWN == gLastExecEvent - || LAST_EXEC_LOGOUT_UNKNOWN == gLastExecEvent) + // If viewer crashed after a freeze was detected, + // crash still takes precendence. + // So only check watchdog marker if there is no error marker. + if (LLAPRFile::isExist(watchdog_marker_file, NULL, LL_APR_RB)) { // watchdog marker gets created if we detect a freeze, // so if viwer did not stop gracefully, and we know it wasn't a crash, @@ -4102,9 +4235,49 @@ void LLAppViewer::processMarkerFiles() << LL_ENDL; } } - removeWatchdogMarker(); + // If 'close' marker is found, viewer either started shutdown but + // failed, OS did not recover from hibernation or viewer got + // killed by task manager. + // Marker does not indicate that viewer was closed or is closing, + // just that 'close' was requested before viewer died. + else if (LLAPRFile::isExist(close_marker_file, NULL, LL_APR_RB)) + { + // Unfortunately we can't reliably distinguish + // task manager's case from genuine shutdown, so we + // have to report all of them as the same thing. + // Todo: but we can distinguish hibernation, might want + // to simply not report it as an issue. + if (markerIsSameVersion(close_marker_file)) + { + gLastExecEvent = LAST_EXEC_OS_EVENT; + LL_INFOS("MarkerFile") << "'Close' marker '" << close_marker_file << "' found, setting LastExecEvent to OS_EVENT" + << LL_ENDL; + } + } + else if ((LAST_EXEC_UNKNOWN == gLastExecEvent) + && !LLAPRFile::isExist(inited_marker_file, NULL, LL_APR_RB)) + { + // Viewer didn't get to a login screen. + gLastExecEvent = LAST_EXEC_INIT; + LL_INFOS("MarkerFile") << "'Inited' marker '" + << inited_marker_file + << "' not found, assuming that init crashed." + << LL_ENDL; + } } } + if (LLAPRFile::isExist(watchdog_marker_file, NULL, LL_APR_RB)) + { + removeWatchdogMarker(); + } + if (LLAPRFile::isExist(inited_marker_file, NULL, LL_APR_RB)) + { + removeInitedMarker(); + } + if (LLAPRFile::isExist(close_marker_file, NULL, LL_APR_RB)) + { + removeCloseRequestMarker(); + } #if LL_DARWIN if (!mSecondInstance && gLastExecEvent != LAST_EXEC_NORMAL) @@ -4148,6 +4321,7 @@ void LLAppViewer::removeMarkerFiles() { LL_WARNS("MarkerFile") << "logout marker '"<capabilitiesReceived()) + { + constexpr bool include_preferences = true; + send_viewer_stats(include_preferences); + } sendLogoutRequest(); } @@ -4307,6 +4488,14 @@ void LLAppViewer::abortQuit() mClosingFloaters = false; } +void LLAppViewer::sendViewerStatistics(bool include_preferences) +{ + if (!gDisconnected) + { + send_viewer_stats(include_preferences); + } +} + void LLAppViewer::migrateCacheDirectory() { #if LL_WINDOWS || LL_DARWIN @@ -5589,6 +5778,59 @@ bool LLAppViewer::errorMarkerExists() const return LLAPRFile::isExist(error_marker_file, NULL, LL_APR_RB); } +void LLAppViewer::createCloseRequestMarker() const +{ + // WINDOW THREAD! since we need this to act fast. + // This does not indicate that viewer was closed or is closing, + // but that 'close' was requested. + if (!mSecondInstance) + { + std::string close_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); + + LLAPRFile file; + file.open(close_marker, LL_APR_WB); + if (file.getFileHandle()) + { + recordMarkerVersion(file); + file.close(); + } + } +} + +void LLAppViewer::removeCloseRequestMarker() const +{ + if (!mSecondInstance) + { + std::string close_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); + LLFile::remove(close_marker, ENOENT); + } +} + +void LLAppViewer::createInitedMarker() const +{ + if (!mSecondInstance) + { + std::string inited_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + + LLAPRFile file; + file.open(inited_marker, LL_APR_WB); + if (file.getFileHandle()) + { + recordMarkerVersion(file); + file.close(); + } + } +} + +void LLAppViewer::removeInitedMarker() const +{ + if (!mSecondInstance) + { + std::string inited_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + LLFile::remove(inited_marker, ENOENT); + } +} + void LLAppViewer::createWatchdogMarker() const { if (!mSecondInstance) @@ -5604,12 +5846,13 @@ void LLAppViewer::createWatchdogMarker() const } } } + void LLAppViewer::removeWatchdogMarker() const { if (!mSecondInstance) { std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); - LLFile::remove(error_marker_file); + LLFile::remove(error_marker_file, ENOENT); } } @@ -5631,6 +5874,29 @@ void LLAppViewer::outOfMemorySoftQuit() } } +void LLAppViewer::setPermitOSHibernation(bool permit) +{ + if (permit) + { + if (mCurrentHibernationMode != LL_HIBERNATE_MODE_DEFAULT) + { + // Will call OS specific code to let OS hibernate when idle + setOSHibernationMode(LL_HIBERNATE_MODE_DEFAULT); + mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + } + } + else + { + static LLCachedControl os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); + eHibernationMode mode = static_cast(os_hibernation_mode()); + if (mode != LL_HIBERNATE_MODE_DEFAULT && mCurrentHibernationMode != mode) + { + setOSHibernationMode(mode); + mCurrentHibernationMode = mode; + } + } +} + void LLAppViewer::idleNameCache() { // Neither old nor new name cache can function before agent has a region @@ -5675,7 +5941,6 @@ void LLAppViewer::idleNetwork() pingMainloopTimeout("idleNetwork"); gObjectList.mNumNewObjects = 0; - S32 total_decoded = 0; static LLCachedControl speed_test(gSavedSettings, "SpeedTest", false); if (!speed_test()) @@ -5683,64 +5948,56 @@ void LLAppViewer::idleNetwork() LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("idle network"); //LL_RECORD_BLOCK_TIME(FTM_IDLE_NETWORK); // decode LLTimer check_message_timer; - // Read all available packets from network const S64 frame_count = gFrameCount; // U32->S64 - F32 total_time = 0.0f; + S32 total_decoded = 0; + // Process packets from network + LockMessageChecker lmc(gMessageSystem); + while (lmc.checkAllMessages(frame_count, gServicePump)) { - bool needs_drain = false; - LockMessageChecker lmc(gMessageSystem); - while (lmc.checkAllMessages(frame_count, gServicePump)) + ++total_decoded; + + // Time-box processing of network packets to prevent framerate catastrophe + if (check_message_timer.getElapsedTimeF32() >= CheckMessagesMaxTime) { - if (gDoDisconnect) + // Drain the socket buffer so we know how many messages remain to process + S32 num_buffered_packets = gMessageSystem->drainUdpSocket(); + if (num_buffered_packets > total_decoded) { - // We're disconnecting, don't process any more messages from the server - // We're usually disconnecting due to either network corruption or a - // server going down, so this is OK. - break; + // Grow CheckMessagesMaxTime until we process more packets each frame than arrive. + // This might spiral out of control on very slow computers on fast networks when + // the bandwidth settings are too high. There is a mechanism for providing backpressure + // to network bandwidth but it may be inadequate for the task + // (see LLViewerThrottle::updateDynamicThrottle() for more details). + CheckMessagesMaxTime *= 1.035f; // 3.5% ~= 2x in 20 frames, ~8x in 60 frames } - - total_decoded++; - - if (total_decoded > MESSAGE_MAX_PER_FRAME) + else if (num_buffered_packets == 0) { - needs_drain = true; - break; - } - - // Prevent slow packets from completely destroying the frame rate. - // This usually happens due to clumps of avatars taking huge amount - // of network processing time (which needs to be fixed, but this is - // a good limit anyway). - total_time = check_message_timer.getElapsedTimeF32(); - if (total_time >= CheckMessagesMaxTime) - { - needs_drain = true; - break; + // Reset CheckMessagesMaxTime to default value + CheckMessagesMaxTime = CHECK_MESSAGES_DEFAULT_MAX_TIME; } + break; } - if (needs_drain || gMessageSystem->mPacketRing.getNumBufferedPackets() > 0) + + if (total_decoded > MESSAGE_MAX_PER_FRAME) { - // Rather than allow packets to silently backup on the socket - // we drain them into our own buffer so we know how many exist. - S32 num_buffered_packets = gMessageSystem->drainUdpSocket(); - if (num_buffered_packets > 0) - { - // Increase CheckMessagesMaxTime so that we will eventually catch up - CheckMessagesMaxTime *= 1.035f; // 3.5% ~= 2x in 20 frames, ~8x in 60 frames - } + // MESSAGE_MAX_PER_FRAME is very high (400) + // We expect to run out of time before reaching here, but just in case... + gMessageSystem->drainUdpSocket(); + break; } - else + + if (gDoDisconnect) { - // Reset CheckMessagesMaxTime to default value - CheckMessagesMaxTime = CHECK_MESSAGES_DEFAULT_MAX_TIME; + // We're disconnecting so no need to process packets. + break; } + } - // Handle per-frame message system processing. + // Handle per-frame message system processing. - static LLCachedControl ack_collection_time(gSavedSettings, "AckCollectTime", 0.1f); - lmc.processAcks(ack_collection_time()); - } + static LLCachedControl ack_collection_time(gSavedSettings, "AckCollectTime", 0.1f); + lmc.processAcks(ack_collection_time()); } add(LLStatViewer::NUM_NEW_OBJECTS, gObjectList.mNumNewObjects); @@ -5847,6 +6104,9 @@ void LLAppViewer::disconnectViewer() // Pass the connection state to LLUrlEntryParcel not to attempt // parcel info requests while disconnected. LLUrlEntryParcel::setDisconnected(gDisconnected); + + // Restore default OS hibernation mode + setPermitOSHibernation(true); } void LLAppViewer::forceErrorLLError() @@ -6061,6 +6321,32 @@ F32 LLAppViewer::getMainloopTimeoutSec() const } } +std::string LLAppViewer::getMainloopWatchdogState() const +{ + if (!mMainloopTimeout) + { + return std::string(); + } + std::string state = mMainloopTimeout->getState(); + + if (mMainloopTimeout->hasExpired()) + { + return "Expired at " + state; + } + + // Check if the watchdog is currently active (timer started) + if (!mMainloopTimeout->isAlive()) + { + // Timer is not running, meaning watchdog is paused/stopped + if (state.empty()) + { + return "Paused"; + } + return "Paused at " + state; + } + return state; +} + void LLAppViewer::handleLoginComplete() { gLoggedInTime.start(); @@ -6112,6 +6398,15 @@ void LLAppViewer::handleLoginComplete() // we logged in successfully, so save settings on logout LL_INFOS() << "Login successful, per account settings will be saved on log out." << LL_ENDL; mSavePerAccountSettings=true; + + // Don't allow hibernation while we're running + setPermitOSHibernation(false); + // Track 'hibernation' mode changes + mOSHibernationModeChangeConnection = gSavedSettings.getControl("OSHibernationMode")->getSignal()->connect([](LLControlVariable* control, const LLSD& new_val, const LLSD& old_val) + { + // setPermitOSHibernation will sort itself out based on new mode. + LLAppViewer::instance()->setPermitOSHibernation(false); + }); } //virtual diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index d76e5015e94..1b1a89d7566 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -17,7 +17,7 @@ * * $LicenseInfo:firstyear=2007&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 @@ -76,9 +76,10 @@ typedef enum LAST_EXEC_LOGOUT_CRASH, LAST_EXEC_BAD_ALLOC, LAST_EXEC_MISSING_FILES, - LAST_EXEC_GRAPHICS_INIT, + LAST_EXEC_INIT, LAST_EXEC_UNKNOWN, LAST_EXEC_LOGOUT_UNKNOWN, + LAST_EXEC_OS_EVENT, LAST_EXEC_COUNT } eLastExecEvent; @@ -113,6 +114,7 @@ class LLAppViewer : public LLApp const LLSD& substitutions = LLSD()); // Display an error dialog and forcibly quit. void earlyExitNoNotify(); // Do not display error dialog then forcibly quit. void abortQuit(); // Called to abort a quit request. + void sendViewerStatistics(bool include_preferences); bool quitRequested() { return mQuitRequested; } bool logoutRequestSent() { return mLogoutRequestSent; } @@ -210,6 +212,7 @@ class LLAppViewer : public LLApp void pingMainloopTimeout(std::string_view state); F32 getMainloopTimeoutSec() const; + std::string getMainloopWatchdogState() const; // Handle the 'login completed' event. // *NOTE:Mani Fix this for login abstraction!! @@ -254,6 +257,10 @@ class LLAppViewer : public LLApp void createErrorMarker(eLastExecEvent error_code) const; bool errorMarkerExists() const; + void createCloseRequestMarker() const; + void removeCloseRequestMarker() const; + void createInitedMarker() const; + void removeInitedMarker() const; void createWatchdogMarker() const; void removeWatchdogMarker() const; @@ -263,6 +270,8 @@ class LLAppViewer : public LLApp // Note: mQuitRequested can be aborted by user. void outOfMemorySoftQuit(); + virtual void setPermitOSHibernation(bool permit); + #ifdef LL_DISCORD static void initDiscordSocial(); static void updateDiscordActivity(); @@ -274,10 +283,19 @@ class LLAppViewer : public LLApp virtual bool initWindow(); // Initialize the viewer's window. virtual void initLoggingAndGetLastDuration(); // Initialize log files, logging system virtual void initConsole() {}; // Initialize OS level debugging console. + virtual void cleanupConsole() {}; // Cleanup OS level debugging console. virtual bool initHardwareTest() { return true; } // A false result indicates the app should quit. virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + typedef enum + { + LL_HIBERNATE_MODE_DEFAULT = 0, // Use the platform's default behavior. + LL_HIBERNATE_MODE_PREVENT = 1, + LL_HIBERNATE_MODE_PREVENT_SCREEN = 2, + } eHibernationMode; + virtual void setOSHibernationMode(eHibernationMode mode); + virtual bool initParseCommandLine(LLCommandLineParser& clp) { return true; } // Allow platforms to specify the command line args. @@ -384,6 +402,9 @@ class LLAppViewer : public LLApp LLAppCoreHttp mAppCoreHttp; bool mIsFirstRun; + + eHibernationMode mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + boost::signals2::scoped_connection mOSHibernationModeChangeConnection; }; // Globals with external linkage. From viewer.h diff --git a/indra/newview/llappviewerlinux.cpp b/indra/newview/llappviewerlinux.cpp index 89d19d180b5..fe0d6f6e7da 100644 --- a/indra/newview/llappviewerlinux.cpp +++ b/indra/newview/llappviewerlinux.cpp @@ -58,6 +58,10 @@ namespace void (*gOldTerminateHandler)() = NULL; } +// Initialize static members +guint32 LLAppViewerLinux::sPowerInhibitCookie = 0; +bool LLAppViewerLinux::sPowerInhibitActive = false; + static void exceptionTerminateHandler() { @@ -117,6 +121,11 @@ LLAppViewerLinux::LLAppViewerLinux() LLAppViewerLinux::~LLAppViewerLinux() { + // Clean up any power management inhibition on exit + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } } bool LLAppViewerLinux::init() @@ -329,6 +338,299 @@ bool LLAppViewerLinux::sendURLToOtherInstance(const std::string& url) } #endif // LL_DBUS_ENABLED + +void LLAppViewerLinux::setOSHibernationMode(eHibernationMode mode) +{ + if (mode == LL_HIBERNATE_MODE_DEFAULT) + { + // Allow OS to sleep/hibernate - remove any inhibition + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT) + { + // Prevent system sleep, but allow display to turn off + // Release any existing inhibition first to allow mode switching + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } + + if (inhibitPowerManagement(false)) + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep, display sleep allowed" << LL_ENDL; + } + else + { + LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Prevent both system and display sleep + // Release any existing inhibition first to allow mode switching + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } + + if (inhibitPowerManagement(true)) + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep and display sleep" << LL_ENDL; + } + else + { + LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep and display sleep" << LL_ENDL; + } + } +} + +// TODO: This is AI Generated!!!, needs review and testing. +bool LLAppViewerLinux::inhibitPowerManagement(bool inhibit_display) +{ +#if LL_DBUS_ENABLED + // Try to use D-Bus to inhibit power management via various desktop environment APIs + // This works with GNOME, KDE, XFCE, and most modern Linux desktop environments + + if (!grab_dbus_syms(DBUSGLIB_DYLIB_DEFAULT_NAME)) + { + LL_WARNS("OS") << "Failed to load D-Bus symbols for power management" << LL_ENDL; + return false; + } + + GError* error = nullptr; + DBusGConnection* bus = lldbus_g_bus_get(DBUS_BUS_SESSION, &error); + + if (!bus) + { + LL_WARNS("OS") << "Failed to connect to D-Bus session bus: " + << (error ? error->message : "unknown error") << LL_ENDL; + if (error) + g_error_free(error); + return false; + } + + // Try multiple power management services in order of preference + // 1. org.freedesktop.PowerManagement (older standard) + // 2. org.gnome.SessionManager (GNOME) + // 3. org.kde.Solid.PowerManagement (KDE) + + const char* services[] = { + "org.freedesktop.PowerManagement", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* paths[] = { + "/org/freedesktop/PowerManagement/Inhibit", + "/org/gnome/SessionManager", + "/org/kde/Solid/PowerManagement" + }; + + const char* interfaces[] = { + "org.freedesktop.PowerManagement.Inhibit", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* methods[] = { + "Inhibit", + "Inhibit", + "inhibit" + }; + + bool success = false; + + for (int i = 0; i < 3 && !success; ++i) + { + DBusGProxy* proxy = lldbus_g_proxy_new_for_name( + bus, + services[i], + paths[i], + interfaces[i] + ); + + if (!proxy) + continue; + + error = nullptr; + guint32 cookie = 0; + + if (i == 0) // freedesktop.PowerManagement + { + // Inhibit(application_name: s, reason: s) -> cookie: u + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_STRING, "Second Life Viewer", + G_TYPE_STRING, inhibit_display ? + "Viewer active - preventing system and display sleep" : + "Viewer active - preventing system sleep", + G_TYPE_INVALID, + G_TYPE_UINT, &cookie, + G_TYPE_INVALID + ); + } + else if (i == 1) // GNOME SessionManager + { + // Inhibit(app_id: s, toplevel_xid: u, reason: s, flags: u) -> cookie: u + // flags: 4 = suspend, 8 = idle (display), 12 = both + guint32 flags = inhibit_display ? 12 : 4; + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_STRING, "SecondLifeViewer", + G_TYPE_UINT, 0, // toplevel_xid (0 = none) + G_TYPE_STRING, inhibit_display ? + "Viewer active - preventing system and display sleep" : + "Viewer active - preventing system sleep", + G_TYPE_UINT, flags, + G_TYPE_INVALID, + G_TYPE_UINT, &cookie, + G_TYPE_INVALID + ); + } + else if (i == 2) // KDE Solid + { + // Different method signature for KDE + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_INVALID, + G_TYPE_INT, &cookie, + G_TYPE_INVALID + ); + } + + if (success) + { + sPowerInhibitCookie = cookie; + sPowerInhibitActive = true; + LL_INFOS("OS") << "Successfully inhibited power management using " + << services[i] << LL_ENDL; + } + else if (error) + { + LL_DEBUGS("OS") << "Failed to inhibit via " << services[i] + << ": " << error->message << LL_ENDL; + g_error_free(error); + error = nullptr; + } + + g_object_unref(proxy); + } + + return success; + +#else // !LL_DBUS_ENABLED + LL_WARNS("OS") << "Power management control not available - D-Bus support not enabled" << LL_ENDL; + return false; +#endif +} + +void LLAppViewerLinux::uninhibitPowerManagement() +{ +#if LL_DBUS_ENABLED + if (!sPowerInhibitActive || sPowerInhibitCookie == 0) + { + return; + } + + if (!grab_dbus_syms(DBUSGLIB_DYLIB_DEFAULT_NAME)) + { + LL_WARNS("OS") << "Failed to load D-Bus symbols for power management uninhibit" << LL_ENDL; + return; + } + + GError* error = nullptr; + DBusGConnection* bus = lldbus_g_bus_get(DBUS_BUS_SESSION, &error); + + if (!bus) + { + if (error) + g_error_free(error); + return; + } + + // Try to uninhibit using all services that might have been used + const char* services[] = { + "org.freedesktop.PowerManagement", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* paths[] = { + "/org/freedesktop/PowerManagement/Inhibit", + "/org/gnome/SessionManager", + "/org/kde/Solid/PowerManagement" + }; + + const char* interfaces[] = { + "org.freedesktop.PowerManagement.Inhibit", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* methods[] = { + "UnInhibit", + "Uninhibit", + "uninhibit" + }; + + bool success = false; + + for (int i = 0; i < 3; ++i) + { + DBusGProxy* proxy = lldbus_g_proxy_new_for_name( + bus, + services[i], + paths[i], + interfaces[i] + ); + + if (!proxy) + continue; + + error = nullptr; + + if (lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_UINT, sPowerInhibitCookie, + G_TYPE_INVALID, + G_TYPE_INVALID)) + { + success = true; + LL_INFOS("OS") << "Successfully uninhibited power management using " + << services[i] << LL_ENDL; + } + else if (error) + { + LL_DEBUGS("OS") << "Failed to uninhibit via " << services[i] + << ": " << error->message << LL_ENDL; + g_error_free(error); + error = nullptr; + } + + g_object_unref(proxy); + + if (success) + break; + } + + sPowerInhibitCookie = 0; + sPowerInhibitActive = false; + +#endif // LL_DBUS_ENABLED +} + void LLAppViewerLinux::initCrashReporting(bool reportFreeze) { std::string cmd =gDirUtilp->getExecutableDir(); diff --git a/indra/newview/llappviewerlinux.h b/indra/newview/llappviewerlinux.h index dde223878da..6ab3682515d 100644 --- a/indra/newview/llappviewerlinux.h +++ b/indra/newview/llappviewerlinux.h @@ -68,6 +68,16 @@ class LLAppViewerLinux : public LLAppViewer virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + virtual void setOSHibernationMode(eHibernationMode mode); + +private: + // Power management state tracking + static guint32 sPowerInhibitCookie; + static bool sPowerInhibitActive; + + // Helper methods for power management + bool inhibitPowerManagement(bool inhibit_display); + void uninhibitPowerManagement(); }; #if LL_DBUS_ENABLED diff --git a/indra/newview/llappviewermacosx-objc.h b/indra/newview/llappviewermacosx-objc.h index 3fbf4202f1a..13b80561933 100644 --- a/indra/newview/llappviewermacosx-objc.h +++ b/indra/newview/llappviewermacosx-objc.h @@ -31,5 +31,7 @@ #include void force_ns_sxeption(); +void register_url_schemes(); +void set_os_hibernation_mode(int mode); #endif // LL_LLAPPVIEWERMACOSX_OBJC_H diff --git a/indra/newview/llappviewermacosx-objc.mm b/indra/newview/llappviewermacosx-objc.mm index 96a6bc6edce..ab4ae428b98 100644 --- a/indra/newview/llappviewermacosx-objc.mm +++ b/indra/newview/llappviewermacosx-objc.mm @@ -29,6 +29,7 @@ #endif #import +#import #include #include "llappviewermacosx-objc.h" @@ -38,3 +39,93 @@ void force_ns_sxeption() NSException *exception = [NSException exceptionWithName:@"Forced NSException" reason:nullptr userInfo:nullptr]; @throw exception; } + +void register_url_schemes() +{ + @autoreleasepool // Objective-C automatic memory tracking and release. + { + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath]; + + // Force Launch Services to re-register this app bundle + OSStatus status = LSRegisterURL((__bridge CFURLRef)bundleURL, true); + + if (status == noErr) + { + // Explicitly set this app as the default handler for our URL schemes + NSArray *schemes = @[@"secondlife", @"x-grid-location-info"]; + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + + for (NSString *scheme in schemes) + { + LSSetDefaultHandlerForURLScheme((__bridge CFStringRef)scheme, + (__bridge CFStringRef)bundleID); + } + } + } +} + +// Add these as static variables at file scope +static IOPMAssertionID gPowerAssertionID = kIOPMNullAssertionID; + +void set_os_hibernation_mode(int mode) +{ + // Release existing assertion + if (gPowerAssertionID != kIOPMNullAssertionID) + { + IOReturn result = IOPMAssertionRelease(gPowerAssertionID); + if (result == kIOReturnSuccess) + { + gPowerAssertionID = kIOPMNullAssertionID; + NSLog(@"Permitted OS hibernation/sleep"); + } + else + { + NSLog(@"Failed to release power assertion: %d", result); + } + } + + if (mode == 1) + { + // Prevent OS from sleeping/hibernating + CFStringRef assertionName = CFSTR("Second Life Viewer"); + // kIOPMAssertionTypeNoIdleSleep prevents idle sleep + IOReturn result = IOPMAssertionCreateWithName( + kIOPMAssertionTypeNoIdleSleep, + kIOPMAssertionLevelOn, + assertionName, + &gPowerAssertionID + ); + + if (result == kIOReturnSuccess) + { + NSLog(@"Prevented OS hibernation/sleep, allow display sleep"); + } + else + { + NSLog(@"Failed to create power assertion: %d", result); + } + } + else if (mode == 2) + { + // Prevent OS from sleeping/hibernating, prevent screen from going off + CFStringRef assertionName = CFSTR("Second Life Viewer"); + // kIOPMAssertionTypeNoIdleSleep prevents idle sleep + // kIOPMAssertionTypeNoDisplaySleep prevents display sleep + IOReturn result = IOPMAssertionCreateWithName( + kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, + assertionName, + &gPowerAssertionID + ); + + if (result == kIOReturnSuccess) + { + NSLog(@"Prevented OS hibernation/sleep or screen from turning off"); + } + else + { + NSLog(@"Failed to create power assertion: %d", result); + } + } +} diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp index 2bd9c8661a5..c188bb459f3 100644 --- a/indra/newview/llappviewermacosx.cpp +++ b/indra/newview/llappviewermacosx.cpp @@ -49,6 +49,7 @@ #include "llerrorcontrol.h" #include "llvoavatarself.h" // for gAgentAvatarp->getFullname() #include +#include "llwindowmacosx_iokit.h" #ifdef LL_CARBON_CRASH_HANDLER #include #endif @@ -125,6 +126,7 @@ bool pumpMainLoop() void cleanupViewer() { + set_os_hibernation_mode(0); // restore default OS hibernation behavior if(!LLApp::isError()) { if (gViewerAppPtr) @@ -412,6 +414,27 @@ bool LLAppViewerMacOSX::restoreErrorTrap() return reset_count == 0; } +bool LLAppViewerMacOSX::initSLURLHandler() +{ + if (isSecondInstance()) + { + return false; + } + // Main secondlife:// registration is in info.plist, but macOS + // Launch Services caches URL scheme handlers, and a different + // viewer might still be registered. + // Register URL schemes with Launch Services on every launch + register_url_schemes(); + + return true; +} + +void LLAppViewerMacOSX::setOSHibernationMode(eHibernationMode mode) +{ + // pass to objective-c++ + set_os_hibernation_mode((int)mode); +} + std::string LLAppViewerMacOSX::generateSerialNumber() { char serial_md5[MD5HEX_STR_SIZE]; // Flawfinder: ignore @@ -419,7 +442,7 @@ std::string LLAppViewerMacOSX::generateSerialNumber() // JC: Sample code from http://developer.apple.com/technotes/tn/tn1103.html CFStringRef serialNumber = NULL; - io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, + io_service_t platformExpert = IOServiceGetMatchingService(kLLIOMainPort, IOServiceMatching("IOPlatformExpertDevice")); if (platformExpert) { diff --git a/indra/newview/llappviewermacosx.h b/indra/newview/llappviewermacosx.h index d50812a35ee..1153bd4191c 100644 --- a/indra/newview/llappviewermacosx.h +++ b/indra/newview/llappviewermacosx.h @@ -46,6 +46,8 @@ class LLAppViewerMacOSX : public LLAppViewer protected: virtual bool restoreErrorTrap(); + virtual bool initSLURLHandler(); + virtual void setOSHibernationMode(eHibernationMode mode); std::string generateSerialNumber(); virtual bool initParseCommandLine(LLCommandLineParser& clp); diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 2e4e9e29d52..416efa93545 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&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 @@ -193,6 +193,14 @@ namespace } LLAppViewer* app = LLAppViewer::instance(); + + // Include mainloop watchdog state if available + std::string watchdog_state = app->getMainloopWatchdogState(); + if (!watchdog_state.empty()) + { + sBugSplatSender->setAttribute(WCSTR(L"WatchdogState"), WCSTR(watchdog_state)); + } + if (!app->isSecondInstance() && !app->errorMarkerExists()) { // If marker doesn't exist, create a marker with 'other' or 'logout' code for next launch @@ -840,12 +848,7 @@ bool LLAppViewerWin32::cleanup() bool result = LLAppViewer::cleanup(); gDXHardware.cleanup(); - - if (mIsConsoleAllocated) - { - FreeConsole(); - mIsConsoleAllocated = false; - } + cleanupConsole(); return result; } @@ -929,6 +932,15 @@ void LLAppViewerWin32::initConsole() return LLAppViewer::initConsole(); } +void LLAppViewerWin32::cleanupConsole() +{ + if (mIsConsoleAllocated) + { + FreeConsole(); + mIsConsoleAllocated = false; + } +} + void write_debug_dx(const char* str) { std::string value = gDebugInfo["DXInfo"].asString(); @@ -1001,7 +1013,7 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) if (other_window != NULL) { - LL_DEBUGS() << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; + LL_DEBUGS("AppInit") << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; COPYDATASTRUCT cds; const S32 SLURL_MESSAGE_TYPE = 0; cds.dwData = SLURL_MESSAGE_TYPE; @@ -1009,13 +1021,281 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) cds.lpData = (void*)url.c_str(); LRESULT msg_result = SendMessage(other_window, WM_COPYDATA, NULL, (LPARAM)&cds); - LL_DEBUGS() << "SendMessage(WM_COPYDATA) to other window '" + LL_DEBUGS("AppInit") << "SendMessage(WM_COPYDATA) to other window '" << getWindowTitle() << "' returned " << msg_result << LL_ENDL; return true; } return false; } +void LLAppViewerWin32::setOSHibernationMode(eHibernationMode mode) +{ + // ES_CONTINUOUS tells Windows to reset the idle timer + // and restore normal operation + // ES_SYSTEM_REQUIRED prevents system sleep/hibernation + // ES_DISPLAY_REQUIRED prevents display sleep + + if (mode == LL_HIBERNATE_MODE_DEFAULT) + { + // Allow OS to hibernate - clear the previous execution state flags + // ES_CONTINUOUS without other flags allows the system to idle normally + SetThreadExecutionState(ES_CONTINUOUS); + LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; + } + else if (mode == LL_HIBERNATE_MODE_PREVENT) + { + // Prevent OS from hibernating while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED + ); + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation, but allowed display sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Prevent OS from hibernating or turning screen off while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + // ES_DISPLAY_REQUIRED keeps the display on + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED + ); + + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation and display sleep, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep" << LL_ENDL; + } + } +} + +bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir) +{ + // Velopack installs viewer like this: + // %appdata%\Local\ChannelNameViewer\Update.exe // which is our uninstaller + // %appdata%\Local\ChannelNameViewer\SecondLifeViewer.exe // wrapper, redirects to main executable + // %appdata%\Local\ChannelNameViewer\current\SecondLifeViewer.exe // main executable + // For reliability don't expect install_dir to be actually in the base path, strip 'current' + + const std::wstring current_suffix = L"\\current"; + std::wstring normalized_path(install_dir); + if (normalized_path.length() >= current_suffix.length() && + _wcsicmp(normalized_path.c_str() + normalized_path.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + normalized_path.resize(normalized_path.length() - current_suffix.length()); + } + + wchar_t window_class[256]; // Assume max length < 255 chars. + mbstowcs(window_class, sWindowClass, 255); + window_class[255] = 0; + + // Normalize the directory path + wchar_t our_dir_normalized[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(normalized_path.c_str(), MAX_PATH, our_dir_normalized, &file_part); + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS() << "Failed to normalize our executable path" << LL_ENDL; + return false; + } + + // Remove trailing backslash if present + size_t dir_len = wcslen(our_dir_normalized); + if (dir_len > 0 && our_dir_normalized[dir_len - 1] == L'\\') + { + our_dir_normalized[dir_len - 1] = L'\0'; + dir_len--; + } + + // This message is meant for velopack, so we don't expect to have + // a window of our own, store any matching windows. + struct EnumData + { + const wchar_t* target_class; + const wchar_t* our_dir_normalized; + std::vector found_windows; + }; + + EnumData enum_data; + enum_data.target_class = window_class; + enum_data.our_dir_normalized = our_dir_normalized; + + // Callback function to find all matching windows + auto find_windows_callback = [](HWND hwnd, LPARAM lParam) -> BOOL + { + EnumData* data = reinterpret_cast(lParam); + wchar_t class_name[256]; + + if (GetClassName(hwnd, class_name, 256) > 0) + { + if (wcscmp(class_name, data->target_class) == 0) + { + // Get the process ID for this window + DWORD process_id = 0; + GetWindowThreadProcessId(hwnd, &process_id); + + // Open the process to query its executable path + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id); + if (hProcess) + { + wchar_t exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + if (QueryFullProcessImageNameW(hProcess, 0, exe_path, &size)) + { + // Normalize the other process's path + wchar_t other_dir_normalized[MAX_PATH]; + wchar_t* other_file_part = nullptr; + DWORD result = GetFullPathNameW(exe_path, MAX_PATH, other_dir_normalized, &other_file_part); + + if (result > 0 && result < MAX_PATH) + { + // Remove the filename part to get just the directory + // We are doing this to avoid incidents, like having + // multiple viewer version exes in the same folder. + if (other_file_part) + { + *other_file_part = L'\0'; + } + + // Remove trailing backslash if present + size_t other_dir_len = wcslen(other_dir_normalized); + if (other_dir_len > 0 && other_dir_normalized[other_dir_len - 1] == L'\\') + { + other_dir_normalized[other_dir_len - 1] = L'\0'; + other_dir_len--; + } + + // Strip "\current" suffix if present to normalize comparison + // This handles both release (with \current) and debug builds (without) + const std::wstring current_suffix = L"\\current"; + if (other_dir_len >= current_suffix.length()) + { + size_t offset = other_dir_len - current_suffix.length(); + if (_wcsicmp(other_dir_normalized + offset, current_suffix.c_str()) == 0) + { + other_dir_normalized[offset] = L'\0'; + } + } + + // Compare directories (case-insensitive) + if (_wcsicmp(other_dir_normalized, data->our_dir_normalized) == 0) + { + data->found_windows.push_back(hwnd); + } + } + } + CloseHandle(hProcess); + } + } + } + + return TRUE; // Continue enumeration + }; + + // Find all matching windows and send shutdown messages + EnumWindows(find_windows_callback, reinterpret_cast(&enum_data)); + + if (enum_data.found_windows.empty()) + { + LL_DEBUGS("AppInit") << "No other instances found" << LL_ENDL; + return false; + } + + LL_INFOS("AppInit") << "Found " << (S32)(enum_data.found_windows.size()) << " other instance(s), sending shutdown messages" << LL_ENDL; + + // Get our own process ID to include in the message + DWORD our_process_id = GetCurrentProcessId(); + + constexpr UINT timeout_ms = 2000; // 2s. Viewer's message thread is supposed to be fast. + for (HWND other_window : enum_data.found_windows) + { + if (IsWindow(other_window)) + { + DWORD_PTR result = 0; + LRESULT send_result = SendMessageTimeout( + other_window, + WM_POST_UNINSTALL_, + static_cast(our_process_id), + static_cast(WM_POST_UNINSTALL_MSG_SHUTDOWN), + SMTO_ABORTIFHUNG | SMTO_BLOCK, + timeout_ms, + &result + ); + + if (send_result == 0) + { + DWORD error = GetLastError(); + if (error == ERROR_TIMEOUT) + { + LL_WARNS("AppInit") << "Shutdown message timed out for window " << std::hex << other_window << std::dec << LL_ENDL; + } + else + { + LL_WARNS("AppInit") << "Failed to send shutdown message to window " << std::hex << other_window + << ", error: " << error << std::dec << LL_ENDL; + } + + PostMessage(other_window, WM_CLOSE, 0, 0); + } + else + { + LL_DEBUGS("AppInit") << "Shutdown message sent successfully to window " << std::hex << other_window << std::dec << LL_ENDL; + } + } + } + + // Poll for up to 30 seconds, checking every 5 seconds + const S32 MAX_WAIT_TIME_MS = 60000; // 30 seconds + const S32 POLL_INTERVAL_MS = 5000; // 5 seconds + S32 elapsed_time_ms = 0; + size_t still_open_count = enum_data.found_windows.size(); + + while (elapsed_time_ms < MAX_WAIT_TIME_MS) + { + LL_INFOS("AppInit") << "Waiting for " << (S32)still_open_count << " instance(s) to close... (" + << (S32)(elapsed_time_ms / 1000) << "s elapsed)" << LL_ENDL; + + ms_sleep(POLL_INTERVAL_MS); + elapsed_time_ms += POLL_INTERVAL_MS; + + // Check if the specific windows we found still exist + // Don't enumerate all windows for new ones, assume that + // no instances were reused and assume user won't open + // the app again. For now just check our list. + still_open_count = 0; + for (HWND hwnd : enum_data.found_windows) + { + if (IsWindow(hwnd)) + { + still_open_count++; + } + } + + if (still_open_count == 0) + { + LL_INFOS("AppInit") << "All other instances have closed after " << (S32)(elapsed_time_ms / 1000) << " seconds" << LL_ENDL; + return false; + } + } + + if (still_open_count != 0) + { + LL_WARNS("AppInit") << "Proceeding with uninstall with " << (S32)still_open_count << " instance(s) still open." << LL_ENDL; + } + + return true; +} + std::string LLAppViewerWin32::generateSerialNumber() { diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 53177f7f951..7c3c7a475a4 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&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 @@ -46,10 +46,15 @@ class LLAppViewerWin32 : public LLAppViewer bool reportCrashToBugsplat(void* pExcepInfo) override; bool reportCustomToBugsplat(const std::string& description) override; + // returns true if other windows were found and are still running. + static bool sendShutdownToOtherInstances(const std::wstring& install_dir); + protected: bool initWindow() override; // Override to initialize the viewer's window. void initLoggingAndGetLastDuration() override; // Override to clean stack_trace info. void initConsole() override; // Initialize OS level debugging console. + void cleanupConsole() override; + bool initHardwareTest() override; // Win32 uses DX9 to test hardware. bool initParseCommandLine(LLCommandLineParser& clp) override; @@ -57,6 +62,7 @@ class LLAppViewerWin32 : public LLAppViewer bool restoreErrorTrap() override; bool sendURLToOtherInstance(const std::string& url) override; + void setOSHibernationMode(eHibernationMode mode) override; std::string generateSerialNumber(); diff --git a/indra/newview/llavataractions.cpp b/indra/newview/llavataractions.cpp index fb1426a2353..c95e98982d7 100644 --- a/indra/newview/llavataractions.cpp +++ b/indra/newview/llavataractions.cpp @@ -853,6 +853,13 @@ namespace action_give_inventory break; } LLViewerInventoryItem* inv_item = gInventory.getItem(*it); + if (!inv_item) + { + shared = false; + LL_WARNS() << "Failed to share an item " << *it + << ". Item was not found in inventory." << LL_ENDL; + continue; + } if (!inv_item->getPermissions().allowCopyBy(gAgentID)) { if (!noncopy_item_names.empty()) diff --git a/indra/newview/llchathistory.cpp b/indra/newview/llchathistory.cpp index c1af09ebc76..af94c6d4435 100644 --- a/indra/newview/llchathistory.cpp +++ b/indra/newview/llchathistory.cpp @@ -175,6 +175,49 @@ class LLChatHistoryHeader: public LLPanel LLFloaterSidePanelContainer::showPanel("people", "panel_people", LLSD().with("people_panel_tab_name", "blocked_panel").with("blocked_to_select", getAvatarId())); } + else if (level == "report_abuse") + { + std::string time_string; + if (mTime > 0) // have frame time + { + time_t current_time = time_corrected(); + time_t message_time = (time_t)(current_time - LLFrameTimer::getElapsedSeconds() + mTime); + + // Report abuse shouldn't use AM/PM, use 24-hour time + time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + + LLTrans::getString("TimeDay") + "]/[" + + LLTrans::getString("TimeYear") + "] [" + + LLTrans::getString("TimeHour") + "]:[" + + LLTrans::getString("TimeMin") + "]"; + + LLSD substitution; + + substitution["datetime"] = (S32)message_time; + LLStringUtil::format(time_string, substitution); + } + else + { + // From history. This might be empty or not full. + // See LLChatLogParser::parse + time_string = getChild("time_box")->getValue().asString(); + + // Just add current date if not full. + // Should be fine since both times are supposed to be SLT. + if (!time_string.empty() && time_string.size() < 7) + { + time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + + LLTrans::getString("TimeDay") + "]/[" + + LLTrans::getString("TimeYear") + "] " + time_string; + + LLSD substitution; + // To avoid adding today's date to yesterday's timestamp, + // use creation time instead of current time + substitution["datetime"] = (S32)mCreationTime; + LLStringUtil::format(time_string, substitution); + } + } + LLFloaterReporter::showFromChatObj(getAvatarId(), time_string, mText); + } else if (level == "unblock") { LLMuteList::getInstance()->remove(LLMute(getAvatarId(), mFrom, LLMute::OBJECT)); @@ -461,7 +504,7 @@ class LLChatHistoryHeader: public LLPanel time_string = getChild("time_box")->getValue().asString(); // Just add current date if not full. - // Should be fine since both times are supposed to be stl + // Should be fine since both times are supposed to be SLT. if (!time_string.empty() && time_string.size() < 7) { time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" @@ -475,7 +518,7 @@ class LLChatHistoryHeader: public LLPanel LLStringUtil::format(time_string, substitution); } } - LLFloaterReporter::showFromChat(mAvatarID, mFrom, time_string, mText); + LLFloaterReporter::showFromChatAv(mAvatarID, mFrom, time_string, mText); } else if(level == "block_unblock") { diff --git a/indra/newview/llchatitemscontainerctrl.cpp b/indra/newview/llchatitemscontainerctrl.cpp index 5ac4ce0d522..2e8fb752b8b 100644 --- a/indra/newview/llchatitemscontainerctrl.cpp +++ b/indra/newview/llchatitemscontainerctrl.cpp @@ -35,8 +35,10 @@ #include "llcommandhandler.h" #include "llfloaterreg.h" #include "lllocalcliprect.h" +#include "llpanelblockedlist.h" #include "lltrans.h" #include "llfloaterimnearbychat.h" +#include "llfloaterreporter.h" #include "llfloaterworldmap.h" #include "llviewermenu.h" @@ -93,6 +95,32 @@ class LLObjectHandler : public LLCommandHandler } return true; } + if (verb == "block") + { + if (params.size() > 2) + { + const std::string object_name = LLURI::unescape(params[2].asString()); + LLMute mute(object_id, object_name, LLMute::OBJECT); + LLMuteList::getInstance()->add(mute); + LLPanelBlockedList::showPanelAndSelect(mute.mID); + } + return true; + } + if (verb == "unblock") + { + if (params.size() > 2) + { + const std::string object_name = params[2].asString(); + LLMute mute(object_id, object_name, LLMute::OBJECT); + LLMuteList::getInstance()->remove(mute); + } + return true; + } + if (verb == "reportAbuse" && web == NULL) + { + LLFloaterReporter::showFromObject(object_id, LLUUID::null); + return true; + } return false; } diff --git a/indra/newview/llcofwearables.cpp b/indra/newview/llcofwearables.cpp index 6bd15d4b617..f5d144dcc67 100644 --- a/indra/newview/llcofwearables.cpp +++ b/indra/newview/llcofwearables.cpp @@ -292,7 +292,8 @@ LLCOFWearables::LLCOFWearables() : LLPanel(), mBodyPartsTab(NULL), mLastSelectedTab(NULL), mAccordionCtrl(NULL), - mCOFVersion(-1) + mCOFVersion(-1), + mRefreshPending(false) { mClothingMenu = new CofClothingContextMenu(this); mAttachmentMenu = new CofAttachmentContextMenu(this); @@ -326,6 +327,10 @@ bool LLCOFWearables::postBuild() mClothing->setCommitOnSelectionChange(true); mBodyParts->setCommitOnSelectionChange(true); + mClothing->setReorderValidateCallback(boost::bind(&LLCOFWearables::canReorderClothing, this, _1, _2)); + mClothing->setReorderCallback(boost::bind(&LLCOFWearables::onClothingReordered, this, _1, _2)); + mClothing->setReorderEndedCallback(boost::bind(&LLCOFWearables::onReorderEnded, this)); + //clothing is sorted according to its position relatively to the body mAttachments->setComparator(&WEARABLE_NAME_COMPARATOR); mBodyParts->setComparator(&WEARABLE_NAME_COMPARATOR); @@ -377,6 +382,50 @@ void LLCOFWearables::onSelectionChange(LLFlatListView* selected_list) onCommit(); } +bool LLCOFWearables::canReorderClothing(const LLSD& dragged_value, const LLSD& neighbour_value) +{ + LLViewerInventoryItem* dragged = gInventory.getItem(dragged_value.asUUID()); + LLViewerInventoryItem* neighbour = gInventory.getItem(neighbour_value.asUUID()); + if (!dragged || !neighbour) return false; // reject placeholder/dummy rows + if (!dragged->isWearableType() || !neighbour->isWearableType()) return false; + + return dragged->getWearableType() == neighbour->getWearableType(); +} + +void LLCOFWearables::onClothingReordered(const LLSD& dragged_value, S32 /*new_index*/) +{ + LLViewerInventoryItem* item = gInventory.getItem(dragged_value.asUUID()); + if (!item || !item->isWearableType()) return; + + LLWearableType::EType type = item->getWearableType(); + + // Persist the affected type group's full order from the list's current order + // (furthest-to-closest), which covers both single- and multi-row drags. + uuid_vec_t ordered_link_ids; + std::vector values; + mClothing->getValues(values); + for (const LLSD& value : values) + { + LLViewerInventoryItem* other = gInventory.getItem(value.asUUID()); + if (!other || !other->isWearableType() || other->getWearableType() != type) continue; + + ordered_link_ids.push_back(value.asUUID()); + } + + if (ordered_link_ids.size() < 2) return; + + LLAppearanceMgr::getInstance()->reorderWearableGroup(type, ordered_link_ids); +} + +void LLCOFWearables::onReorderEnded() +{ + // Run a refresh that arrived (and was skipped) while the drag was active. + if (mRefreshPending) + { + refresh(); + } +} + void LLCOFWearables::onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expanded) { bool had_selected_items = mClothing->numSelected() || mAttachments->numSelected() || mBodyParts->numSelected(); @@ -401,6 +450,15 @@ void LLCOFWearables::onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expa void LLCOFWearables::refresh() { + // Don't rebuild mid-drag, clearing the list would cancel the active reorder. + // Remember it so onReorderEnded() can re-run the skipped refresh on release. + if (mClothing && mClothing->isReorderActive()) + { + mRefreshPending = true; + return; + } + mRefreshPending = false; + const LLUUID cof_id = LLAppearanceMgr::instance().getCOF(); if (cof_id.isNull()) { @@ -560,6 +618,12 @@ LLPanelClothingListItem* LLCOFWearables::buildClothingListItem(LLViewerInventory item_panel->setShowMoveUpButton(!first); item_panel->setShowMoveDownButton(!last); + // hint that rows in a multi-layer group can be dragged to reorder + if (!(first && last)) + { + item_panel->setToolTip(LLTrans::getString("ReorderClothingTooltip")); + } + //setting callbacks //*TODO move that item panel's inner structure disclosing stuff into the panels item_panel->childSetAction("btn_delete", boost::bind(mCOFCallbacks.mDeleteWearable)); diff --git a/indra/newview/llcofwearables.h b/indra/newview/llcofwearables.h index b349f35921e..7d4d8917b4b 100644 --- a/indra/newview/llcofwearables.h +++ b/indra/newview/llcofwearables.h @@ -102,6 +102,12 @@ class LLCOFWearables : public LLPanel void onSelectionChange(LLFlatListView* selected_list); void onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expanded); + // Clothing drag-to-reorder: only allow moves within the same wearable type, + // and translate a drop into a layer-order change. + bool canReorderClothing(const LLSD& dragged_value, const LLSD& neighbour_value); + void onClothingReordered(const LLSD& dragged_value, S32 new_index); + void onReorderEnded(); + LLPanelClothingListItem* buildClothingListItem(LLViewerInventoryItem* item, bool first, bool last); LLPanelBodyPartsListItem* buildBodypartListItem(LLViewerInventoryItem* item); LLPanelDeletableWearableListItem* buildAttachemntListItem(LLViewerInventoryItem* item); @@ -132,6 +138,9 @@ class LLCOFWearables : public LLPanel /* COF category version since last refresh */ S32 mCOFVersion; + + /* a refresh arrived mid-drag and was skipped; re-run it when the drag ends */ + bool mRefreshPending; }; diff --git a/indra/newview/lldrawpool.cpp b/indra/newview/lldrawpool.cpp index e60b3eb5dc5..cc702e8f564 100644 --- a/indra/newview/lldrawpool.cpp +++ b/indra/newview/lldrawpool.cpp @@ -440,7 +440,11 @@ void LLRenderPass::pushBatches(U32 type, bool texture, bool batch_textures) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - pushBatch(*pparams, texture, batch_textures); + llassert(pparams); // figure out how null got here, it shouldn't be happening + if (pparams) + { + pushBatch(*pparams, texture, batch_textures); + } } } else @@ -459,7 +463,10 @@ void LLRenderPass::pushUntexturedBatches(U32 type) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - pushUntexturedBatch(*pparams); + if (pparams) + { + pushUntexturedBatch(*pparams); + } } } @@ -479,7 +486,7 @@ void LLRenderPass::pushRiggedBatches(U32 type, bool texture, bool batch_textures LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + if (pparams && uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) { pushBatch(*pparams, texture, batch_textures); } @@ -504,7 +511,7 @@ void LLRenderPass::pushUntexturedRiggedBatches(U32 type) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + if (pparams && uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) { pushUntexturedBatch(*pparams); } @@ -520,8 +527,11 @@ void LLRenderPass::pushMaskBatches(U32 type, bool texture, bool batch_textures) { LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(pparams->mAlphaMaskCutoff); - pushBatch(*pparams, texture, batch_textures); + if (pparams) + { + LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(pparams->mAlphaMaskCutoff); + pushBatch(*pparams, texture, batch_textures); + } } } @@ -539,13 +549,16 @@ void LLRenderPass::pushRiggedMaskBatches(U32 type, bool texture, bool batch_text LLCullResult::increment_iterator(i, end); - llassert(pparams); - - LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(pparams->mAlphaMaskCutoff); + llassert(pparams); // figure out how null got here, it shouldn't be happening - if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + if (pparams) { - pushBatch(*pparams, texture, batch_textures); + LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(pparams->mAlphaMaskCutoff); + + if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + { + pushBatch(*pparams, texture, batch_textures); + } } } } diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index 9d1b11880b5..8755120920b 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -267,8 +267,8 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) gGL.setColorMask(true, false); - if (!rigged && getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER) - { //render "highlight alpha" on final non-rigged pass + if (!rigged && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) + { //render "highlight alpha" on final non-rigged pass for non-HUDs (HUDs only run pre-water alpha pass) // NOTE -- hacky call here protected by !rigged instead of alongside "forwardRender" // so renderDebugAlpha is executed while gls_pipeline_alpha and depth GL state // variables above are still in scope @@ -278,7 +278,7 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) void LLDrawPoolAlpha::renderDebugAlpha() { - if (sShowDebugAlpha && !gCubeSnapshot) + if (sShowDebugAlpha && !gCubeSnapshot && !LLPipeline::sReflectionRender) { gHighlightProgram.bind(); gGL.diffuseColor4f(1, 0, 0, 1); diff --git a/indra/newview/lleventpoll.cpp b/indra/newview/lleventpoll.cpp index 9eddec55c58..836ff634297 100644 --- a/indra/newview/lleventpoll.cpp +++ b/indra/newview/lleventpoll.cpp @@ -331,26 +331,51 @@ namespace Details { // LLSD is too smart for it's own good and may act like a smart // pointer for the content of (*i), so instead of passing (*i) - // pass a prepared name and move ownership of "body", - // as we are not going to need "body" anywhere else. + // pass a prepared name and copy the body std::string msg_name = (*i)["message"].asString(); - // WARNING: This is a shallow copy! - // If something still retains the data (like in httpAdapter?) this might still - // result in a crash, if it does appear to be the case, make a deep copy or - // convert data to string and pass that string. - const LLSD body = (*i)["body"]; - (*i)["body"].clear(); - work = [this, msg_name, body]() + // Create a deep copy using binary serialization + try { - handleMessage(msg_name, body); - }; + std::stringstream body_stream; + LLSDSerialize::toBinary((*i)["body"], body_stream); + std::string body_str = body_stream.str(); + (*i)["body"].clear(); + work = [this, msg_name, body_str]() + { + try + { + LLSD body; + std::istringstream istr(body_str); + LLSDSerialize::fromBinary(body, istr, body_str.size()); + handleMessage(msg_name, body); + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in handleMessage() for " << msg_name << LL_ENDL; + } + }; + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in eventPollCoro for " << msg_name << LL_ENDL; + } } main_queue->post(work); } else { - handleMessage(*i); + try + { + handleMessage(*i); + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in handleMessage() for " << (*i)["message"].asString() << LL_ENDL; + } } } } diff --git a/indra/newview/llfilepicker_mac.mm b/indra/newview/llfilepicker_mac.mm index 99e93bafbf7..3a0bc4005ea 100644 --- a/indra/newview/llfilepicker_mac.mm +++ b/indra/newview/llfilepicker_mac.mm @@ -26,15 +26,28 @@ #ifdef LL_DARWIN #import +#import #include #include "llfilepicker_mac.h" +// Convert a file extension or UTI string into a UTType for use with +// NSOpenPanel/NSSavePanel's allowedContentTypes. +static UTType *contentTypeForString(NSString *typeString) +{ + UTType *type = [UTType typeWithFilenameExtension:typeString]; + if (!type) + { + type = [UTType typeWithIdentifier:typeString]; + } + return type; +} + NSOpenPanel *init_panel(const std::vector* allowed_types, unsigned int flags) { int i; NSOpenPanel *panel = [NSOpenPanel openPanel]; - NSMutableArray *fileTypes = nil; + NSMutableArray *fileTypes = nil; if ( allowed_types && !allowed_types->empty()) @@ -43,9 +56,13 @@ for (i=0;isize();++i) { - [fileTypes addObject: - [NSString stringWithCString:(*allowed_types)[i].c_str() - encoding:[NSString defaultCStringEncoding]]]; + NSString *typeString = [NSString stringWithCString:(*allowed_types)[i].c_str() + encoding:[NSString defaultCStringEncoding]]; + UTType *type = contentTypeForString(typeString); + if (type) + { + [fileTypes addObject:type]; + } } } @@ -57,9 +74,9 @@ [panel setCanChooseFiles: ( (flags & F_FILE)?true:false )]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; - if (fileTypes) + if (fileTypes && fileTypes.count > 0) { - [panel setAllowedFileTypes:fileTypes]; + [panel setAllowedContentTypes:fileTypes]; } else { @@ -196,12 +213,25 @@ void doLoadDialogModeless(const std::vector* allowed_types, NSSavePanel *panel = [NSSavePanel savePanel]; NSString *extensionns = [NSString stringWithCString:extension->c_str() encoding:[NSString defaultCStringEncoding]]; - NSArray *fileType = [extensionns componentsSeparatedByString:@","]; + NSArray *extensions = [extensionns componentsSeparatedByString:@","]; + + NSMutableArray *fileType = [[NSMutableArray alloc] init]; + for (NSString *ext in extensions) + { + UTType *type = contentTypeForString(ext); + if (type) + { + [fileType addObject:type]; + } + } //[panel setMessage:@"Save Image File"]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; [panel setCanSelectHiddenExtension:true]; - [panel setAllowedFileTypes:fileType]; + if (fileType.count > 0) + { + [panel setAllowedContentTypes:fileType]; + } NSString *fileName = [NSString stringWithCString:file->c_str() encoding:[NSString defaultCStringEncoding]]; NSURL* url = [NSURL fileURLWithPath:fileName]; @@ -231,12 +261,25 @@ void doSaveDialogModeless(const std::string* file, NSSavePanel *panel = [NSSavePanel savePanel]; NSString *extensionns = [NSString stringWithCString:extension->c_str() encoding:[NSString defaultCStringEncoding]]; - NSArray *fileType = [extensionns componentsSeparatedByString:@","]; + NSArray *extensions = [extensionns componentsSeparatedByString:@","]; + + NSMutableArray *fileType = [[NSMutableArray alloc] init]; + for (NSString *ext in extensions) + { + UTType *type = contentTypeForString(ext); + if (type) + { + [fileType addObject:type]; + } + } //[panel setMessage:@"Save Image File"]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; [panel setCanSelectHiddenExtension:true]; - [panel setAllowedFileTypes:fileType]; + if (fileType.count > 0) + { + [panel setAllowedContentTypes:fileType]; + } NSString *fileName = [NSString stringWithCString:file->c_str() encoding:[NSString defaultCStringEncoding]]; NSURL* url = [NSURL fileURLWithPath:fileName]; diff --git a/indra/newview/llfloaterbuycurrency.cpp b/indra/newview/llfloaterbuycurrency.cpp index e41f893c433..506cff31f41 100644 --- a/indra/newview/llfloaterbuycurrency.cpp +++ b/indra/newview/llfloaterbuycurrency.cpp @@ -316,16 +316,23 @@ void LLFloaterBuyCurrency::handleBuyCurrency(bool has_piof, bool has_target, con if (has_piof) { LLFloaterBuyCurrencyUI* ui = LLFloaterReg::showTypedInstance("buy_currency"); - if (has_target) + if (ui) { - ui->target(name, price); + if (has_target) + { + ui->target(name, price); + } + else + { + ui->noTarget(); + } + ui->updateUI(); + ui->collapsePanels(!has_target); } else { - ui->noTarget(); + LL_WARNS() << "Cannot instantiate buy_currency floater" << LL_ENDL; } - ui->updateUI(); - ui->collapsePanels(!has_target); } else { diff --git a/indra/newview/llfloatergestureautocompletepicker.cpp b/indra/newview/llfloatergestureautocompletepicker.cpp new file mode 100644 index 00000000000..14d2065b5b4 --- /dev/null +++ b/indra/newview/llfloatergestureautocompletepicker.cpp @@ -0,0 +1,165 @@ +/** + * @file llfloatergestureautocompletepicker.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * 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 + * 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 "llviewerprecompiledheaders.h" + +#include "llfloatergestureautocompletepicker.h" + +#include "llgestureautocompletehelper.h" +#include "llscrolllistctrl.h" +#include "llscrolllistitem.h" + +LLFloaterGestureAutocompletePicker::LLFloaterGestureAutocompletePicker(const LLSD& key) +: LLFloater(key), mGestureList(NULL) +{ + setFocusStealsFrontmost(false); + setBackgroundVisible(false); + setAutoFocus(false); +} + +bool LLFloaterGestureAutocompletePicker::postBuild() +{ + mGestureList = getChild("gesture_list"); + mGestureList->setCommitOnKeyboardMovement(false); + mGestureList->setCommitCallback(boost::bind(&LLFloaterGestureAutocompletePicker::commitSelected, this)); + + return LLFloater::postBuild(); +} + +void LLFloaterGestureAutocompletePicker::onOpen(const LLSD& key) +{ + LLGestureAutocompleteHelper& helper = LLGestureAutocompleteHelper::instance(); + mGestureList->clearRows(); + + const std::vector& rows = helper.rows(); + + for (const auto& row : rows) + { + LLSD element; + element["value"] = row.value; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = row.trigger; + element["columns"][1]["column"] = "name"; + element["columns"][1]["value"] = row.name; + mGestureList->addElement(element); + } + + if (rows.empty() && !helper.emptyText().empty()) + { + LLSD element; + element["enabled"] = false; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = helper.emptyText(); + element["columns"][1]["column"] = "name"; + element["columns"][1]["value"] = LLStringUtil::null; + mGestureList->addElement(element); + } + + if (helper.total() > rows.size()) + { + LLSD element; + element["enabled"] = false; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = LLStringUtil::null; + element["columns"][1]["column"] = "name"; + + LLStringUtil::format_map_t args; + args["[COUNT]"] = llformat("%d", (S32)rows.size()); + args["[TOTAL]"] = llformat("%d", (S32)helper.total()); + element["columns"][1]["value"] = getString("showing_count", args); + + mGestureList->addElement(element); + } + + mGestureList->selectFirstItem(); + gFloaterView->adjustToFitScreen(this, false); +} + +bool LLFloaterGestureAutocompletePicker::handleKey(KEY key, MASK mask, bool called_from_parent) +{ + if (mask == MASK_NONE) + { + switch (key) + { + case KEY_UP: + mGestureList->selectPrevItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_DOWN: + mGestureList->selectNextItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_RETURN: + case KEY_TAB: + commitSelected(); + return true; + case KEY_ESCAPE: + LLGestureAutocompleteHelper::instance().hideHelper(); + return true; + case KEY_LEFT: + case KEY_RIGHT: + return true; + default: + break; + } + } + + return LLFloater::handleKey(key, mask, called_from_parent); +} + +void LLFloaterGestureAutocompletePicker::onClose(bool app_quitting) +{ + if (!app_quitting) + { + LLGestureAutocompleteHelper::instance().hideHelper(); + } +} + +void LLFloaterGestureAutocompletePicker::goneFromFront() +{ + LLGestureAutocompleteHelper::instance().hideHelper(); +} + +bool LLFloaterGestureAutocompletePicker::commitSelected() +{ + LLScrollListItem* item = mGestureList->getFirstSelected(); + + if (!item || !item->getEnabled()) + { + return false; + } + + const std::string value = mGestureList->getSelectedValue().asString(); + + if (value.empty()) + { + return false; + } + + setValue(value); + onCommit(); + + return true; +} diff --git a/indra/newview/llfloaterhowto.h b/indra/newview/llfloatergestureautocompletepicker.h similarity index 58% rename from indra/newview/llfloaterhowto.h rename to indra/newview/llfloatergestureautocompletepicker.h index 9d7793817a6..71f754138d0 100644 --- a/indra/newview/llfloaterhowto.h +++ b/indra/newview/llfloatergestureautocompletepicker.h @@ -1,10 +1,9 @@ /** - * @file llfloaterhowto.h - * @brief A variant of web floater meant to open guidebook + * @file llfloatergestureautocompletepicker.h * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2021, 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 @@ -24,35 +23,25 @@ * $/LicenseInfo$ */ -#ifndef LL_LLFLOATERHOWTO_H -#define LL_LLFLOATERHOWTO_H +#pragma once -#include "llfloaterwebcontent.h" +#include "llfloater.h" -class LLMediaCtrl; +class LLScrollListCtrl; - -class LLFloaterHowTo : - public LLFloaterWebContent +class LLFloaterGestureAutocompletePicker : public LLFloater { public: - LOG_CLASS(LLFloaterHowTo); - - typedef LLFloaterWebContent::Params Params; - - LLFloaterHowTo(const Params& key); + LLFloaterGestureAutocompletePicker(const LLSD& key); + bool postBuild() override; void onOpen(const LLSD& key) override; - - bool handleKeyHere(KEY key, MASK mask) override; - - static LLFloaterHowTo* getInstance(); - - bool matchesKey(const LLSD& key) override { return true; /*single instance*/ }; + bool handleKey(KEY key, MASK mask, bool called_from_parent) override; + void onClose(bool app_quitting) override; + void goneFromFront() override; private: - bool postBuild() override; -}; - -#endif // LL_LLFLOATERHOWTO_H + bool commitSelected(); + LLScrollListCtrl* mGestureList; +}; diff --git a/indra/newview/llfloaterhowto.cpp b/indra/newview/llfloaterhowto.cpp deleted file mode 100644 index 6a9f113d533..00000000000 --- a/indra/newview/llfloaterhowto.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file llfloaterhowto.cpp - * @brief A variant of web floater meant to open guidebook - * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2021, 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 "llviewerprecompiledheaders.h" - -#include "llfloaterhowto.h" - -#include "llfloaterreg.h" -#include "llviewercontrol.h" -#include "llweb.h" - - -constexpr S32 STACK_WIDTH = 300; -constexpr S32 STACK_HEIGHT = 505; // content will be 500 - -LLFloaterHowTo::LLFloaterHowTo(const Params& key) : - LLFloaterWebContent(key) -{ - mShowPageTitle = false; -} - -bool LLFloaterHowTo::postBuild() -{ - LLFloaterWebContent::postBuild(); - - return true; -} - -void LLFloaterHowTo::onOpen(const LLSD& key) -{ - LLFloaterWebContent::Params p(key); - if (!p.url.isProvided() || p.url.getValue().empty()) - { - std::string url = gSavedSettings.getString("GuidebookURL"); - p.url = LLWeb::expandURLSubstitutions(url, LLSD()); - } - p.show_chrome = false; - - LLFloaterWebContent::onOpen(p); - - if (p.preferred_media_size().isEmpty()) - { - // Elements from LLFloaterWebContent did not pick up restored size (save_rect) of LLFloaterHowTo - // set the stack size and position (alternative to preferred_media_size) - LLLayoutStack *stack = getChild("stack1"); - LLRect stack_rect = stack->getRect(); - stack->reshape(STACK_WIDTH, STACK_HEIGHT); - stack->setOrigin(stack_rect.mLeft, stack_rect.mTop - STACK_HEIGHT); - stack->updateLayout(); - } -} - -LLFloaterHowTo* LLFloaterHowTo::getInstance() -{ - return LLFloaterReg::getTypedInstance("guidebook"); -} - -bool LLFloaterHowTo::handleKeyHere(KEY key, MASK mask) -{ - bool handled = false; - - if (KEY_F1 == key ) - { - closeFloater(); - handled = true; - } - - return handled; -} diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index ef4252eba62..c2a90084207 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -158,15 +158,23 @@ void LLFloaterIMContainer::sessionIDUpdated(const LLUUID& old_session_id, const // Note however that the LLFloaterIMSession has its session id updated through a call to sessionInitReplyReceived() // and do not need to be deleted and recreated (trying this creates loads of problems). We do need however to suppress // its related mSessions record as it's indexed with the wrong id. - // Grabbing the updated LLFloaterIMSession and readding it in mSessions will eventually be done by addConversationListItem(). mSessions.erase(old_session_id); - // Delete the model and participants related to the old session - bool change_focus = removeConversationListItem(old_session_id); + // Remove the old conversation widget without changing focus - we'll immediately re-add with + // the new id and select it, avoiding an unnecessary focus switch to an adjacent conversation. + bool was_selected = removeConversationListItem(old_session_id, false); // Create a new conversation with the new id - addConversationListItem(new_session_id, change_focus); + addConversationListItem(new_session_id, was_selected); LLFloaterIMSessionTab::addToHost(new_session_id); + + // addToHost is a no-op for already-hosted floaters, so mSessions won't be + // updated by addFloater. Re-register manually so message flash works. + LLFloaterIMSessionTab* conversp = LLFloaterIMSessionTab::findConversation(new_session_id); + if (conversp && mSessions.find(new_session_id) == mSessions.end()) + { + mSessions[new_session_id] = conversp; + } } diff --git a/indra/newview/llfloaterimnearbychat.cpp b/indra/newview/llfloaterimnearbychat.cpp index f0d696361ae..8fa183b1806 100644 --- a/indra/newview/llfloaterimnearbychat.cpp +++ b/indra/newview/llfloaterimnearbychat.cpp @@ -55,6 +55,7 @@ #include "llfloaterimnearbychatlistener.h" #include "llagent.h" // gAgent #include "llgesturemgr.h" +#include "llgestureautocompletehelper.h" #include "llmultigesture.h" #include "llkeyboard.h" #include "llanimationstates.h" @@ -70,6 +71,8 @@ #include "llautoreplace.h" #include "lluiusage.h" +#include + S32 LLFloaterIMNearbyChat::sLastSpecialChatChannel = 0; static LLFloaterIMNearbyChatListener sChatListener; @@ -77,6 +80,72 @@ static LLFloaterIMNearbyChatListener sChatListener; constexpr S32 EXPANDED_HEIGHT = 266; constexpr S32 COLLAPSED_HEIGHT = 60; constexpr S32 EXPANDED_MIN_HEIGHT = 150; +constexpr size_t MAX_GESTURE_AUTOCOMPLETE_ROWS = 50; + +namespace +{ +bool buildGestureAutocompleteRows( + const std::string& prefix, + std::vector& rows, + size_t& total, + std::string& empty_text) +{ + rows.clear(); + total = 0; + empty_text.clear(); + + // Wait for at least one character after the slash before offering matches. + if (prefix.size() < 2 || prefix[0] != '/' || prefix.find_first_of(" \t") != std::string::npos) + { + return false; + } + + std::string lower_prefix = prefix; + LLStringUtil::toLower(lower_prefix); + + std::map unique; + const LLGestureMgr::item_map_t& active = LLGestureMgr::instance().getActiveGestures(); + + for (const auto& entry : active) + { + LLMultiGesture* gesture = entry.second; + + if (!gesture || gesture->getTrigger().empty() || gesture->getTrigger()[0] != '/') + { + continue; + } + + std::string lower_trigger = gesture->getTrigger(); + LLStringUtil::toLower(lower_trigger); + + if (lower_trigger.compare(0, lower_prefix.size(), lower_prefix) != 0) + { + continue; + } + + unique.emplace( + gesture->getTrigger(), + gesture->mName); + } + + for (const auto& gesture : unique) + { + ++total; + + if (rows.size() < MAX_GESTURE_AUTOCOMPLETE_ROWS) + { + rows.push_back({ gesture.first, gesture.first, gesture.second }); + } + } + + if (rows.empty()) + { + empty_text = "No matching gestures"; + } + + return total > 0; +} +} // legacy callback glue void send_chat_from_viewer(const std::string& utf8_out_text, EChatType type, S32 channel); @@ -501,8 +570,34 @@ void LLFloaterIMNearbyChat::onChatBoxKeystroke() KEY key = gKeyboard->currentKey(); + static LLCachedControl autocomplete_gestures(gSavedSettings, "ChatAutocompleteGestures", true); + + if (autocomplete_gestures) + { + std::vector rows; + size_t total = 0; + std::string empty_text; + const std::string utf8_trigger = wstring_to_utf8str(raw_text); + + if (buildGestureAutocompleteRows(utf8_trigger, rows, total, empty_text)) + { + LLGestureAutocompleteHelper::instance().showHelper( + mInputEditor, + rows, + total, + empty_text, + [this](std::string trigger) + { + mInputEditor->setText(trigger + " "); + mInputEditor->endOfDoc(); + }); + return; + } + + LLGestureAutocompleteHelper::instance().hideHelper(mInputEditor); + } // Ignore "special" keys, like backspace, arrows, etc. - if (gSavedSettings.getBOOL("ChatAutocompleteGestures") + if (autocomplete_gestures && length > 1 && raw_text[0] == '/' && key < KEY_SPECIAL) @@ -589,6 +684,9 @@ void LLFloaterIMNearbyChat::sendChat( EChatType type ) LLWString text = mInputEditor->getConvertedText(); LLWStringUtil::trim(text); LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines. + + LLGestureAutocompleteHelper::instance().hideHelper(); + if (!text.empty()) { // Check if this is destined for another channel diff --git a/indra/newview/llfloaterimsessiontab.h b/indra/newview/llfloaterimsessiontab.h index b27ac1b8f9e..3ca1187ec96 100644 --- a/indra/newview/llfloaterimsessiontab.h +++ b/indra/newview/llfloaterimsessiontab.h @@ -118,6 +118,8 @@ class LLFloaterIMSessionTab virtual void sessionVoiceOrIMStarted(const LLUUID& session_id) override {}; // Stub virtual void sessionIDUpdated(const LLUUID& old_session_id, const LLUUID& new_session_id) override {}; // Stub + bool isP2PSessionType() { return mIsP2PChat; } + protected: // callback for click on any items of the visual states menu diff --git a/indra/newview/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp index 163edf04269..77a98943aca 100644 --- a/indra/newview/llfloaterinspect.cpp +++ b/indra/newview/llfloaterinspect.cpp @@ -78,18 +78,21 @@ LLFloaterInspect::~LLFloaterInspect(void) { mCreatorNameCacheConnection.disconnect(); } - if(!LLFloaterReg::instanceVisible("build")) + if (!LLApp::isExiting()) { - if(LLToolMgr::getInstance()->getBaseTool() == LLToolCompInspect::getInstance()) + if (!LLFloaterReg::instanceVisible("build")) { - LLToolMgr::getInstance()->clearTransientTool(); + if (LLToolMgr::getInstance()->getBaseTool() == LLToolCompInspect::getInstance()) + { + LLToolMgr::getInstance()->clearTransientTool(); + } + // Switch back to basic toolset + LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); + } + else + { + LLFloaterReg::showInstance("build", LLSD(), true); } - // Switch back to basic toolset - LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); - } - else - { - LLFloaterReg::showInstance("build", LLSD(), true); } } diff --git a/indra/newview/llfloaterobjectweights.cpp b/indra/newview/llfloaterobjectweights.cpp index bd8530abd7e..cf89518c03d 100644 --- a/indra/newview/llfloaterobjectweights.cpp +++ b/indra/newview/llfloaterobjectweights.cpp @@ -33,8 +33,10 @@ #include "lltextbox.h" #include "llagent.h" +#include "llcallbacklist.h" #include "llviewerparcelmgr.h" #include "llviewerregion.h" +#include "llmeshrepository.h" static const std::string lod_strings[4] = { @@ -74,6 +76,14 @@ bool LLCrossParcelFunctor::apply(LLViewerObject* obj) LLFloaterObjectWeights::LLFloaterObjectWeights(const LLSD& key) : LLFloater(key), + mWeightsDirty(false), + mSelectionDirty(true), + mSelectionMeshDirty(true), + mSelectionLastLOD(-1), + mSelectionLastTris(0), + mSelectionLastArea(0), + mLastActiveLODRequests(0), + mSelectionRefreshTime(0.), mSelectedObjects(NULL), mSelectedPrims(NULL), mSelectedDownloadWeight(NULL), @@ -88,6 +98,13 @@ LLFloaterObjectWeights::LLFloaterObjectWeights(const LLSD& key) mTrianglesShown(nullptr), mPixelArea(nullptr) { + mSelectionConnection = LLSelectMgr::getInstance()->mUpdateSignal.connect( + [this]() + { + mSelectionDirty = true; + mSelectionMeshDirty = true; // assume that mesh data will arrive with a delay. + } + ); } LLFloaterObjectWeights::~LLFloaterObjectWeights() @@ -114,6 +131,11 @@ bool LLFloaterObjectWeights::postBuild() mTrianglesShown = getChild("triangles_shown"); mPixelArea = getChild("pixel_area"); + mHighLodTris = getChild("high_lod_tris"); + mMediumLodTris = getChild("medium_lod_tris"); + mLowLodTris = getChild("low_lod_tris"); + mLowestLodTris = getChild("lowest_lod_tris"); + return true; } @@ -122,6 +144,14 @@ void LLFloaterObjectWeights::onOpen(const LLSD& key) { refresh(); updateLandImpacts(LLViewerParcelMgr::getInstance()->getFloatingParcelSelection()->getParcel()); + + onIdleRefresh(this); + gIdleCallbacks.addFunction(onIdleRefresh, this); +} + +void LLFloaterObjectWeights::onClose(bool app_quitting) +{ + gIdleCallbacks.deleteFunction(onIdleRefresh, this); } // virtual @@ -131,8 +161,9 @@ void LLFloaterObjectWeights::onWeightsUpdate(const SelectionCost& selection_cost mSelectedPhysicsWeight->setText(llformat("%.1f", selection_cost.mPhysicsCost)); mSelectedServerWeight->setText(llformat("%.1f", selection_cost.mSimulationCost)); - S32 render_cost = LLSelectMgr::getInstance()->getSelection()->getSelectedObjectRenderCost(); - mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + // onWeightsUpdate comes from a coroutine. + // Postpone LLSelectMgr operations as getSelectedObjectRenderCost is not coroutine-safe + mWeightsDirty = true; toggleWeightsLoadingIndicators(false); } @@ -152,20 +183,13 @@ void LLFloaterObjectWeights::setErrorStatus(S32 status, const std::string& reaso void LLFloaterObjectWeights::draw() { - // Normally it's a bad idea to set text and visibility inside draw - // since it can cause rect updates go to different, already drawn elements, - // but floater is very simple and these elements are supposed to be isolated + // This logic might be a bit too expensive for draw(), + // but this floater needs to react fast, even if it's + // detrimental to performance. Having callbacks like + // 'tris changed' in every selected object might be + // more impactful on performance. LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); - if (selection->isEmpty()) - { - const std::string text = getString("nothing_selected"); - mLodLevel->setText(text); - mTrianglesShown->setText(text); - mPixelArea->setText(text); - - toggleRenderLoadingIndicators(false); - } - else + if (!selection->isEmpty() && !mSelectionDirty) { S32 object_lod = -1; bool multiple_lods = false; @@ -192,27 +216,71 @@ void LLFloaterObjectWeights::draw() } } - if (multiple_lods) + if (mSelectionLastTris != total_tris) { - mLodLevel->setText(getString("multiple_lods")); - toggleRenderLoadingIndicators(false); + mSelectionDirty = true; } - else if (object_lod < 0) + else if (mSelectionLastArea != pixel_area) { - // nodes are waiting for data - toggleRenderLoadingIndicators(true); + mSelectionDirty = true; } - else + else if (multiple_lods) { - mLodLevel->setText(getString(lod_strings[object_lod])); - toggleRenderLoadingIndicators(false); + mSelectionDirty |= mSelectionLastLOD != -1; + } + else if (mSelectionLastLOD != object_lod) + { + mSelectionDirty = true; } - mTrianglesShown->setText(llformat("%d", total_tris)); - mPixelArea->setText(llformat("%ld", (S64)pixel_area)); // value capped at 10M } LLFloater::draw(); } +void LLFloaterObjectWeights::onIdleRefresh(void* user_data) +{ + LLFloaterObjectWeights* self = (LLFloaterObjectWeights*)user_data; + + F64 current_time = LLTimer::getTotalSeconds(); + S32 current_lod_requests = LLMeshRepoThread::sActiveLODRequests.load(); + // Can't look for a specific mesh (no callback mechanics and we + // need multiple ones), so just update periodically if something loads + self->mSelectionMeshDirty |= (self->mLastActiveLODRequests > current_lod_requests); + bool throttle_elapsed = (current_time - self->mSelectionRefreshTime) >= 2.0; + + if (self->mLastActiveLODRequests < current_lod_requests) + { + // we are interested in finished downloads, + // so don't refresh if more requests are getting added. + self->mLastActiveLODRequests = current_lod_requests; + } + + if (self->mSelectionDirty) + { + // Always refresh on selection changes + self->refreshDataFromSelection(); + self->mSelectionDirty = false; + self->mSelectionRefreshTime = current_time; + // refreshDataFromSelection could have indirectly initiated more requests + self->mLastActiveLODRequests = LLMeshRepoThread::sActiveLODRequests.load(); + } + else if (self->mSelectionMeshDirty && throttle_elapsed) + { + // LOD requests count changed or mesh needs lod data reload + self->refreshDataFromSelection(); + self->mSelectionRefreshTime = current_time; + self->mSelectionMeshDirty = false; + // refreshDataFromSelection could have indirectly initiated more requests + self->mLastActiveLODRequests = LLMeshRepoThread::sActiveLODRequests.load(); + } + else if (self->mWeightsDirty) // also done in refreshDataFromSelection + { + LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); + S32 render_cost = selection->getSelectedObjectRenderCost(); + self->mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + self->mWeightsDirty = false; + } +} + void LLFloaterObjectWeights::updateLandImpacts(const LLParcel* parcel) { if (!parcel || LLSelectMgr::getInstance()->getSelection()->isEmpty()) @@ -240,6 +308,101 @@ void LLFloaterObjectWeights::updateLandImpacts(const LLParcel* parcel) } } +void LLFloaterObjectWeights::refreshDataFromSelection() +{ + LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); + if (selection->isEmpty()) + { + const std::string text = getString("nothing_selected"); + mLodLevel->setText(text); + mTrianglesShown->setText(text); + mPixelArea->setText(text); + + mHighLodTris->setText(text); + mMediumLodTris->setText(text); + mLowLodTris->setText(text); + mLowestLodTris->setText(text); + + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + else + { + S32 object_lod = -1; + bool multiple_lods = false; + S32 total_tris = 0; + F32 pixel_area = 0; + S32 high_tris = 0; + S32 medium_tris = 0; + S32 low_tris = 0; + S32 lowest_tris = 0; + for (LLObjectSelection::valid_root_iterator iter = selection->valid_root_begin(); + iter != selection->valid_root_end(); ++iter) + { + LLViewerObject* object = (*iter)->getObject(); + S32 lod = object->getLOD(); + if (object_lod < 0) + { + object_lod = lod; + } + else if (object_lod != lod) + { + multiple_lods = true; + } + + if (object->isRootEdit()) + { + total_tris += object->recursiveGetTriangleCount(); + pixel_area += object->getPixelArea(); + object->recursiveGetLODTriangleCount(high_tris, medium_tris, low_tris, lowest_tris); + } + } + + mSelectionLastTris = total_tris; + mSelectionLastArea = pixel_area; + if (multiple_lods) + { + mSelectionLastLOD = -1; + } + else + { + mSelectionLastLOD = object_lod; + } + + if (multiple_lods) + { + mLodLevel->setText(getString("multiple_lods")); + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + else if (object_lod < 0) + { + // nodes are waiting for data + toggleRenderLoadingIndicators(true); + toggleLODLoadingIndicators(true); + } + else + { + mLodLevel->setText(getString(lod_strings[object_lod])); + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + mTrianglesShown->setText(llformat("%d", total_tris)); + mPixelArea->setText(llformat("%ld", (S64)pixel_area)); // value capped at 10M + mHighLodTris->setText(llformat("%d", high_tris)); + mMediumLodTris->setText(llformat("%d", medium_tris)); + mLowLodTris->setText(llformat("%d", low_tris)); + mLowestLodTris->setText(llformat("%d", lowest_tris)); + } + + if (mWeightsDirty) + { + S32 render_cost = selection->getSelectedObjectRenderCost(); + mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + mWeightsDirty = false; + } +} + void LLFloaterObjectWeights::refresh() { LLSelectMgr* sel_mgr = LLSelectMgr::getInstance(); @@ -341,6 +504,19 @@ void LLFloaterObjectWeights::toggleRenderLoadingIndicators(bool visible) mPixelArea->setVisible(!visible); } +void LLFloaterObjectWeights::toggleLODLoadingIndicators(bool visible) +{ + childSetVisible("high_lod_tris_loading_indicator", visible); + childSetVisible("medium_lod_tris_loading_indicator", visible); + childSetVisible("low_lod_tris_loading_indicator", visible); + childSetVisible("lowest_lod_tris_loading_indicator", visible); + + mHighLodTris->setVisible(!visible); + mMediumLodTris->setVisible(!visible); + mLowLodTris->setVisible(!visible); + mLowestLodTris->setVisible(!visible); +} + void LLFloaterObjectWeights::updateIfNothingSelected() { const std::string text = getString("nothing_selected"); diff --git a/indra/newview/llfloaterobjectweights.h b/indra/newview/llfloaterobjectweights.h index bda625564ba..dac87d2a106 100644 --- a/indra/newview/llfloaterobjectweights.h +++ b/indra/newview/llfloaterobjectweights.h @@ -61,13 +61,17 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver bool postBuild() override; void onOpen(const LLSD& key) override; + void onClose(bool app_quitting) override; void onWeightsUpdate(const SelectionCost& selection_cost) override; void setErrorStatus(S32 status, const std::string& reason) override; void draw() override; + static void onIdleRefresh(void* user_data); + void updateLandImpacts(const LLParcel* parcel); + void refreshDataFromSelection(); void refresh() override; private: @@ -76,9 +80,19 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver void toggleWeightsLoadingIndicators(bool visible); void toggleLandImpactsLoadingIndicators(bool visible); void toggleRenderLoadingIndicators(bool visible); + void toggleLODLoadingIndicators(bool visible); void updateIfNothingSelected(); + bool mWeightsDirty; + bool mSelectionDirty; + bool mSelectionMeshDirty; + F64 mSelectionRefreshTime; + S32 mSelectionLastLOD; + S32 mSelectionLastTris; + F32 mSelectionLastArea; + S32 mLastActiveLODRequests; + LLTextBox *mSelectedObjects; LLTextBox *mSelectedPrims; @@ -95,6 +109,13 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver LLTextBox *mLodLevel; LLTextBox *mTrianglesShown; LLTextBox *mPixelArea; + + LLTextBox *mHighLodTris; + LLTextBox *mMediumLodTris; + LLTextBox *mLowLodTris; + LLTextBox *mLowestLodTris; + + boost::signals2::scoped_connection mSelectionConnection; }; #endif //LL_LLFLOATEROBJECTWEIGHTS_H diff --git a/indra/newview/llfloaterreporter.cpp b/indra/newview/llfloaterreporter.cpp index 7e7eb91636b..6322dcd3c97 100644 --- a/indra/newview/llfloaterreporter.cpp +++ b/indra/newview/llfloaterreporter.cpp @@ -660,9 +660,9 @@ void LLFloaterReporter::showFromAvatar(const LLUUID& avatar_id, const std::strin } // static -void LLFloaterReporter::showFromChat(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description) +void LLFloaterReporter::showFromChatObj(const LLUUID& object_id, const std::string& time, const std::string& description) { - show(avatar_id, avatar_name); + show(object_id, LLStringUtil::null, LLUUID::null); LLStringUtil::format_map_t args; args["[MSG_TIME]"] = time; @@ -671,8 +671,25 @@ void LLFloaterReporter::showFromChat(const LLUUID& avatar_id, const std::string& LLFloaterReporter *self = LLFloaterReg::findTypedInstance("reporter"); if (self) { - std::string description = self->getString("chat_report_format", args); - self->getChild("details_edit")->setValue(description); + std::string description_frmt = self->getString("chat_report_format", args); + self->getChild("details_edit")->setValue(description_frmt); + } +} + +// static +void LLFloaterReporter::showFromChatAv(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description) +{ + show(avatar_id, avatar_name); + + LLStringUtil::format_map_t args; + args["[MSG_TIME]"] = time; + args["[MSG_DESCRIPTION]"] = description; + + LLFloaterReporter* self = LLFloaterReg::findTypedInstance("reporter"); + if (self) + { + std::string description_frmt = self->getString("chat_report_format", args); + self->getChild("details_edit")->setValue(description_frmt); } } diff --git a/indra/newview/llfloaterreporter.h b/indra/newview/llfloaterreporter.h index 31b05235a65..446e7d63b61 100644 --- a/indra/newview/llfloaterreporter.h +++ b/indra/newview/llfloaterreporter.h @@ -93,7 +93,8 @@ class LLFloaterReporter static void showFromObject(const LLUUID& object_id, const LLUUID& experience_id = LLUUID::null); static void showFromAvatar(const LLUUID& avatar_id, const std::string avatar_name); - static void showFromChat(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description); + static void showFromChatObj(const LLUUID& object_id, const std::string& time, const std::string& description); + static void showFromChatAv(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description); static void showFromExperience(const LLUUID& experience_id); static void onClickSend (void *userdata); diff --git a/indra/newview/llhudnametag.cpp b/indra/newview/llhudnametag.cpp index 4327d281e58..5d20e722de6 100644 --- a/indra/newview/llhudnametag.cpp +++ b/indra/newview/llhudnametag.cpp @@ -41,9 +41,9 @@ #include "llhudrender.h" #include "llui.h" #include "llviewercamera.h" +#include "llviewerregion.h" #include "llviewertexturelist.h" #include "llviewerobject.h" -#include "llvovolume.h" #include "llviewerwindow.h" #include "llstatusbar.h" #include "llmenugl.h" @@ -291,7 +291,22 @@ void LLHUDNameTag::renderText() + (x_pixel_vec * screen_offset.mV[VX]) + (y_pixel_vec * screen_offset.mV[VY]); - LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE); + // Check if an underwater name tag should be rendered over the water (while camera is above the water) + bool render_over_water = false; + static LLCachedControl nametag_over_water(gSavedSettings, "NametagOverWater", true); + if (nametag_over_water && + mSourceObject && + mSourceObject->getRegion() && + LLPipeline::sRenderTransparentWater && + !LLViewerCamera::getInstance()->cameraUnderWater()) + { + if (mSourceObject->getPositionAgent().mV[VZ] < mSourceObject->getRegion()->getWaterHeight()) + { + render_over_water = true; + } + } + LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE, render_over_water ? GL_ALWAYS : GL_LEQUAL); + LLRect screen_rect; screen_rect.setCenterAndSize(0, static_cast(lltrunc(-mHeight / 2 + mOffsetY)), static_cast(lltrunc(mWidth)), static_cast(lltrunc(mHeight))); mRoundedRectImgp->draw3D(render_position, x_pixel_vec, y_pixel_vec, screen_rect, bg_color); diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp index 252eb32e7ca..f8cc6885c4e 100644 --- a/indra/newview/llimview.cpp +++ b/indra/newview/llimview.cpp @@ -3245,8 +3245,9 @@ void LLIMMgr::addMessage( return; } - // Fetch group chat history, enabled by default. - if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory")) + // Fetch group chat or ad-hoc history, enabled by default. + static LLCachedControl fetch_chat_history(gSavedPerAccountSettings, "FetchGroupChatHistory", true); + if (fetch_chat_history && !session->isP2PSessionType()) { std::string chat_url = gAgent.getRegionCapability("ChatSessionRequest"); if (!chat_url.empty()) @@ -4089,8 +4090,9 @@ class LLViewerChatterBoxSessionStartReply : public LLHTTPNode { im_floater->processSessionUpdate(body["session_info"]); - // Send request for chat history, if enabled. - if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory")) + // Send request for chat history, if enabled. Skip for peer-to-peer IMs. + static LLCachedControl fetch_chat_history(gSavedPerAccountSettings, "FetchGroupChatHistory", true); + if (fetch_chat_history && !im_floater->isP2PSessionType()) { std::string url = gAgent.getRegionCapability("ChatSessionRequest"); if (!url.empty()) diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index c8ea14a11e9..8e4e70474f0 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -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 @@ -249,11 +249,27 @@ const std::string& LLInvFVBridge::getName() const const std::string& LLInvFVBridge::getDisplayName() const { - if(mDisplayName.empty()) + if(mSearchableName.empty()) { - buildDisplayName(); + // first request of display name, build search string and cache it for later use + buildSearchableName(); } - return mDisplayName; + + LLInventoryModel* model = getInventoryModel(); + if (model) + { + LLViewerInventoryCategory* cat = model->getCategory(mUUID); + if (cat) + { + return cat->getDisplayName(); + } + LLViewerInventoryItem* item = model->getItem(mUUID); + if (item) + { + return item->getName(); + } + } + return LLStringUtil::null; } std::string LLInvFVBridge::getSearchableDescription() const @@ -2035,22 +2051,22 @@ PermissionMask LLItemBridge::getPermissionMask() const } // virtual -void LLItemBridge::buildDisplayName() const +void LLItemBridge::buildSearchableName() const { - if (getItem()) + LLViewerInventoryItem* item = getItem(); + if (item) { - mDisplayName.assign(getItem()->getName()); + // for items, display name matches item name + mSearchableName.assign(item->getName()); } else { - mDisplayName.assign(LLStringUtil::null); + mSearchableName.assign(LLStringUtil::null); } - - mSearchableName.assign(mDisplayName); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); - // Name set, so trigger a sort + // Searchable and name set, so trigger a sort LLInventorySort sorter = static_cast(mRootViewModel).getSorter(); if (mParent && !sorter.isByDate()) { @@ -2371,39 +2387,17 @@ void LLFolderBridge::selectItem() } } -void LLFolderBridge::buildDisplayName() const +void LLFolderBridge::buildSearchableName() const { - LLFolderType::EType preferred_type = getPreferredType(); - - // *TODO: to be removed when database supports multi language. This is a - // temporary attempt to display the inventory folder in the user locale. - // mantipov: *NOTE: be sure this code is synchronized with LLFriendCardsManager::findChildFolderUUID - // it uses the same way to find localized string - - // HACK: EXT - 6028 ([HARD CODED]? Inventory > Library > "Accessories" folder) - // Translation of Accessories folder in Library inventory folder - bool accessories = false; - if(getName() == "Accessories") + LLViewerInventoryCategory* cat = gInventory.getCategory(getUUID()); + if (cat) { - //To ensure that Accessories folder is in Library we have to check its parent folder. - //Due to parent LLFolderViewFloder is not set to this item yet we have to check its parent via Inventory Model - LLInventoryCategory* cat = gInventory.getCategory(getUUID()); - if(cat) - { - const LLUUID& parent_folder_id = cat->getParentUUID(); - accessories = (parent_folder_id == gInventory.getLibraryRootFolderID()); - } + mSearchableName.assign(cat->getDisplayName()); } - - //"Accessories" inventory category has folder type FT_NONE. So, this folder - //can not be detected as protected with LLFolderType::lookupIsProtectedType - mDisplayName.assign(getName()); - if (accessories || LLFolderType::lookupIsProtectedType(preferred_type)) + else { - LLTrans::findString(mDisplayName, std::string("InvFolder ") + getName(), LLSD()); + mSearchableName.assign(LLStringUtil::null); } - - mSearchableName.assign(mDisplayName); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); @@ -2417,6 +2411,8 @@ void LLFolderBridge::buildDisplayName() const std::string LLFolderBridge::getLabelSuffix() const { + // Folders, unlike items, have context dependent suffixes + // that may change as the folder is loaded static LLCachedControl xui_debug(gSavedSettings, "DebugShowXUINames", 0); if (mIsLoading && mTimeSinceRequestStart.getElapsedTimeF32() >= FOLDER_LOADING_MESSAGE_DELAY) @@ -6576,18 +6572,29 @@ void LLCallingCardBridge::refreshFolderViewItem() void LLCallingCardBridge::checkSearchBySuffixChanges() { - if (!mDisplayName.empty()) + if (!mSearchableName.empty()) { - // changes in mDisplayName are processed by rename function and here it will be always same + LLViewerInventoryItem* item = getItem(); + if (!item) + { + // checkSearchBySuffixChanges is only used by friend list + // so if item is not found, we removed the calling card or + // are no longer friends. + mSearchableName.clear(); + return; + } + + // changes in display name are processed by rename function and here it will be always same // suffixes are also of fixed length, and we are processing change of one at a time, // so it should be safe to use length (note: mSearchableName is capitalized) - auto old_length = mSearchableName.length(); - auto new_length = mDisplayName.length() + getLabelSuffix().length(); + size_t old_length = mSearchableName.length(); + const std::string& display_name = item->getName(); + size_t new_length = display_name.length() + getLabelSuffix().length(); if (old_length == new_length) { return; } - mSearchableName.assign(mDisplayName); + mSearchableName.assign(display_name); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); if (new_lengthupdateServer(false); model->updateItem(new_item); model->notifyObservers(); - buildDisplayName(); + buildSearchableName(); if (isAgentAvatarValid()) { diff --git a/indra/newview/llinventorybridge.h b/indra/newview/llinventorybridge.h index decb2c05287..68e7a7f80e6 100644 --- a/indra/newview/llinventorybridge.h +++ b/indra/newview/llinventorybridge.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 @@ -87,7 +87,7 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory virtual const LLUUID& getUUID() const { return mUUID; } virtual const LLUUID& getThumbnailUUID() const { return LLUUID::null; } virtual bool isFavorite() const { return false; } - virtual void clearDisplayName() { mDisplayName.clear(); } + virtual void clearSearchableName() { mSearchableName.clear(); } virtual void restoreItem() {} virtual void restoreToWorld() {} @@ -202,12 +202,11 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory LLInventoryType::EType mInvType; bool mIsLink; LLTimer mTimeSinceRequestStart; - mutable std::string mDisplayName; mutable std::string mSearchableName; void purgeItem(LLInventoryModel *model, const LLUUID &uuid); void removeObject(LLInventoryModel *model, const LLUUID &uuid); - virtual void buildDisplayName() const {} + virtual void buildSearchableName() const {} }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -266,7 +265,7 @@ class LLItemBridge : public LLInvFVBridge protected: bool confirmRemoveItem(const LLSD& notification, const LLSD& response); virtual bool isItemPermissive() const; - virtual void buildDisplayName() const; + virtual void buildSearchableName() const; void doActionOnCurSelectedLandmark(LLLandmarkList::loaded_callback_t cb); private: @@ -287,7 +286,7 @@ class LLFolderBridge : public LLInvFVBridge void callback_dropItemIntoFolder(const LLSD& notification, const LLSD& response, LLInventoryItem* inv_item); void callback_dropCategoryIntoFolder(const LLSD& notification, const LLSD& response, LLInventoryCategory* inv_category); - virtual void buildDisplayName() const; + virtual void buildSearchableName() const; virtual void performAction(LLInventoryModel* model, std::string action); virtual void openItem(); diff --git a/indra/newview/llinventoryobserver.h b/indra/newview/llinventoryobserver.h index 99cb9ec8118..dbafc6fcb6f 100644 --- a/indra/newview/llinventoryobserver.h +++ b/indra/newview/llinventoryobserver.h @@ -183,7 +183,7 @@ class LLInventoryAddItemByAssetObserver : public LLInventoryObserver item_ref_t mAddedItems; item_ref_t mWatchedAssets; -private: +protected: bool mIsDirty; }; diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp index 06dd8304163..301a0a0cc92 100644 --- a/indra/newview/llinventorypanel.cpp +++ b/indra/newview/llinventorypanel.cpp @@ -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 @@ -198,7 +198,6 @@ LLFolderView * LLInventoryPanel::createFolderRoot(LLUUID root_id ) p.title = getLabel(); p.rect = LLRect(0, 0, getRect().getWidth(), 0); p.parent_panel = this; - p.tool_tip = p.name; p.listener = mInvFVBridgeBuilder->createBridge( LLAssetType::AT_CATEGORY, LLAssetType::AT_CATEGORY, LLInventoryType::IT_CATEGORY, @@ -581,8 +580,9 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve LLInvFVBridge* bridge = (LLInvFVBridge*)view_item->getViewModelItem(); if(bridge) { - // Clear the display name first, so it gets properly re-built during refresh() - bridge->clearDisplayName(); + // Clear the searchable name first, so it gets + // properly re-built during refresh() + bridge->clearSearchableName(); view_item->refresh(); } @@ -1063,10 +1063,20 @@ LLFolderViewFolder * LLInventoryPanel::createFolderViewFolder(LLInvFVBridge * br { LLFolderViewFolder::Params params(mParams.folder); - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "fld"; +#endif params.root = mFolderRoot.get(); params.listener = bridge; - params.tool_tip = params.name; params.allow_drop = allow_drop; params.font_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultColor); @@ -1079,12 +1089,23 @@ LLFolderViewItem * LLInventoryPanel::createFolderViewItem(LLInvFVBridge * bridge { LLFolderViewItem::Params params(mParams.item); - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique, are large and can change, + // there is little we can use them for in release builds. + // Prefer shorter + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "itm"; +#endif params.creation_date = bridge->getCreationDate(); params.root = mFolderRoot.get(); params.listener = bridge; params.rect = LLRect (0, 0, 0, 0); - params.tool_tip = params.name; params.font_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultColor); params.font_highlight_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultHighlightColor); @@ -1676,7 +1697,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* prev_bridge = (LLFolderBridge*)prev_folder_item->getViewModelItem(); if(prev_bridge) { - prev_bridge->clearDisplayName(); + prev_bridge->clearSearchableName(); prev_bridge->setShowDescendantsCount(false); prev_folder_item->refresh(); } @@ -1685,7 +1706,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* bridge = (LLFolderBridge*)folder_item->getViewModelItem(); if(bridge) { - bridge->clearDisplayName(); + bridge->clearSearchableName(); bridge->setShowDescendantsCount(true); folder_item->refresh(); mPreviousSelectedFolder = bridge->getUUID(); @@ -1700,7 +1721,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* prev_bridge = (LLFolderBridge*)prev_folder_item->getViewModelItem(); if(prev_bridge) { - prev_bridge->clearDisplayName(); + prev_bridge->clearSearchableName(); prev_bridge->setShowDescendantsCount(false); prev_folder_item->refresh(); } @@ -1720,7 +1741,7 @@ void LLInventoryPanel::updateFolderLabel(const LLUUID& folder_id) LLFolderBridge* bridge = (LLFolderBridge*)folder_item->getViewModelItem(); if(bridge) { - bridge->clearDisplayName(); + bridge->clearSearchableName(); bridge->setShowDescendantsCount(true); folder_item->refresh(); } diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp index 0358233637e..02f013ad3c3 100644 --- a/indra/newview/lllogininstance.cpp +++ b/indra/newview/lllogininstance.cpp @@ -134,6 +134,7 @@ void LLLoginInstance::reconnect() { // Sort of like connect, only using the pre-existing // request params. + mAttemptComplete = false; std::vector uris; LLGridManager::getInstance()->getLoginURIs(uris); mLoginModule->connect(uris.front(), mRequestData); diff --git a/indra/newview/llmachineid.cpp b/indra/newview/llmachineid.cpp index 0a90cf0699e..0373b251436 100644 --- a/indra/newview/llmachineid.cpp +++ b/indra/newview/llmachineid.cpp @@ -34,7 +34,7 @@ #include #elif LL_DARWIN #include -#include +#include "llwindowmacosx_iokit.h" #endif unsigned char static_unique_id[] = {0,0,0,0,0,0}; unsigned char static_legacy_id[] = {0,0,0,0,0,0}; @@ -350,7 +350,7 @@ bool LLWMIMethods::getGenericSerialNumber(const BSTR &select, const LPCWSTR &var bool getSerialNumber(unsigned char *unique_id, size_t len) { CFStringRef serial_cf_str = NULL; - io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, + io_service_t platformExpert = IOServiceGetMatchingService(kLLIOMainPort, IOServiceMatching("IOPlatformExpertDevice")); if (platformExpert) { diff --git a/indra/newview/llmanipscale.cpp b/indra/newview/llmanipscale.cpp index 66420d1cada..87b79fd18cb 100644 --- a/indra/newview/llmanipscale.cpp +++ b/indra/newview/llmanipscale.cpp @@ -1309,6 +1309,12 @@ void LLManipScale::updateSnapGuides(const LLBBox& bbox) LLVector3 grid_scale; LLQuaternion grid_rotation; LLSelectMgr::getInstance()->getGrid(grid_origin, grid_rotation, grid_scale); + LLViewerCamera* camera = LLViewerCamera::getInstance(); + if (!camera) + { + // can be null on shutdown + return; + } bool uniform = LLManipScale::getUniform(); diff --git a/indra/newview/llmaterialeditor.cpp b/indra/newview/llmaterialeditor.cpp index 4e14f416e9b..323ea9f552f 100644 --- a/indra/newview/llmaterialeditor.cpp +++ b/indra/newview/llmaterialeditor.cpp @@ -177,6 +177,65 @@ void LLFloaterComboOptions::onCancel() closeFloater(); } +class LLMaterialEditorTaskMoveObserver : public LLInventoryAddItemByAssetObserver +{ +public: + LLMaterialEditorTaskMoveObserver( + const LLUUID& asset_id, + const LLUUID& dest_folder_id, + LLPointer cb) + : mDestFolderID(dest_folder_id), + mCallback(cb) + { + watchAsset(asset_id); + } + + virtual ~LLMaterialEditorTaskMoveObserver() + { + gInventory.removeObserver(this); + } + + virtual void changed(U32 mask) override + { + // Call base class to populate mAddedItems + LLInventoryAddItemByAssetObserver::changed(mask); + + // If base class completed (which calls done() and clears mAddedItems), + // and we set mIsDirty, that means we're finished + if (mIsDirty) + { + gInventory.removeObserver(this); + delete this; + } + } + +protected: + virtual void done() override + { + // Find the moved item + // There must be only one since we are watching only one item, + // changed wouldn't fire otherwise. But just in case. + if (mAddedItems.size() == 1) + { + LLUUID item_id = mAddedItems[0]; + + // Fire the callback + if (mCallback) + { + mCallback->fire(item_id); + } + } + + // Todo: gInventoryMoveObserver normally opens inventory + // on completion, should this do the same? + } + +private: + LLUUID mDestFolderID; + std::string mNewName; + LLPointer mCallback; +}; + class LLMaterialEditorCopiedCallback : public LLInventoryCallback { public: @@ -189,6 +248,18 @@ class LLMaterialEditorCopiedCallback : public LLInventoryCallback mHasUnsavedChanges(has_unsaved_changes) {} + LLMaterialEditorCopiedCallback( + const std::string & buffer, + const LLSD & old_key, + const std::string & new_name, + bool has_unsaved_changes) + : mBuffer(buffer), + mOldKey(old_key), + mNewName(new_name), + mHasUnsavedChanges(has_unsaved_changes) + { + } + LLMaterialEditorCopiedCallback( const LLSD &old_key, const std::string &new_name) @@ -201,7 +272,10 @@ class LLMaterialEditorCopiedCallback : public LLInventoryCallback { if (!mNewName.empty()) { - // making a copy from a notecard doesn't change name, do it now + // making a copy from a task inventory (object, notecard) + // doesn't change name, do it now + // Todo: Can calling update_inventory_item and finishSaveAs + // cause a race condition? LLViewerInventoryItem* item = gInventory.getItem(inv_item_id); if (item->getName() != mNewName) { @@ -1811,6 +1885,52 @@ void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& mNotecardInventoryID, mAuxItem.get(), gInventoryCallbacks.registerCB(cb)); + + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } + else if (mObjectUUID.notNull()) + { + // Item is in task (object) inventory - must move to agent + // inventory first. + // Note: If this is too flimsy, just disable the 'save as' + // button when in object inventory and create a server ticket, + // as we need a copy with callback variant. + LLViewerObject* object = gObjectList.findObject(mObjectUUID); + if (object) + { + LLPermissions perm(item->getPermissions()); + if (perm.allowCopyBy(gAgent.getID(), gAgent.getGroupID()) + && perm.allowTransferTo(gAgent.getID())) + { + // Create a callback for after the item is copied/moved + std::string buffer = getEncodedAsset(); + LLPointer cb = new LLMaterialEditorCopiedCallback( + buffer, + getKey(), + new_name, + mUnsavedChanges); + + // Create observer to watch for the item arriving in inventory + // The observer will fire our callback when the move completes + LLMaterialEditorTaskMoveObserver* observer = new LLMaterialEditorTaskMoveObserver( + item->getAssetUUID(), + parent_id, + cb + ); + gInventory.addObserver(observer); + + // Copies(not moves) item from object to agent inventory + object->moveInventory(parent_id, item->getUUID()); + + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } + else + { + LL_WARNS("MaterialEditor") << "Insufficient permissions to copy material from object inventory" << LL_ENDL; + } + } } else { @@ -1823,10 +1943,10 @@ void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& parent_id, new_name, cb); - } - mAssetStatus = PREVIEW_ASSET_LOADING; - setEnabled(false); + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } } else { diff --git a/indra/newview/llmediactrl.cpp b/indra/newview/llmediactrl.cpp index 4a808ba052c..679157dc1e3 100644 --- a/indra/newview/llmediactrl.cpp +++ b/indra/newview/llmediactrl.cpp @@ -62,9 +62,15 @@ #include "lllineeditor.h" #include "llfloaterwebcontent.h" #include "llwindowshade.h" +#include "lleventapi.h" +#include "llui.h" extern bool gRestoreGL; +const std::string PAGE_TEXT_EXTRACT_MARKER = "PAGE_TEXT_EXTRACT:"; + +class LLMediaCtrlListener; + static LLDefaultChildRegistry::Register r("web_browser"); LLMediaCtrl::Params::Params() @@ -100,6 +106,7 @@ LLMediaCtrl::LLMediaCtrl( const Params& p) : mUpdateScrolls( false ), mTextureWidth ( 1024 ), mTextureHeight ( 1024 ), + mLoadingState( LOADING_STATE_INITIALIZING ), mClearCache(false), mHomePageMimeType(p.initial_mime_type), mErrorPageURL(p.error_page_url), @@ -1024,6 +1031,7 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_NAVIGATE_BEGIN, url is " << self->getNavigateURI() << LL_ENDL; hideNotification(); + mLoadingState = LOADING_STATE_LOADING; }; break; @@ -1034,6 +1042,7 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) { mHidingInitialLoad = false; } + mLoadingState = LOADING_STATE_LOADED; }; break; @@ -1062,6 +1071,7 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) { navigateTo(mErrorPageURL, HTTP_CONTENT_TEXT_HTML); }; + mLoadingState = LOADING_STATE_ERROR; }; break; @@ -1106,12 +1116,14 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) case MEDIA_EVENT_PLUGIN_FAILED: { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_PLUGIN_FAILED" << LL_ENDL; + mLoadingState = LOADING_STATE_ERROR; }; break; case MEDIA_EVENT_PLUGIN_FAILED_LAUNCH: { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_PLUGIN_FAILED_LAUNCH" << LL_ENDL; + mLoadingState = LOADING_STATE_ERROR; }; break; @@ -1194,6 +1206,33 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) case MEDIA_EVENT_DEBUG_MESSAGE: { LL_INFOS("media") << self->getDebugMessageText() << LL_ENDL; + + // Handle text extraction responses + std::string debug_text = self->getDebugMessageText(); + if (debug_text.find(PAGE_TEXT_EXTRACT_MARKER) != std::string::npos) + { + if (LLPluginClassMedia* plugin = getMediaPlugin()) + { + // Disable plugin debugging if it was used just for text extraction + static LLCachedControl media_debugging(gSavedSettings, "MediaPluginDebugging", false); + plugin->enableMediaPluginDebugging(media_debugging); + } + // Extract the pump name and page text + size_t marker_pos = debug_text.find(PAGE_TEXT_EXTRACT_MARKER); + if (marker_pos != std::string::npos) + { + std::string remaining = debug_text.substr(marker_pos + PAGE_TEXT_EXTRACT_MARKER.length()); + size_t colon_pos = remaining.find(':'); + if (colon_pos != std::string::npos) + { + std::string pump_name = remaining.substr(0, colon_pos); + std::string page_text = remaining.substr(colon_pos + 1); + + // Send the response directly to the specified pump + LLEventPumps::instance().obtain(pump_name).post(LLSD().with("text", page_text)); + } + } + } }; break; }; @@ -1274,3 +1313,148 @@ bool LLMediaCtrl::wantsReturnKey() const { return true; } + +std::string LLMediaCtrl::getMediaMimeType() +{ + return mMediaSource ? mMediaSource->getMimeType() : "unknown"; +} + +std::string LLMediaCtrl::getMediaLoadingStatus() +{ + if (!mMediaSource) + { + return "error"; + } + + switch (mLoadingState) + { + case LOADING_STATE_INITIALIZING: + return "initializing"; + case LOADING_STATE_LOADING: + return "loading"; + case LOADING_STATE_LOADED: + return "loaded"; + case LOADING_STATE_ERROR: + default: + return "error"; + } +} + +std::string LLMediaCtrl::getMediaTitle() +{ + if (mMediaSource) + { + if (LLPluginClassMedia* plugin = mMediaSource->getMediaPlugin()) + { + return plugin->getMediaName(); + } + } + return "unknown"; +} + +bool LLMediaCtrl::executeJavaScript(const std::string& script) +{ + if (mMediaSource && mMediaSource->hasMedia()) + { + mMediaSource->executeJavaScript(script); + return true; + } + return false; +} + +class LLMediaCtrlListener: public LLEventAPI +{ +public: + LLMediaCtrlListener(); + +private: + void getMediaInfo(const LLSD& request); + void getMediaText(const LLSD& request); + void replyError(const LLSD& request, const std::string& error); + LLMediaCtrl* findMediaCtrl(const std::string& path); +}; + +LLMediaCtrlListener::LLMediaCtrlListener(): + LLEventAPI("LLMediaAPI", "Acces to LLMediaCtrl(web_browse widget) info") +{ + add("getMediaInfo", + "Get information about the web_browser widget specified by [\"path\"].\n" + "Returns URL, MIME type, and loading status of the widget.", + &LLMediaCtrlListener::getMediaInfo, + llsd::map("path", LLSD(), "reply", LLSD())); + + add("getMediaText", + "Get text content from the web_browser widget specified by [\"path\"].\n" + "Returns the text content of the page or a portion of it.", + &LLMediaCtrlListener::getMediaText, + llsd::map("path", LLSD(), "reply", LLSD())); +} + +LLMediaCtrl* LLMediaCtrlListener::findMediaCtrl(const std::string& path) +{ + LLView* view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); + if (!view) + { + return nullptr; + } + return dynamic_cast(view); +} + +void LLMediaCtrlListener::getMediaInfo(const LLSD& request) +{ + Response reply(LLSD(), request); + std::string path = request["path"]; + + LLMediaCtrl* media_ctrl = findMediaCtrl(path); + if (!media_ctrl) + { + reply["error"] = "Could not find web_browser widget at path: " + path; + return; + } + + reply["url"] = media_ctrl->getCurrentNavUrl(); + reply["mime_type"] = media_ctrl->getMediaMimeType(); + reply["status"] = media_ctrl->getMediaLoadingStatus(); + reply["title"] = media_ctrl->getMediaTitle(); +} + +void LLMediaCtrlListener::replyError(const LLSD& request, const std::string& error) +{ + Response reply(LLSD(), request); + reply["error"] = error; +} + +void LLMediaCtrlListener::getMediaText(const LLSD& request) +{ + std::string path = request["path"]; + + LLMediaCtrl* media_ctrl = findMediaCtrl(path); + if (!media_ctrl) + { + replyError(request, "Could not find web_browser widget at path: " + path); + return; + } + + LLPluginClassMedia* plugin = media_ctrl->getMediaPlugin(); + if (!plugin) + { + replyError(request, "Media plugin is not available for widget at path: " + path); + return; + } + + // Enable plugin debugging to capture console messages + plugin->enableMediaPluginDebugging(true); + std::string pump_name = request["reply"].asString(); + + // Execute JavaScript to extract page text, embedding pump name in the marker + const std::string text_extract_script = "console.log('" + PAGE_TEXT_EXTRACT_MARKER + pump_name + ":' + " + "(document.body ? (document.body.innerText ? document.body.innerText.substring(0, 1000).replace(/\\s+/g, ' ').trim() : " + "'No text content') : 'Document body not ready'));"; + + if (!media_ctrl->executeJavaScript(text_extract_script)) + { + replyError(request, "Failed to execute JavaScript for text extraction"); + } +} + +static LLMediaCtrlListener sMediaCtrlListener; diff --git a/indra/newview/llmediactrl.h b/indra/newview/llmediactrl.h index a644ef30719..e407bfbbcbc 100644 --- a/indra/newview/llmediactrl.h +++ b/indra/newview/llmediactrl.h @@ -77,6 +77,14 @@ class LLMediaCtrl : public: virtual ~LLMediaCtrl(); + enum ELoadingState + { + LOADING_STATE_INITIALIZING = 0, + LOADING_STATE_LOADING = 1, + LOADING_STATE_LOADED = 2, + LOADING_STATE_ERROR = 3 + }; + void setBorderVisible( bool border_visible ); // For the tutorial window, we don't want to take focus on clicks, @@ -181,6 +189,11 @@ class LLMediaCtrl : virtual bool acceptsTextInput() const { return true; } + std::string getMediaMimeType(); + std::string getMediaLoadingStatus(); + std::string getMediaTitle(); + bool executeJavaScript(const std::string& script); + protected: void convertInputCoords(S32& x, S32& y); @@ -217,6 +230,7 @@ class LLMediaCtrl : viewer_media_t mMediaSource; S32 mTextureWidth, mTextureHeight; + ELoadingState mLoadingState; class LLWindowShade* mWindowShade; LLHandle mContextMenuHandle; diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index b72301c5666..36bcba2c5c5 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -167,7 +167,8 @@ LLMuteList::LLMuteList() : mLoadState(ML_INITIAL), mLoadSource(MLS_NONE), mRequestStartTime(0.f), - mTriedCacheFallback(false) + mTriedCacheFallback(false), + mTriedRegionChangeRetry(false) { gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList); @@ -857,6 +858,9 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) // Guard against potentially writing back to disk since we're not recovering our connection mLoadState = ML_LOADED; mLoadSource = MLS_FALLBACK_CACHE; + // This code path means we have disconnected/crashed before our request has been sent. + // As a result we do not NEED to do anything more than set these state values. + // cache() is liable to be called on shutdown, but since we've set a dirty state it will avoid writing to disk. return; } if (!gAgent.getRegion()) @@ -879,7 +883,7 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) void LLMuteList::cache(const LLUUID& agent_id) { // Write to disk even if empty, but never from degraded fallback state. - if (isLoaded() && mLoadSource != MLS_FALLBACK_CACHE) + if (isLoaded() && !isLoadedDegraded()) { const std::string filename = getCacheFilename(agent_id); saveToFile(filename); diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h index 2781e9b1779..aff23c72d13 100644 --- a/indra/newview/llmutelist.h +++ b/indra/newview/llmutelist.h @@ -129,9 +129,9 @@ class LLMuteList : public LLSingleton // Load state accessors. bool isLoaded() const { return mLoadState == ML_LOADED; } // Loaded, but not necessarily from server. bool isFailed() const { return mLoadState == ML_FAILED; } // Unable to load any mute list. Server did not reply. - // Loaded from server, which is the only source we consider authoritative. - bool isLoadedFromServer() const { return isLoaded() && (mLoadSource == MLS_SERVER || mLoadSource == MLS_SERVER_EMPTY); } - // Loaded, but from cache. Would be nice to upgrade to a server load from here if possible. + // Loaded from an authoritative server response, including when the server directs us to use our cached copy. + bool isLoadedFromServer() const { return isLoaded() && (mLoadSource == MLS_SERVER || mLoadSource == MLS_SERVER_EMPTY || mLoadSource == MLS_SERVER_CACHE); } + // Loaded without an authoritative server response. Would be nice to upgrade to a server load from here if possible. bool isLoadedDegraded() const { return isLoaded() && !isLoadedFromServer(); } // Advance the load state machine, trying cache fallback if necessary. diff --git a/indra/newview/lloutfitslist.cpp b/indra/newview/lloutfitslist.cpp index 7db79c70106..a5137984f24 100644 --- a/indra/newview/lloutfitslist.cpp +++ b/indra/newview/lloutfitslist.cpp @@ -519,6 +519,22 @@ void LLOutfitsList::resetItemSelection(LLWearableItemsList* list, const LLUUID& list->resetSelection(); mItemSelected = false; signalSelectionOutfitUUID(category_id); + + // If filtering was applied while tab was collapsed, item visibility is updated but the parent tab height might not be updated. + // Force rearrange to recompute the height, when tab is expanded. + static LLCachedControl show_all_items(gSavedSettings, "OutfitListFilterFullList", 1); + if (!show_all_items) + { + outfits_map_t::const_iterator tab_iter = mOutfitsMap.find(category_id); + if (tab_iter != mOutfitsMap.end()) + { + LLOutfitAccordionCtrlTab* tab = tab_iter->second; + if (tab && tab->getDisplayChildren()) + { + list->notify(LLSD().with("rearrange", true)); + } + } + } } void LLOutfitsList::onChangeOutfitSelection(LLWearableItemsList* list, const LLUUID& category_id) @@ -1648,7 +1664,7 @@ bool LLOutfitListSortMenu::onEnable(LLSD::String param) } else if ("show_entire_outfit" == param) { - static LLCachedControl filter_mode(gSavedSettings, "OutfitListFilterFullList", 0); + static LLCachedControl filter_mode(gSavedSettings, "OutfitListFilterFullList", 1); return filter_mode; } diff --git a/indra/newview/llpaneldirgroups.cpp b/indra/newview/llpaneldirgroups.cpp index 992d92091cf..324d84efde9 100644 --- a/indra/newview/llpaneldirgroups.cpp +++ b/indra/newview/llpaneldirgroups.cpp @@ -51,6 +51,12 @@ bool LLPanelDirGroups::postBuild() childSetAction("Search", &LLPanelDirBrowser::onClickSearchCore, this); setDefaultBtn( "Search" ); + if (gAgent.isTeen()) + { + childSetEnabled("incmature", false); + gSavedSettings.setBOOL("ShowMatureGroups", false); + } + return true; } @@ -72,11 +78,25 @@ void LLPanelDirGroups::performQuery() U32 scope = DFQ_GROUPS; // Check group mature filter. - if ( !gSavedSettings.getBOOL("ShowMatureGroups") || gAgent.isTeen() ) + if ( gSavedSettings.getBOOL("ShowMatureGroups") && !gAgent.isTeen() ) { + // Supposed behavior: + // if nothing is set will search for <= mature + // if DFQ_INC_PG is set, will look for <= PG + // if DFQ_INC_MATURE is set, will look for == mature + // if DFQ_INC_ADULT is set, will look for >= adult + // Not compatible with legacy DFQ_FILTER_MATURE. + // But there appears to be a server bug, so we only use + // this to show all and use legacy setting for 'pg only' + scope |= DFQ_INC_PG; + scope |= DFQ_INC_MATURE; + scope |= DFQ_INC_ADULT; + } + else + { + // DFQ_FILTER_MATURE is a legacy setting scope |= DFQ_FILTER_MATURE; } - mCurrentSortColumn = "score"; mCurrentSortAscending = false; diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp index 345426824e6..3488685f141 100644 --- a/indra/newview/llpanelface.cpp +++ b/indra/newview/llpanelface.cpp @@ -854,24 +854,56 @@ struct LLPanelFaceSetAlignedTEFunctor : public LLSelectedTEFunctor } // Also align GLTF material if any - S32 gltf_info_index = 0; // base texture + LLGLTFMaterial::TextureInfo gltf_info_index = mPanel->getPBRTextureInfo(); LLVector2 gltf_offset, gltf_scale; F32 gltf_rot; - if (facep->calcAlignedPlanarGLTF(mCenterFace, &gltf_offset, &gltf_scale, &gltf_rot, gltf_info_index)) + + if (gltf_info_index == LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT) { + // "Complete material" - update all texture transforms LLGLTFMaterial new_override; const LLTextureEntry* tep = object->getTE(te); if (tep && tep->getGLTFMaterialOverride()) { new_override = *tep->getGLTFMaterialOverride(); } + bool any_changed = false; + + for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i) + { + if (facep->calcAlignedPlanarGLTF(mCenterFace, &gltf_offset, &gltf_scale, &gltf_rot, i)) + { + LLGLTFMaterial::TextureTransform& transform = new_override.mTextureTransform[i]; + transform.mOffset.set(gltf_offset.mV[0], gltf_offset.mV[1]); + transform.mScale.set(gltf_scale.mV[0], gltf_scale.mV[1]); + transform.mRotation = gltf_rot; + any_changed = true; + } + } + + if (any_changed) + { + LLGLTFMaterialList::queueModify(object, te, &new_override); + } + } + else + { + if (facep->calcAlignedPlanarGLTF(mCenterFace, &gltf_offset, &gltf_scale, &gltf_rot, gltf_info_index)) + { + LLGLTFMaterial new_override; + const LLTextureEntry* tep = object->getTE(te); + if (tep && tep->getGLTFMaterialOverride()) + { + new_override = *tep->getGLTFMaterialOverride(); + } - LLGLTFMaterial::TextureTransform& transform = new_override.mTextureTransform[gltf_info_index]; - transform.mOffset.set(gltf_offset.mV[0], gltf_offset.mV[1]); - transform.mScale.set(gltf_scale.mV[0], gltf_scale.mV[1]); - transform.mRotation = gltf_rot; + LLGLTFMaterial::TextureTransform& transform = new_override.mTextureTransform[gltf_info_index]; + transform.mOffset.set(gltf_offset.mV[0], gltf_offset.mV[1]); + transform.mScale.set(gltf_scale.mV[0], gltf_scale.mV[1]); + transform.mRotation = gltf_rot; - LLGLTFMaterialList::queueModify(object, te, &new_override); + LLGLTFMaterialList::queueModify(object, te, &new_override); + } } } if (!set_aligned) @@ -1073,6 +1105,9 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/) // only turn on auto-adjust button if there is a media renderer and the media is loaded mBtnAlign->setEnabled(editable); + // enable if needed before changing selection + mComboMatMedia->setEnabledByValue("Materials", !has_pbr_material); + if (mComboMatMedia->getCurrentIndex() < MATMEDIA_MATERIAL) { // When selecting an object with a pbr and UI combo is not set, @@ -1677,7 +1712,6 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/) mCheckFullbright->setValue((S32)(fullbright_flag != 0)); mCheckFullbright->setEnabled(editable && !has_pbr_material); mCheckFullbright->setTentative(!identical_fullbright); - mComboMatMedia->setEnabledByValue("Materials", !has_pbr_material); } // Repeats per meter diff --git a/indra/newview/llpanelgrouproles.cpp b/indra/newview/llpanelgrouproles.cpp index e1f2d7588c8..426a89fe6cf 100644 --- a/indra/newview/llpanelgrouproles.cpp +++ b/indra/newview/llpanelgrouproles.cpp @@ -1938,9 +1938,11 @@ LLPanelGroupRolesSubTab::LLPanelGroupRolesSubTab() mRolesList(NULL), mAssignedMembersList(NULL), mAllowedActionsList(NULL), + mActionDescription(NULL), mRoleName(NULL), mRoleTitle(NULL), mRoleDescription(NULL), + mMembersNotLoadedLbl(NULL), mMemberVisibleCheck(NULL), mDeleteRoleButton(NULL), mCopyRoleButton(NULL), @@ -1969,6 +1971,7 @@ bool LLPanelGroupRolesSubTab::postBuildSubTab(LLView* root) mAssignedMembersList = parent->getChild("role_assigned_members"); mAllowedActionsList = parent->getChild("role_allowed_actions"); mActionDescription = parent->getChild("role_action_description"); + mMembersNotLoadedLbl = parent->getChild("members_not_loaded"); mRoleName = parent->getChild("role_name"); mRoleTitle = parent->getChild("role_title"); @@ -2199,12 +2202,18 @@ void LLPanelGroupRolesSubTab::update(LLGroupChange gc) } } - if ((GC_ROLE_MEMBER_DATA == gc || GC_MEMBER_DATA == gc) - && gdatap - && gdatap->isMemberDataComplete() - && gdatap->isRoleMemberDataComplete()) + if (gdatap && gdatap->isMemberDataComplete()) { - buildMembersList(); + if ((GC_ROLE_MEMBER_DATA == gc || GC_MEMBER_DATA == gc) + && gdatap->isRoleMemberDataComplete()) + { + buildMembersList(); + } + mMembersNotLoadedLbl->setVisible(false); + } + else + { + mMembersNotLoadedLbl->setVisible(true); } } diff --git a/indra/newview/llpanelgrouproles.h b/indra/newview/llpanelgrouproles.h index e320efa1c7b..18c0e5dfeaa 100644 --- a/indra/newview/llpanelgrouproles.h +++ b/indra/newview/llpanelgrouproles.h @@ -297,6 +297,7 @@ class LLPanelGroupRolesSubTab : public LLPanelGroupSubTab LLLineEditor* mRoleTitle; LLTextEditor* mRoleDescription; + LLUICtrl* mMembersNotLoadedLbl; LLCheckBoxCtrl* mMemberVisibleCheck; LLButton* mDeleteRoleButton; LLButton* mCreateRoleButton; diff --git a/indra/newview/llpanelmarketplaceinboxinventory.cpp b/indra/newview/llpanelmarketplaceinboxinventory.cpp index 557c7bbd7ba..e0dbd9acd21 100644 --- a/indra/newview/llpanelmarketplaceinboxinventory.cpp +++ b/indra/newview/llpanelmarketplaceinboxinventory.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2009&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 @@ -82,10 +82,20 @@ LLFolderViewFolder * LLInboxInventoryPanel::createFolderViewFolder(LLInvFVBridge LLInboxFolderViewFolder::Params params; - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "fld"; +#endif params.root = mFolderRoot.get(); params.listener = bridge; - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; params.allow_drop = allow_drop; @@ -99,12 +109,22 @@ LLFolderViewItem * LLInboxInventoryPanel::createFolderViewItem(LLInvFVBridge * b LLInboxFolderViewItem::Params params; - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "itm"; +#endif params.creation_date = bridge->getCreationDate(); params.root = mFolderRoot.get(); params.listener = bridge; params.rect = LLRect (0, 0, 0, 0); - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; diff --git a/indra/newview/llpanelobjectinventory.cpp b/indra/newview/llpanelobjectinventory.cpp index d27ce81e4f0..126f6d3776f 100644 --- a/indra/newview/llpanelobjectinventory.cpp +++ b/indra/newview/llpanelobjectinventory.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 @@ -1611,7 +1611,6 @@ void LLPanelObjectInventory::createViewsForCategory(LLInventoryObject::object_li params.name = obj->getName(); params.root = mFolders; params.listener = bridge; - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; view = LLUICtrlFactory::create(params); @@ -1625,7 +1624,6 @@ void LLPanelObjectInventory::createViewsForCategory(LLInventoryObject::object_li params.listener = bridge; params.creation_date = bridge->getCreationDate(); params.rect = LLRect(); - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; view = LLUICtrlFactory::create(params); diff --git a/indra/newview/llpanelplaces.cpp b/indra/newview/llpanelplaces.cpp index 5c866905d6a..5dd4960542e 100644 --- a/indra/newview/llpanelplaces.cpp +++ b/indra/newview/llpanelplaces.cpp @@ -84,6 +84,7 @@ static const std::string LANDMARK_INFO_TYPE = "landmark"; static const std::string REMOTE_PLACE_INFO_TYPE = "remote_place"; static const std::string TELEPORT_HISTORY_INFO_TYPE = "teleport_history"; static const std::string LANDMARK_TAB_INFO_TYPE = "open_landmark_tab"; +static const std::string TELEPORT_HISTORY_TAB_INFO_TYPE = "open_teleport_history_tab"; // Support for secondlife:///app/parcel/{UUID}/about SLapps class LLParcelHandler : public LLCommandHandler @@ -411,6 +412,15 @@ void LLPanelPlaces::onOpen(const LLSD& key) // Update the buttons at the bottom of the panel updateVerbs(); } + else if (key_type == TELEPORT_HISTORY_TAB_INFO_TYPE) + { + // toggle twice, similar to LANDMARK_TAB_INFO_TYPE + togglePlaceInfoPanel(false); + mPlaceInfoType = key_type; + togglePlaceInfoPanel(false); + onTabSelected(); + updateVerbs(); + } else if (key_type == CREATE_PICK_TYPE) { LLUUID item_id = key["item_id"]; @@ -1104,6 +1114,18 @@ void LLPanelPlaces::togglePlaceInfoPanel(bool visible) } } } + else if (mPlaceInfoType == TELEPORT_HISTORY_TAB_INFO_TYPE) + { + mLandmarkInfo->setVisible(false); + mPlaceProfile->setVisible(false); + if (!visible) + { + if (LLPanel* teleport_history_panel = mTabContainer->getPanelByName("Teleport History")) + { + mTabContainer->selectTabPanel(teleport_history_panel); + } + } + } } // virtual diff --git a/indra/newview/llpanelprofilepicks.cpp b/indra/newview/llpanelprofilepicks.cpp index c9626bf9ea0..6870d719afe 100644 --- a/indra/newview/llpanelprofilepicks.cpp +++ b/indra/newview/llpanelprofilepicks.cpp @@ -622,6 +622,7 @@ bool LLPanelProfilePick::postBuild() { mPickName = getChild("pick_name"); mPickDescription = getChild("pick_desc"); + mPickLocation = getChild("pick_location"); mSaveButton = getChild("save_changes_btn"); mCreateButton = getChild("create_changes_btn"); mCancelButton = getChild("cancel_changes_btn"); @@ -646,11 +647,20 @@ bool LLPanelProfilePick::postBuild() mPickDescription->setKeystrokeCallback(boost::bind(&LLPanelProfilePick::onPickChanged, this, _1)); mPickDescription->setFocusReceivedCallback(boost::bind(&LLPanelProfilePick::onDescriptionFocusReceived, this)); - getChild("pick_location")->setEnabled(false); + mPickLocation->setEnabled(false); return true; } +void LLPanelProfilePick::reshape(S32 width, S32 height, bool called_from_parent) +{ + LLPanelProfilePropertiesProcessorTab::reshape(width, height, called_from_parent); + if (mPickLocation) + { + mPickLocation->setCursor(0); + } +} + void LLPanelProfilePick::onDescriptionFocusReceived() { if (!mIsEditing && getSelfProfile()) @@ -749,7 +759,14 @@ void LLPanelProfilePick::setPickLocation(const LLUUID &parcel_id, const std::str void LLPanelProfilePick::setPickLocation(const std::string& location) { - getChild("pick_location")->setValue(location); + mPickLocation->setValue(location); + // Pick location can be set with a long 'substitute' value or + // just a long value. + // If user sets cursor at the end, application of the substitute + // value can shift text from visible are to the left. When text + // gets restored or set, text position isn't, so just drop cursor + // position. + mPickLocation->setCursor(0); mPickLocationStr = location; mLastRequestTimer.reset(); } @@ -896,6 +913,10 @@ void LLPanelProfilePick::sendParcelInfoRequest() void LLPanelProfilePick::processParcelInfo(const LLParcelData& parcel_data) { + // Region might have moved since the pick was saved; refresh the stored global position + // using the parcel info so map/teleport use the current location. + setPosGlobal(LLVector3d(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z)); + setPickLocation(createLocationText(LLStringUtil::null, parcel_data.name, parcel_data.sim_name, getPosGlobal())); // We have received parcel info for the requested ID so clear it now. diff --git a/indra/newview/llpanelprofilepicks.h b/indra/newview/llpanelprofilepicks.h index 847ac57cea5..b925f09f92d 100644 --- a/indra/newview/llpanelprofilepicks.h +++ b/indra/newview/llpanelprofilepicks.h @@ -112,6 +112,8 @@ class LLPanelProfilePick void setAvatarId(const LLUUID& avatar_id) override; + void reshape(S32 width, S32 height, bool called_from_parent = true) override; + void setPickId(const LLUUID& id) { mPickId = id; } virtual LLUUID& getPickId() { return mPickId; } @@ -228,13 +230,14 @@ class LLPanelProfilePick protected: - LLTextureCtrl* mSnapshotCtrl; - LLLineEditor* mPickName; - LLTextEditor* mPickDescription; - LLButton* mSetCurrentLocationButton; - LLButton* mSaveButton; - LLButton* mCreateButton; - LLButton* mCancelButton; + LLTextureCtrl* mSnapshotCtrl = nullptr; + LLLineEditor* mPickName = nullptr; + LLTextEditor* mPickDescription = nullptr; + LLLineEditor* mPickLocation = nullptr; + LLButton* mSetCurrentLocationButton = nullptr; + LLButton* mSaveButton = nullptr; + LLButton* mCreateButton = nullptr; + LLButton* mCancelButton = nullptr; LLVector3d mPosGlobal; LLUUID mParcelId; diff --git a/indra/newview/llpanelvoicedevicesettings.cpp b/indra/newview/llpanelvoicedevicesettings.cpp index d8d6bcf5fd5..609177046e0 100644 --- a/indra/newview/llpanelvoicedevicesettings.cpp +++ b/indra/newview/llpanelvoicedevicesettings.cpp @@ -149,7 +149,7 @@ void LLPanelVoiceDeviceSettings::draw() LLColor4 color; if (power_bar_idx < discrete_power) { - color = (power_bar_idx >= 3) ? LLUIColorTable::instance().getColor("OverdrivenColor") : LLUIColorTable::instance().getColor("SpeakingColor"); + color = (power_bar_idx >= 3) ? LLUIColorTable::instance().getColor("OverdrivenColor") : LLUIColorTable::instance().getColor("OutfitGalleryItemSelected"); } else { @@ -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(); } } diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index 1b64eab3c0a..72f92785fcc 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -252,7 +252,6 @@ LLSelectMgr::LLSelectMgr() LLSelectMgr::~LLSelectMgr() { clearSelections(); - mSlectionLodModChangedConnection.disconnect(); } void LLSelectMgr::clearSelections() diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h index 11aad3b806f..792a37297ff 100644 --- a/indra/newview/llselectmgr.h +++ b/indra/newview/llselectmgr.h @@ -943,7 +943,6 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton bool mForceSelection; std::vector mPauseRequests; - boost::signals2::connection mSlectionLodModChangedConnection; }; // *DEPRECATED: For callbacks or observers, use diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 55f23eb0210..cd4c9d40b1f 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -24,6 +24,72 @@ * $/LicenseInfo$ */ +// +// LOGIN AND CONNECTION SEQUENCE OVERVIEW +// ====================================== +// The Viewer connects to the SL service in two phases: HTTP authentication +// followed by UDP "Circuit" establishment to the first Simulator. +// +// PHASE 1: HTTP LOGIN (see lllogin.cpp, process_login_success_response()) +// ----------------------------------------------------------------------- +// Viewer sends an XMLRPC HTTP POST to LoginServer containing: +// - Credentials (first name, last name, password) +// - Client version, channel, MAC address, machine ID +// - Start location preferences +// +// Login-server responds with critical connection data: +// - agent_id, session_id, secure_session_id (authentication tokens) +// - Circuit_code (used to establish UDP Circuit with Simulator) +// - sim_ip, sim_port (Simulator address to connect to) +// - seed_capability (base URL for HTTP capability requests) +// - region_x, region_y (region grid coordinates) +// +// PHASE 2: UDP CIRCUIT ESTABLISHMENT (see idle_startup() state machine below) +// --------------------------------------------------------------------------- +// After HTTP login succeeds, Viewer establishes a UDP Circuit with the +// simulator. This also happens whenever the Viewer connects to new Simulators +// in the same session. The following UDP messages are exchanged: +// +// 1. UseCircuitCode (Viewer -> Simulator) +// - Sent in STATE_WORLD_INIT +// - Contains: Circuit_code, session_id, agent_id +// - Establishes the UDP Circuit with the Simulator +// +// 2. RegionHandshake (Simulator -> Viewer) +// - Handled by process_region_handshake() in llworld.cpp +// - Contains: region name, terrain textures, water height, region flags +// - Viewer responds with RegionHandshakeReply +// +// 3. CompleteAgentMovement (Viewer -> Simulator) +// - Sent in STATE_AGENT_SEND via send_complete_agent_movement() +// - Signals the Viewer is ready to enter the world +// +// 4. AgentMovementComplete (Simulator -> Viewer) +// - Handled by process_agent_movement_complete() in llviewermessage.cpp +// - Contains: final agent position, look_at direction, region handle +// - Sets gAgentMovementCompleted = true +// - Agent is now fully connected to the region +// +// STARTUP STATE MACHINE +// --------------------- +// The connection sequence is managed by idle_startup() which progresses +// through these key states: +// +// STATE_LOGIN_WAIT - Waiting for HTTP login response +// STATE_LOGIN_PROCESS_RESPONSE - Processing login response data +// STATE_WORLD_INIT - Send UseCircuitCode, enable UDP Circuit +// STATE_WORLD_WAIT - Wait for Circuit acknowledgment +// STATE_AGENT_SEND - Send CompleteAgentMovement +// STATE_AGENT_WAIT - Wait for AgentMovementComplete +// STATE_INVENTORY_SEND - Agent connected, begin loading inventory +// +// HTTP CAPABILITIES +// ----------------- +// After UDP connection, Viewer fetches "capability" URLs from the +// seed_capability endpoint. These provide HTTP endpoints for various +// services (inventory, textures, etc.) that supplement the UDP protocol. +// + #include "llviewerprecompiledheaders.h" #include "llappviewer.h" @@ -277,7 +343,6 @@ void show_first_run_dialog(); bool first_run_dialog_callback(const LLSD& notification, const LLSD& response); void set_startup_status(const F32 frac, const std::string& string, const std::string& msg); bool login_alert_status(const LLSD& notification, const LLSD& response); -void use_circuit_callback(void**, S32 result); void register_viewer_callbacks(LLMessageSystem* msg); void asset_callback_nothing(const LLUUID&, LLAssetType::EType, void*, S32); bool callback_choose_gender(const LLSD& notification, const LLSD& response); @@ -336,7 +401,7 @@ void do_startup_frame() break; } } - if (needs_drain || gMessageSystem->mPacketRing.getNumBufferedPackets() > 0) + if (needs_drain || gMessageSystem->getNumBufferedPackets() > 0) { gMessageSystem->drainUdpSocket(); } @@ -720,7 +785,7 @@ bool idle_startup() F32 dropPercent = gSavedSettings.getF32("PacketDropPercentage"); - msg->mPacketRing.setDropPercentage(dropPercent); + msg->setDropPercentage(dropPercent); } LL_INFOS("AppInit") << "Message System Initialized." << LL_ENDL; @@ -832,6 +897,7 @@ bool idle_startup() set_startup_status(0.03f, msg.c_str(), gAgent.mMOTD.c_str()); do_startup_frame(); // LLViewerMedia::initBrowser(); + LLAppViewer::instance()->createInitedMarker(); LLStartUp::setStartupState( STATE_LOGIN_SHOW ); return false; } @@ -1353,6 +1419,10 @@ bool idle_startup() { set_startup_status(0.30f, LLTrans::getString("LoginInitializingWorld"), gAgent.mMOTD); do_startup_frame(); + + // close login UI before world UI is initialized, if it is still visible + LLPanelLogin::closePanel(); + // We should have an agent id by this point. llassert(!(gAgentID == LLUUID::null)); @@ -1714,13 +1784,48 @@ bool idle_startup() gUseCircuitCallbackCalled = false; msg->enableCircuit(gFirstSim, true); - // now, use the circuit info to tell simulator about us! + + // UDP CONNECTION STEP 1: Send UseCircuitCode + // This is the first UDP message sent to Simulator after HTTP login. + // It establishes the UDP circuit using the circuit_code received from + // LoginServer. Simulator will respond with an ACK, then send + // RegionHandshake message with region details. LL_INFOS("AppInit") << "viewer: UserLoginLocationReply() Enabling " << gFirstSim << " with code " << msg->mOurCircuitCode << LL_ENDL; msg->newMessageFast(_PREHASH_UseCircuitCode); msg->nextBlockFast(_PREHASH_CircuitCode); msg->addU32Fast(_PREHASH_Code, msg->mOurCircuitCode); msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); msg->addUUIDFast(_PREHASH_ID, gAgent.getID()); + + // build a lambda to be used as callback on ACK or timeout + void (*use_circuit_callback)(void**, S32) = [](void**, S32 result) + { + // bail if we're quitting. + if(LLApp::isExiting()) return; + if( !gUseCircuitCallbackCalled ) + { + gUseCircuitCallbackCalled = true; + if (result != LL_ERR_NOERR) + { + // Make sure user knows something bad happened. JC + LL_WARNS("AppInit") << "Backing up to login screen!" << LL_ENDL; + if (gRememberPassword) + { + LLNotificationsUtil::add("LoginPacketNeverReceived", LLSD(), LLSD(), login_alert_status); + } + else + { + LLNotificationsUtil::add("LoginPacketNeverReceivedNoTP", LLSD(), LLSD(), login_alert_status); + } + reset_login(); + } + else + { + gGotUseCircuitCodeAck = true; + } + } + }; + msg->sendReliable( gFirstSim, gSavedSettings.getS32("UseCircuitCodeMaxRetries"), @@ -1738,6 +1843,9 @@ bool idle_startup() //--------------------------------------------------------------------- // World Wait //--------------------------------------------------------------------- + // UDP CONNECTION STEP 2: Wait for UseCircuitCode acknowledgment + // While waiting, Simulator also sends RegionHandshake (handled by + // process_region_handshake() in llworld.cpp) containing region info. if(STATE_WORLD_WAIT == LLStartUp::getStartupState()) { LL_DEBUGS("AppInit") << "Waiting for simulator ack...." << LL_ENDL; @@ -1753,13 +1861,16 @@ bool idle_startup() //--------------------------------------------------------------------- // Agent Send //--------------------------------------------------------------------- + // UDP CONNECTION STEP 3: Send CompleteAgentMovement + // After the circuit is established and RegionHandshake received, we signal + // to Simulator the Viewer is ready to enter the world. if (STATE_AGENT_SEND == LLStartUp::getStartupState()) { LL_DEBUGS("AppInit") << "Connecting to region..." << LL_ENDL; set_startup_status(0.60f, LLTrans::getString("LoginConnectingToRegion"), gAgent.mMOTD); do_startup_frame(); - // register with the message system so it knows we're - // expecting this message + // Register handler process_agent_movement_complete for AgentMovementComplete - + // the final UDP message confirming the agent is connected. LLMessageSystem* msg = gMessageSystem; msg->setHandlerFuncFast( _PREHASH_AgentMovementComplete, @@ -1802,12 +1913,17 @@ bool idle_startup() //--------------------------------------------------------------------- // Agent Wait //--------------------------------------------------------------------- + // UDP CONNECTION STEP 4: Wait for AgentMovementComplete + // Simulator responds with the agent's confirmed position and look_at + // direction. Once received, gAgentMovementCompleted is set true and the + // agent is fully connected to the region. if (STATE_AGENT_WAIT == LLStartUp::getStartupState()) { do_startup_frame(); if (gAgentMovementCompleted) { + // Connection complete - agent is now in-world LLStartUp::setStartupState( STATE_INVENTORY_SEND ); } do_startup_frame(); @@ -2806,34 +2922,6 @@ bool login_alert_status(const LLSD& notification, const LLSD& response) } -void use_circuit_callback(void**, S32 result) -{ - // bail if we're quitting. - if(LLApp::isExiting()) return; - if( !gUseCircuitCallbackCalled ) - { - gUseCircuitCallbackCalled = true; - if (result) - { - // Make sure user knows something bad happened. JC - LL_WARNS("AppInit") << "Backing up to login screen!" << LL_ENDL; - if (gRememberPassword) - { - LLNotificationsUtil::add("LoginPacketNeverReceived", LLSD(), LLSD(), login_alert_status); - } - else - { - LLNotificationsUtil::add("LoginPacketNeverReceivedNoTP", LLSD(), LLSD(), login_alert_status); - } - reset_login(); - } - else - { - gGotUseCircuitCodeAck = true; - } - } -} - void register_viewer_callbacks(LLMessageSystem* msg) { msg->setHandlerFuncFast(_PREHASH_LayerData, process_layer_data ); @@ -3726,6 +3814,14 @@ bool init_benefits(LLSD& response) return succ; } +// HTTP LOGIN RESPONSE PROCESSING +// Called after successful HTTP XMLRPC authentication. Extracts critical data +// from LoginServer response needed to establish the UDP connection: +// - agent_id, session_id, secure_session_id (authentication tokens) +// - circuit_code (used in UseCircuitCode UDP message) +// - sim_ip, sim_port (simulator address for UDP circuit) +// - seed_capability (URL for fetching HTTP capability endpoints) +// bool process_login_success_response() { LLSD response = LLLoginInstance::getInstance()->getResponse(); @@ -3848,6 +3944,8 @@ bool process_login_success_response() gAgentStartLocation.assign(text); } + // Extract UDP circuit parameters from login response. + // These are used in STATE_WORLD_INIT to establish the UDP circuit. text = response["circuit_code"].asString(); if(!text.empty()) { diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp index 51ade608272..144940c7053 100644 --- a/indra/newview/lltexturefetch.cpp +++ b/indra/newview/lltexturefetch.cpp @@ -1297,7 +1297,7 @@ bool LLTextureFetchWorker::doWork(S32 param) else { mCanUseCapability = false; - if (gDisconnected) + if (gDisconnected || LLAppViewer::isExiting()) { // We lost connection or are shutting down. mCanUseHTTP = false; @@ -1315,6 +1315,12 @@ bool LLTextureFetchWorker::doWork(S32 param) else { mCanUseCapability = false; + if (gDisconnected || LLAppViewer::isExiting()) + { + // We lost connection or are shutting down. + mCanUseHTTP = false; + return true; // abort + } mRegionRetryAttempt++; mRegionRetryTimer.setTimerExpirySec(CAP_MISSING_EXPIRATION_DELAY); // This will happen if not logged in or if a region deoes not have HTTP Texture enabled diff --git a/indra/newview/lltoolbrush.cpp b/indra/newview/lltoolbrush.cpp index cf7b123fa7e..d31dc078e5f 100644 --- a/indra/newview/lltoolbrush.cpp +++ b/indra/newview/lltoolbrush.cpp @@ -685,7 +685,10 @@ bool LLToolBrushLand::canTerraformParcel(LLViewerRegion* regionp) const if (selected_parcel) { bool owner_release = LLViewerParcelMgr::isParcelOwnedByAgent(selected_parcel, GP_LAND_ALLOW_EDIT_LAND); - is_terraform_allowed = ( gAgent.canManageEstate() || (selected_parcel->getOwnerID() == regionp->getOwner()) || owner_release); + is_terraform_allowed = ( regionp->canManageEstate() + || (selected_parcel->getOwnerID() == regionp->getOwner()) + || owner_release + || selected_parcel->getAllowTerraform()); } return is_terraform_allowed; diff --git a/indra/newview/lltooldraganddrop.cpp b/indra/newview/lltooldraganddrop.cpp index 5e2d91d31e8..79139181990 100644 --- a/indra/newview/lltooldraganddrop.cpp +++ b/indra/newview/lltooldraganddrop.cpp @@ -2578,7 +2578,12 @@ bool is_water_exclusion_face(LLViewerObject* obj, S32 face) bool exclude_water = (image->getID() == IMG_ALPHA_GRAD) && obj->isImageAlphaBlended(face); // transparency - exclude_water &= (obj->getTE(face)->getColor().mV[VALPHA] == 1); + // getTE gets from a different source than getTEImage, so check for null + LLTextureEntry* te = obj->getTE(face); + if (te) + { + exclude_water &= (te->getColor().mV[VALPHA] == 1); + } //absence of normal and specular textures image = obj->getTENormalMap(face); diff --git a/indra/newview/llurlfloaterdispatchhandler.cpp b/indra/newview/llurlfloaterdispatchhandler.cpp index 9bee4870be0..cf0657ea2f6 100644 --- a/indra/newview/llurlfloaterdispatchhandler.cpp +++ b/indra/newview/llurlfloaterdispatchhandler.cpp @@ -29,15 +29,9 @@ #include "llurlfloaterdispatchhandler.h" #include "llfloaterreg.h" -#include "llfloaterhowto.h" #include "llfloaterwebcontent.h" #include "llsdserialize.h" -#include "llviewercontrol.h" #include "llviewergenericmessage.h" -#include "llweb.h" - -// Example: -// llOpenFloater("guidebook", "http://page.com", []); // values specified by server side's dispatcher // for llopenfloater @@ -50,20 +44,12 @@ const std::string KEY_URL("floater_url"); const std::string KEY_PARAMS("floater_params"); // Supported floaters -const std::string FLOATER_GUIDEBOOK("guidebook"); -const std::string FLOATER_HOW_TO("how_to"); // alias for guidebook const std::string FLOATER_WEB_CONTENT("web_content"); // All arguments are palceholders! Server side will need to add validation first. // Web content universal argument const std::string KEY_TRUSTED_CONTENT("trusted_content"); -// Guidebook specific arguments -const std::string KEY_WIDTH("width"); -const std::string KEY_HEGHT("height"); -const std::string KEY_CAN_CLOSE("can_close"); -const std::string KEY_TITLE("title"); - // web_content specific arguments const std::string KEY_SHOW_PAGE_TITLE("show_page_title"); const std::string KEY_ALLOW_ADRESS_ENTRY("allow_address_entry"); // It is not recomended to set this to true if trusted content is allowed @@ -143,48 +129,7 @@ bool LLUrlFloaterDispatchHandler::operator()(const LLDispatcher *, const std::st LLFloaterWebContent::Params params; params.url = url; - if (floater == FLOATER_GUIDEBOOK || floater == FLOATER_HOW_TO) - { - LL_DEBUGS("URLFloater") << "Opening how_to floater with parameters: " << message << LL_ENDL; - if (command_params.isMap()) // by default is undefines - { - params.trusted_content = command_params.has(KEY_TRUSTED_CONTENT) ? command_params[KEY_TRUSTED_CONTENT].asBoolean() : false; - - // Script's side argument list can't include other lists, neither - // there is a LLRect type, so expect just width and height - if (command_params.has(KEY_WIDTH) && command_params.has(KEY_HEGHT)) - { - LLRect rect(0, command_params[KEY_HEGHT].asInteger(), command_params[KEY_WIDTH].asInteger(), 0); - params.preferred_media_size.setValue(rect); - } - } - - // Some locations will have customized guidebook, which this function easists for - // only one instance of guidebook can exist at a time, so if this command arrives, - // we need to close previous guidebook then reopen it. - - LLFloater* instance = LLFloaterReg::findInstance("guidebook"); - if (instance) - { - instance->closeHostedFloater(); - } - - LLFloaterReg::toggleInstanceOrBringToFront("guidebook", params); - - if (command_params.isMap()) - { - LLFloater* instance = LLFloaterReg::findInstance("guidebook"); - if (command_params.has(KEY_CAN_CLOSE)) - { - instance->setCanClose(command_params[KEY_CAN_CLOSE].asBoolean()); - } - if (command_params.has(KEY_TITLE)) - { - instance->setTitle(command_params[KEY_TITLE].asString()); - } - } - } - else if (floater == FLOATER_WEB_CONTENT) + if (floater == FLOATER_WEB_CONTENT) { LL_DEBUGS("URLFloater") << "Opening web_content floater with parameters: " << message << LL_ENDL; if (command_params.isMap()) // by default is undefines, might be better idea to init params from command_params diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index 90eb977bba2..f76d68ed721 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -43,15 +43,19 @@ #include "Velopack.h" #if LL_WINDOWS +#include "llappviewerwin32.h" #include #include #include #include #include #include +#include +#include #pragma comment(lib, "shlwapi.lib") #pragma comment(lib, "ole32.lib") +#pragma comment(lib, "propsys.lib") #pragma comment(lib, "shell32.lib") #endif // LL_WINDOWS @@ -238,7 +242,7 @@ static bool custom_download_asset(void* user_data, { // The asset has already been downloaded at the coroutine level (before vpkc_download_updates). // This callback just copies the pre-downloaded file to where Velopack expects it. - // We cannot use getRawAndSuspend here — coroutine context is lost through the Rust FFI boundary. + // We cannot use getRawAndSuspend here - coroutine context is lost through the Rust FFI boundary. if (sPreDownloadedAssetPath.empty()) { LL_WARNS("Velopack") << "No pre-downloaded asset available" << LL_ENDL; @@ -330,6 +334,17 @@ static std::wstring get_desktop_path() return L""; } +static std::wstring get_app_user_model_id() +{ + // Format: CompanyName.ProductName + // This ID is used for: + // 1. Registry registration (HKCU\Software\Classes\Applications\{exe}\AppUserModelID) + // 2. Shortcut property (IPropertyStore::SetValue with PKEY_AppUserModel_ID) + // 3. Runtime process ID (SetCurrentProcessExplicitAppUserModelID if needed) + // Must be consistent across all uses for proper taskbar grouping. + return L"LindenLab." + get_app_name_oneword(); +} + static HRESULT create_shortcut(const std::wstring& shortcut_path, const std::wstring& target_path, const std::wstring& arguments, @@ -354,6 +369,28 @@ static HRESULT create_shortcut(const std::wstring& shortcut_path, PathRemoveFileSpecW(work_dir); shell_link->SetWorkingDirectory(work_dir); + // Set AppUserModelID on the shortcut + IPropertyStore* prop_store = nullptr; + hr = shell_link->QueryInterface(IID_IPropertyStore, (void**)&prop_store); + if (SUCCEEDED(hr)) + { + PROPVARIANT pv; + PropVariantInit(&pv); + std::wstring app_id = get_app_user_model_id(); + hr = InitPropVariantFromString(app_id.c_str(), &pv); + if (SUCCEEDED(hr)) + { + hr = prop_store->SetValue(PKEY_AppUserModel_ID, pv); + PropVariantClear(&pv); + if (SUCCEEDED(hr)) + { + HRESULT hr_commit = prop_store->Commit(); + if (FAILED(hr_commit)) hr = hr_commit; + } + } + prop_store->Release(); + } + IPersistFile* persist_file = nullptr; hr = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); if (SUCCEEDED(hr)) @@ -405,6 +442,42 @@ static void register_protocol_handler(const std::wstring& protocol, } } +static void register_app_user_model_id(const std::wstring& exe_name) +{ + // Register AppUserModelID for the executable + // This allows Windows to properly group taskbar items when users drag-and-drop + // the exe to pin it, without requiring runtime calls to SetCurrentProcessExplicitAppUserModelID + std::wstring key_path = L"SOFTWARE\\Classes\\Applications\\" + exe_name; + HKEY hkey; + + if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS) + { + std::wstring app_user_model_id = get_app_user_model_id(); + LSTATUS status = RegSetValueExW(hkey, L"AppUserModelID", 0, REG_SZ, + (BYTE*)app_user_model_id.c_str(), + (DWORD)((app_user_model_id.size() + 1) * sizeof(wchar_t))); + RegCloseKey(hkey); + + if (status == ERROR_SUCCESS) + { + LL_DEBUGS("Velopack") << "Registered AppUserModelID: " + << ll_convert_wide_to_string(app_user_model_id) + << " for " << ll_convert_wide_to_string(exe_name) << LL_ENDL; + } + else + { + LL_WARNS("Velopack") << "Failed to set AppUserModelID (error " << status << ") for " + << ll_convert_wide_to_string(exe_name) << LL_ENDL; + } + } + else + { + LL_WARNS("Velopack") << "Failed to register AppUserModelID for " + << ll_convert_wide_to_string(exe_name) << LL_ENDL; + } +} + static bool get_shortcut_target(const std::wstring& lnk_path, std::wstring& target_path_str) { // Resolve the shortcut to check its target @@ -717,6 +790,12 @@ static void unregister_protocol_handler(const std::wstring& protocol) RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); } +static void unregister_app_user_model_id(const std::wstring& exe_name) +{ + std::wstring key_path = L"SOFTWARE\\Classes\\Applications\\" + exe_name; + RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); +} + static void register_uninstall_info(const std::wstring& install_dir, const std::wstring& app_name, const std::wstring& version) @@ -793,7 +872,9 @@ static void unregister_uninstall_info() RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); } -static void create_shortcuts(const std::wstring& install_dir, const std::wstring& app_name) +static void create_shortcuts( + const std::wstring& install_dir, + const std::wstring& app_name) { std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name(); std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name; @@ -865,21 +946,32 @@ static void on_after_install(void* user_data, const char* app_version) { std::wstring install_dir = get_install_dir(); std::wstring app_name = get_app_name(); - std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name(); + std::wstring exe_name = get_viewer_exe_name(); + std::wstring exe_path = install_dir + L"\\" + exe_name; register_protocol_handler(PROTOCOL_SECONDLIFE, L"URL:Second Life", exe_path); register_protocol_handler(PROTOCOL_GRID_INFO, L"URL:Second Life", exe_path); + + // Register AppUserModelID for taskbar pinning support + register_app_user_model_id(exe_name); + create_shortcuts(install_dir, app_name); } static void on_before_uninstall(void* user_data, const char* app_version) { std::wstring app_name = get_app_name(); + std::wstring exe_name = get_viewer_exe_name(); unregister_protocol_handler(PROTOCOL_SECONDLIFE); unregister_protocol_handler(PROTOCOL_GRID_INFO); - unregister_uninstall_info(); + unregister_app_user_model_id(exe_name); remove_shortcuts(app_name); + + std::wstring install_dir = get_install_dir(); + LLAppViewerWin32::sendShutdownToOtherInstances(install_dir); + + unregister_uninstall_info(); } static void on_log_message(void* user_data, const char* level, const char* message) @@ -998,7 +1090,7 @@ static void ensure_update_manager(bool allow_downgrade) LL_INFOS("Velopack") << "Auto-detect failed (" << ll_safe_string(err) << "), falling back to explicit locator" << LL_ENDL; - // Auto-detection failed — construct an explicit locator. + // Auto-detection failed - construct an explicit locator. // This handles legacy DMG installs that don't have Velopack's // install state (UpdateMac, sq.version) in the bundle. vpkc_locator_config_t locator = {}; @@ -1166,7 +1258,7 @@ static void on_downloading_closed(const LLSD& notification, const LLSD& response sDownloadingNotification = nullptr; if (sIsRequired) { - // User closed the downloading dialog during a required update — re-show it + // User closed the downloading dialog during a required update - re-show it show_downloading_notification(sTargetVersion); } } @@ -1255,12 +1347,12 @@ void velopack_check_for_updates(const std::string& required_version, const std:: // strictly lower than what we're running (e.g., a retracted build). bool has_required = !required_version.empty(); int ver_cmp = has_required ? compare_running_version(required_version) : 0; - bool allow_downgrade = ver_cmp > 0; // running > required → rollback scenario + bool allow_downgrade = ver_cmp > 0; // running > required -> rollback scenario ensure_update_manager(allow_downgrade); if (!sUpdateManager) return; - // Ask Velopack to check its feed — this is the source of truth + // Ask Velopack to check its feed - this is the source of truth vpkc_update_info_t* update_info = nullptr; vpkc_update_check_t result = vpkc_check_for_updates(sUpdateManager, &update_info); @@ -1286,7 +1378,7 @@ void velopack_check_for_updates(const std::string& required_version, const std:: sPendingCheckInfo = update_info; // Determine if this is mandatory: running version is below VVM's required floor - bool is_required = ver_cmp < 0; // running < required → must update + bool is_required = ver_cmp < 0; // running < required -> must update sIsRequired = is_required; if (is_required) @@ -1297,12 +1389,12 @@ void velopack_check_for_updates(const std::string& required_version, const std:: return; } - // Optional update — check user preference + // Optional update - check user preference U32 updater_setting = gSavedSettings.getU32("UpdaterServiceSetting"); if (updater_setting == 3) { - // "Install each update automatically" — download silently, apply on quit + // "Install each update automatically" - download silently, apply on quit LL_INFOS("Velopack") << "Optional update to " << target_version << ", downloading automatically (UpdaterServiceSetting=3)" << LL_ENDL; velopack_download_pending_update(); diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 82fc4c6d87f..b034a934dec 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -79,13 +79,13 @@ #include "llfloaterfonttest.h" #include "llfloaterforgetuser.h" #include "llfloatergesture.h" +#include "llfloatergestureautocompletepicker.h" #include "llfloatergltfasseteditor.h" #include "llfloatergodtools.h" #include "llfloatergridstatus.h" #include "llfloatergroups.h" #include "llfloaterhelpbrowser.h" #include "llfloaterhoverheight.h" -#include "llfloaterhowto.h" #include "llfloaterhud.h" #include "llfloaterimagepreview.h" #include "llfloaterimsession.h" @@ -383,6 +383,7 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("font_test", "floater_font_test.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("forget_username", "floater_forget_user.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("gesture_autocomplete_picker", "floater_gesture_autocomplete_picker.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("gestures", "floater_gesture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("gltf_asset_editor", "floater_gltf_asset_editor.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("god_tools", "floater_god_tools.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); @@ -503,7 +504,6 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("search", "floater_search.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("legacy_search", "floater_directory.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("profile", "floater_profile.xml",(LLFloaterBuildFunc)&LLFloaterReg::build); - LLFloaterReg::add("guidebook", "floater_how_to.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("slapp_test", "floater_test_slapp.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterUIPreviewUtil::registerFloater(); diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp index efa3f5cd1e5..707b2f19936 100644 --- a/indra/newview/llviewerinventory.cpp +++ b/indra/newview/llviewerinventory.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2014, 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 @@ -673,6 +673,25 @@ void LLViewerInventoryCategory::setVersion(S32 version) mVersion = version; } +const std::string& LLViewerInventoryCategory::getDisplayName() const +{ + if (mNeedsDisplayNameUpdate) + { + buildDisplayName(); + } + if (!mDisplayName.empty()) + { + return mDisplayName; + } + return getName(); +} + +void LLViewerInventoryCategory::invalidateDisplayName() +{ + mNeedsDisplayNameUpdate = true; + mDisplayName.clear(); +} + bool LLViewerInventoryCategory::fetch(S32 expiry_seconds) { if((VERSION_UNKNOWN == getVersion()) @@ -889,6 +908,34 @@ void LLViewerInventoryCategory::localizeName() LLLocalizedInventoryItemsDictionary::getInstance()->localizeInventoryObjectName(mName); } +void LLViewerInventoryCategory::buildDisplayName() const +{ + // Secure and library folders can't be renamed, + // so we only need to do this once. + mNeedsDisplayNameUpdate = false; + + //"Accessories" inventory category has folder type FT_NONE. So, this folder + //can not be detected as protected with LLFolderType::lookupIsProtectedType + // + // HACK: EXT - 6028 ([HARD CODED]? Inventory > Library > "Accessories" folder) + // Translation of Accessories folder in Library inventory folder + LLFolderType::EType preferred_type = getPreferredType(); + + bool is_accessories = false; + if (getName() == "Accessories") + { + // To ensure that Accessories folder is in Library we have to check its parent folder. + const LLUUID& parent_folder_id = getParentUUID(); + is_accessories = (parent_folder_id == gInventory.getLibraryRootFolderID()); + } + + if (is_accessories || LLFolderType::lookupIsProtectedType(preferred_type)) + { + // All predefined folders have translations in strings.xml. + LLTrans::findString(mDisplayName, std::string("InvFolder ") + getName(), LLSD()); + } +} + // virtual bool LLViewerInventoryCategory::unpackMessage(const LLSD& category) { diff --git a/indra/newview/llviewerinventory.h b/indra/newview/llviewerinventory.h index a42bdaa2b0a..f14850da5a1 100644 --- a/indra/newview/llviewerinventory.h +++ b/indra/newview/llviewerinventory.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 @@ -209,6 +209,13 @@ class LLViewerInventoryCategory : public LLInventoryCategory S32 getVersion() const; void setVersion(S32 version); + const std::string& getDisplayName() const; + // The display name gets cached on demand, so needs a cleanup method. + // But in practice only secure folders' display name mismatches + // actual name, and those folders can't be renamed, so in practice + // this is useless unless we want to free memory. + void invalidateDisplayName(); + // Returns true if a fetch was issued (not nessesary in progress). // no requests will happen during expiry_seconds even if fetch completed bool fetch(S32 expiry_seconds = 10); @@ -253,6 +260,19 @@ class LLViewerInventoryCategory : public LLInventoryCategory S32 mDescendentCount; EFetchType mFetching; LLFrameTimer mDescendentsRequested; + + // Display names are generated on demand and cached. + // buildDisplayName is essentially a way to localize + // system and library folders. + // + // TODO: This is on demand and mutable because that's how it + // worked in inventory bridge, before it was moved. + // But system folders always get loaded, it's likely better + // to just generate from the get go. + // Consider merging with localizeName. + void buildDisplayName() const; + mutable std::string mDisplayName; + mutable bool mNeedsDisplayNameUpdate = true; }; class LLInventoryCallback : public LLRefCount diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 554c7ebcb2e..72ad47be89e 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -2298,7 +2298,7 @@ class LLAdvancedDropPacket : public view_listener_t { bool handleEvent(const LLSD& userdata) { - gMessageSystem->mPacketRing.dropPackets(1); + gMessageSystem->dropPackets(1); return true; } }; @@ -3711,7 +3711,7 @@ class LLAvatarSetImpostorMode : public view_listener_t return false; } - LLVOAvatar::cullAvatarsByPixelArea(); + LLVOAvatar::setCullNeedsUpdate(); return true; } // handleEvent() }; @@ -5182,6 +5182,27 @@ void handle_link_objects() } } +void handle_unlink_objects() +{ + if (LLSelectMgr::getInstance()->getSelection()->isEmpty()) + { + LLPanel* visited_panel = LLFloaterSidePanelContainer::getPanel("places", "Teleport History"); + if (visited_panel && visited_panel->isInVisibleChain()) + { + LLFloaterReg::hideInstance("places"); + } + else + { + LLFloaterReg::toggleInstanceOrBringToFront("places"); + LLFloaterSidePanelContainer::showPanel("places", LLSD().with("type", "open_teleport_history_tab")); + } + } + else + { + LLSelectMgr::getInstance()->unlinkObjects(); + } +} + // You can return an object to its owner if it is on your land. class LLObjectReturn : public view_listener_t { @@ -8503,15 +8524,6 @@ class LLToolsEnableSaveToObjectInventory : public view_listener_t } }; -class LLToggleHowTo : public view_listener_t -{ - bool handleEvent(const LLSD& userdata) - { - LLFloaterReg::toggleInstanceOrBringToFront("guidebook"); - return true; - } -}; - class LLViewEnableMouselook : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -9649,6 +9661,16 @@ void handle_flush_name_caches() if (gCacheName) gCacheName->clear(); } +bool is_master_audio_muted() +{ + return LLAppViewer::instance()->getMasterSystemAudioMute(); +} + +void toggle_master_audio() +{ + LLAppViewer::instance()->setMasterSystemAudioMute(!is_master_audio_muted()); +} + class LLUploadCostCalculator : public view_listener_t { std::string mCostStr; @@ -9920,6 +9942,8 @@ void initialize_menus() view_listener_t::addMenu(new LLWorldEnableEnvPreset(), "World.EnableEnvPreset"); view_listener_t::addMenu(new LLWorldCheckBanLines() , "World.CheckBanLines"); view_listener_t::addMenu(new LLWorldShowBanLines() , "World.ShowBanLines"); + commit.add("World.ToggleMasterAudio", boost::bind(&toggle_master_audio)); + enable.add("World.IsMasterAudioMuted", boost::bind(&is_master_audio_muted)); // Tools menu view_listener_t::addMenu(new LLToolsSelectTool(), "Tools.SelectTool"); @@ -9936,7 +9960,7 @@ void initialize_menus() view_listener_t::addMenu(new LLToolsUseSelectionForGrid(), "Tools.UseSelectionForGrid"); view_listener_t::addMenu(new LLToolsSelectNextPartFace(), "Tools.SelectNextPart"); commit.add("Tools.Link", boost::bind(&handle_link_objects)); - commit.add("Tools.Unlink", boost::bind(&LLSelectMgr::unlinkObjects, LLSelectMgr::getInstance())); + commit.add("Tools.Unlink", boost::bind(&handle_unlink_objects)); view_listener_t::addMenu(new LLToolsStopAllAnimations(), "Tools.StopAllAnimations"); view_listener_t::addMenu(new LLToolsReleaseKeys(), "Tools.ReleaseKeys"); view_listener_t::addMenu(new LLToolsEnableReleaseKeys(), "Tools.EnableReleaseKeys"); @@ -9963,10 +9987,6 @@ void initialize_menus() view_listener_t::addMenu(new LLToolsEnablePathfindingRebakeRegion(), "Tools.EnablePathfindingRebakeRegion"); view_listener_t::addMenu(new LLToolsCheckSelectionLODMode(), "Tools.ToolsCheckSelectionLODMode"); - // Help menu - // most items use the ShowFloater method - view_listener_t::addMenu(new LLToggleHowTo(), "Help.ToggleHowTo"); - // Advanced menu view_listener_t::addMenu(new LLAdvancedToggleConsole(), "Advanced.ToggleConsole"); view_listener_t::addMenu(new LLAdvancedCheckConsole(), "Advanced.CheckConsole"); diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 812ba765511..1f1ff30b8f0 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -418,7 +418,26 @@ void send_complete_agent_movement(const LLHost& sim_host) msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); msg->addU32Fast(_PREHASH_CircuitCode, msg->mOurCircuitCode); - msg->sendReliable(sim_host); + + // build a lambda to be used as callback on ACK or timeout + void (*complete_agent_movement_callback)(void**, S32) = [](void**, S32 result) + { + if(LLApp::isExiting()) return; + if (result != LL_ERR_NOERR) + { + LL_WARNS("Messaging") << "CompleteAgentMovement failed with err=" << result << LL_ENDL; + } + }; + + // We use same retry strategy as UseCircuitCode because this is a crucial message + // that MUST arrive else we'll suffer a failed login/teleport/region-cross + msg->sendReliable( + sim_host, + gSavedSettings.getS32("UseCircuitCodeMaxRetries"), + false, + (F32Seconds)gSavedSettings.getF32("UseCircuitCodeTimeout"), + complete_agent_movement_callback, + NULL); } void process_logout_reply(LLMessageSystem* msg, void**) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 7c26cb3c9fa..57915cca85a 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -4034,6 +4034,11 @@ U32 LLViewerObject::getTriangleCount(S32* vcount) const return 0; } +U32 LLViewerObject::getLODTriangleCount(S32 lod) +{ + return 0; +} + U32 LLViewerObject::getHighLODTriangleCount() { return 0; @@ -4055,6 +4060,27 @@ U32 LLViewerObject::recursiveGetTriangleCount(S32* vcount) const return total_tris; } +void LLViewerObject::recursiveGetLODTriangleCount(S32& high_lod, S32& medium_lod, S32& low_lod, S32& lowest_lod) +{ + high_lod = (S32)getLODTriangleCount(LLModel::LOD_HIGH); + medium_lod = (S32)getLODTriangleCount(LLModel::LOD_MEDIUM); + low_lod = (S32)getLODTriangleCount(LLModel::LOD_LOW); + lowest_lod = (S32)getLODTriangleCount(LLModel::LOD_IMPOSTOR); + LLViewerObject::const_child_list_t& child_list = getChildren(); + for (LLViewerObject::const_child_list_t::const_iterator iter = child_list.begin(); + iter != child_list.end(); ++iter) + { + LLViewerObject* childp = *iter; + if (childp) + { + high_lod += (S32)childp->getLODTriangleCount(LLModel::LOD_HIGH); + medium_lod += (S32)childp->getLODTriangleCount(LLModel::LOD_MEDIUM); + low_lod += (S32)childp->getLODTriangleCount(LLModel::LOD_LOW); + lowest_lod += (S32)childp->getLODTriangleCount(LLModel::LOD_IMPOSTOR); + } + } +} + // This is using the stored surface area for each volume (which // defaults to 1.0 for the case of everything except a sculpt) and // then scaling it linearly based on the largest dimension in the diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index 465e221ae47..54cdf046c72 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -427,10 +427,12 @@ class LLViewerObject virtual F32 getStreamingCost() const; virtual bool getCostData(LLMeshCostData& costs) const; virtual U32 getTriangleCount(S32* vcount = NULL) const; + virtual U32 getLODTriangleCount(S32 lod); virtual U32 getHighLODTriangleCount(); F32 recursiveGetScaledSurfaceArea() const; U32 recursiveGetTriangleCount(S32* vcount = NULL) const; + void recursiveGetLODTriangleCount(S32 &high_lod, S32 &medium_lod, S32 &low_lod, S32 &lowest_lod); void setObjectCost(F32 cost); F32 getObjectCost(); diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index 34e5cc9de82..f027a1eaa07 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -386,7 +386,7 @@ LLViewerObject* LLViewerObjectList::processObjectUpdateFromCache(LLVOCacheEntry* objectp->setLastUpdateType(OUT_FULL_COMPRESSED); //newly cached objectp->setLastUpdateCached(true); } - LLVOAvatar::cullAvatarsByPixelArea(); + LLVOAvatar::setCullNeedsUpdate(); return objectp; } @@ -683,7 +683,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys, objectp->setLastUpdateType(update_type); } - LLVOAvatar::cullAvatarsByPixelArea(); + LLVOAvatar::setCullNeedsUpdate(); } void LLViewerObjectList::processCompressedObjectUpdate(LLMessageSystem *mesgsys, diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index f31befd1ab4..5f73d8e9012 100755 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -1768,6 +1768,11 @@ void LLViewerRegion::killInvisibleObjects(F32 max_time) if(iter == mImpl->mActiveSet.end()) { iter = mImpl->mActiveSet.begin(); + if (iter == mImpl->mActiveSet.end()) + { + // Set became empty + break; + } } if((*iter)->getParentID() > 0) { @@ -3202,7 +3207,26 @@ void LLViewerRegion::unpackRegionHandshake() flags |= 0x00000002; //set the bit 1 to be 1 to tell sim the cache file is empty, no need to send cache probes. } msg->addU32("Flags", flags ); - msg->sendReliable(host); + + // build a lambda to be used as callback on ACK or timeout + void (*region_handshake_reply_callback)(void**, S32) = [](void**, S32 result) + { + if(LLApp::isExiting()) return; + if (result != LL_ERR_NOERR) + { + LL_WARNS("Messaging") << "RegionHandshakeReply failed with err=" << result << LL_ENDL; + } + }; + + // This is a crucial message for establishing a connection to a region + // (either the main region or a visible neighbor). + msg->sendReliable( + host, + gSavedSettings.getS32("UseCircuitCodeMaxRetries"), + false, + (F32Seconds)gSavedSettings.getF32("UseCircuitCodeTimeout"), + region_handshake_reply_callback, + NULL); mRegionTimer.reset(); //reset region timer. } diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 6490fe76a29..3b2796572be 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -791,7 +791,7 @@ void send_viewer_stats(bool include_preferences) LLSD &fail = body["stats"]["failures"]; fail["send_packet"] = (S32) gMessageSystem->mSendPacketFailureCount; - fail["dropped"] = (S32) gMessageSystem->mDroppedPackets; + fail["dropped"] = (S32) gMessageSystem->getTotalNumDroppedPackets(); fail["resent"] = (S32) gMessageSystem->mResentPackets; fail["failed_resends"] = (S32) gMessageSystem->mFailedResendPackets; fail["off_circuit"] = (S32) gMessageSystem->mOffCircuitPackets; diff --git a/indra/newview/llviewertexteditor.cpp b/indra/newview/llviewertexteditor.cpp index 95e34b4df16..a58d7546ce4 100644 --- a/indra/newview/llviewertexteditor.cpp +++ b/indra/newview/llviewertexteditor.cpp @@ -249,6 +249,25 @@ class LLEmbeddedItemSegment : public LLTextSegment /*virtual*/ bool canEdit() const { return false; } + /*virtual*/ bool handleRightMouseDown(S32 x, S32 y, MASK mask) + { + bool show_menu = !mEditor.hasSelection() + || (mEditor.mSelectionStart == mStart && mEditor.mSelectionEnd == mStart + 1); + if (show_menu && mEditor.getShowContextMenu()) + { + // User clicked an item, user expects the menu to be + // in 'context' for the item. Change selection to match. + // Todo: Might be better to 'smartly' deselect here + // and to have an object specific menu. + mEditor.setCursorPos(mStart + 1); + mEditor.mSelectionStart = mStart; + mEditor.mSelectionEnd = mStart + 1; + + mEditor.showContextMenu(x, y); + return true; + } + return false; + } /*virtual*/ bool handleHover(S32 x, S32 y, MASK mask) { diff --git a/indra/newview/llviewerthrottle.cpp b/indra/newview/llviewerthrottle.cpp index 3ccfbea6e23..90a60c7a941 100644 --- a/indra/newview/llviewerthrottle.cpp +++ b/indra/newview/llviewerthrottle.cpp @@ -45,8 +45,8 @@ using namespace LLOldEvents; const F32 MAX_FRACTIONAL = 1.5f; const F32 MIN_FRACTIONAL = 0.2f; -const F32 MIN_BANDWIDTH = 50.f; -const F32 MAX_BANDWIDTH = 6000.f; +const F32 MIN_BANDWIDTH = 50.f; // Kbps +const F32 MAX_BANDWIDTH = 6000.f; // Kbps const F32 STEP_FRACTIONAL = 0.1f; const F32 HIGH_BUFFER_LOAD_TRESHOLD = 1.f; const F32 LOW_BUFFER_LOAD_TRESHOLD = 0.8f; @@ -244,6 +244,8 @@ void LLViewerThrottle::sendToSim() const F32 LLViewerThrottle::getMaxBandwidthKbps() { + // Why are thse different than the constants with the same names higher up? + // Anybody know? -- Leviathan constexpr F32 MIN_BANDWIDTH = 100.0f; // 100 Kbps constexpr F32 MAX_BANDWIDTH = 10000.0f; // 10 Mbps @@ -309,6 +311,21 @@ void LLViewerThrottle::resetDynamicThrottle() void LLViewerThrottle::updateDynamicThrottle() { + // User configurable bandwidth settings are merely vague aspirations. Translating those + // to what should be sent to the servers is complicated. Servers will tend to spike data + // transmission upon arrival, packets will arrive faster than we can process them, and this + // can overflow the buffer, which causes packet loss. + // + // In an attempt to avoid catastrophe we periodically measure buffer load and packet loss + // and then transmit a modified desired bandwidth to the server. Unfortunately, this system + // does not work well for spikey data bursts because: + // (1) the response takes too long (up to 5 seconds) to kick in + // (2) once it starts it tends to ramp up too slowly + // (3) it doesn't know which region is providing the flood; it just assumes it is + // all from the main region + // + // TODO: fix those ^^^ problems + if (mUpdateTimer.getElapsedTimeF32() < DYNAMIC_UPDATE_DURATION) { return; @@ -347,5 +364,7 @@ void LLViewerThrottle::updateDynamicThrottle() LL_INFOS() << "Easing network throttle to " << mCurrentBandwidth << LL_ENDL; } + // Now that we've used mBufferLoadRate we reset it to zero because otherwise it only + // increases (see clamping behavior in setBufferLoadRate()). mBufferLoadRate = 0; } diff --git a/indra/newview/llviewerthrottle.h b/indra/newview/llviewerthrottle.h index ef898a97d75..7ff0a7d1f00 100644 --- a/indra/newview/llviewerthrottle.h +++ b/indra/newview/llviewerthrottle.h @@ -77,8 +77,8 @@ class LLViewerThrottle static const std::string sNames[TC_EOF]; protected: - F32 mMaxBandwidth; - F32 mCurrentBandwidth; + F32 mMaxBandwidth; // bps + F32 mCurrentBandwidth; // bps F32 mBufferLoadRate = 0; LLViewerThrottleGroup mCurrent; diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 0f3f24d1af4..ce8aed8d6f0 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -38,6 +38,7 @@ #include "llagent.h" #include "llagentcamera.h" +#include "llcallbacklist.h" #include "llcommandhandler.h" #include "llcommunicationchannel.h" #include "llfloaterreg.h" @@ -1466,12 +1467,68 @@ void LLViewerWindow::handleMouseLeave(LLWindow *window) LLToolTipMgr::instance().blockToolTips(); } +void LLViewerWindow::handlePreCloseRequest() +{ + // WINDOW THREAD! since we need this to act fast. + if (!LLApp::isExiting() && !LLApp::isStopped()) + { + LLAppViewer::instance()->createCloseRequestMarker(); + } +} + +void LLViewerWindow::handleCloseRequestCanceled() +{ + // WINDOW THREAD! since we need this to act fast. + if (!LLApp::isExiting() && !LLApp::isStopped()) + { + LLAppViewer::instance()->removeCloseRequestMarker(); + } +} + +void LLViewerWindow::handleSuspendRequest() +{ + static LLCachedControl os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); + if (os_hibernation_mode == 0) + { + LL_INFOS() << "Got a 'suspend' event from OS" << LL_ENDL; + // Viewer doesn't handle hibernation. + // Just send statistics. + LLAppViewer::instance()->sendViewerStatistics(false); + } + else + { + LL_INFOS() << "Got a 'suspend' event from OS, disconnecting" << LL_ENDL; + // Viewer is set to prevent hibernation if agent isn't away. + // If we got here, likely Agent 'went' away then viewer got + // a hibernation message. + // We have a limited timeframe. Sends stats then disconnect. + LLViewerRegion* region = gAgent.getRegion(); + if (region) + { + LLAppViewer::instance()->sendViewerStatistics(true); + LLAppViewer::instance()->metricsSend(!gDisconnected); + // Make sure to show a message. + LLAppViewer::instance()->forceDisconnect(LLTrans::getString("YouHaveBeenDisconnected")); + } + } +} + bool LLViewerWindow::handleCloseRequest(LLWindow *window, bool from_user) { if (!LLApp::isExiting() && !LLApp::isStopped()) { if (from_user) { + // Task naamger kills viewer after 1 second, 3 seconds + // is overkill, but decided to be on a safe side. + doAfterInterval([]() + { + // if user quits, marker will be cleaned by cleanup, + // if user cancels quit, marker will be cleaned here, + // but if task manager kills us, marker stays. + LLAppViewer::instance()->removeCloseRequestMarker(); + }, 3.0f); + // User has indicated they want to close, but we may need to ask // about modified documents. LLAppViewer::instance()->userQuit(); @@ -1492,6 +1549,9 @@ bool LLViewerWindow::handleSessionExit(LLWindow* window) { // Viewer received WM_ENDSESSION and app will be killed soon if it doesn't respond LLAppViewer* app = LLAppViewer::instance(); + // Normally we'd include preferences, but serializing them can be expensive. + // There is also a chance this won't be processed if the logout request arrives first. + app->sendViewerStatistics(false /*no preferences*/); app->sendSimpleLogoutRequest(); app->earlyExitNoNotify(); diff --git a/indra/newview/llviewerwindow.h b/indra/newview/llviewerwindow.h index ec28a3fc4aa..b68956d853b 100644 --- a/indra/newview/llviewerwindow.h +++ b/indra/newview/llviewerwindow.h @@ -202,6 +202,9 @@ class LLViewerWindow : public LLWindowCallbacks /*virtual*/ bool handleUnicodeChar(llwchar uni_char, MASK mask); // NOT going to handle extended /*virtual*/ bool handleMouseDown(LLWindow *window, LLCoordGL pos, MASK mask); /*virtual*/ bool handleMouseUp(LLWindow *window, LLCoordGL pos, MASK mask); + /*virtual*/ void handlePreCloseRequest(); + /*virtual*/ void handleCloseRequestCanceled(); + /*virtual*/ void handleSuspendRequest(); /*virtual*/ bool handleCloseRequest(LLWindow *window, bool from_user); /*virtual*/ bool handleSessionExit(LLWindow* window); /*virtual*/ void handleQuit(LLWindow *window); diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 2f39a76156c..cd71a2c660c 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -600,6 +600,8 @@ bool LLVOAvatar::sLimitNonImpostors = false; // True unless RenderAvatarMaxNonIm F32 LLVOAvatar::sRenderDistance = 256.f; S32 LLVOAvatar::sNumVisibleAvatars = 0; S32 LLVOAvatar::sNumLODChangesThisFrame = 0; +bool LLVOAvatar::sAvatarCullNeedsUpdate = true; +F64 LLVOAvatar::sLastCullUpdateTime = 0.0; const LLUUID LLVOAvatar::sStepSoundOnLand("e8af4a28-aa83-4310-a7c4-c047e15ea0df"); const LLUUID LLVOAvatar::sStepSounds[LL_MCODE_END] = @@ -2767,6 +2769,28 @@ void LLVOAvatar::idleUpdate(LLAgent &agent, const F64 &time) // force immediate pixel area update on avatars using last frames data (before drawable or camera updates) setPixelAreaAndAngle(gAgent); + if (!isSelf()) + { + F32 current_pixel_area = getPixelArea(); + if (mLastCulledPixelArea >= 0.f) + { + // Avoid rapidly switching two avatars back and forth between ranks. + // And update frequency reduction + F32 pixel_area_change = fabsf(current_pixel_area - mLastCulledPixelArea) / mLastCulledPixelArea; + if (pixel_area_change > 0.1f) // 10% threshold + { + sAvatarCullNeedsUpdate = true; + mLastCulledPixelArea = current_pixel_area; + } + } + else + { + // First frame + sAvatarCullNeedsUpdate = true; + mLastCulledPixelArea = current_pixel_area; + } + } + // force asynchronous drawable update if(mDrawable.notNull()) { @@ -5132,9 +5156,11 @@ void LLVOAvatar::updateVisibility() LL_DEBUGS("AvatarRender") << "visible was " << mVisible << " now " << visible << LL_ENDL; } + if (mVisible != visible) + { + setCullNeedsUpdate(); + } mVisible = visible; - - mVisibilityPreference = visible ? getPixelArea() : 0; } // private @@ -9941,7 +9967,7 @@ void LLVOAvatar::applyParsedAppearanceMessage(LLAppearanceMessageContents& conte setCompositeUpdatesEnabled( true ); // If all of the avatars are completely baked, release the global image caches to conserve memory. - cullAvatarsByPixelArea(); + setCullNeedsUpdate(); if (isSelf()) { @@ -10601,39 +10627,71 @@ S32 LLVOAvatar::getUnbakedPixelAreaRank() return 0; } -// static +// static, gets called once per frame from updateApparentAngles. void LLVOAvatar::cullAvatarsByPixelArea() { - LLCharacter::sInstances.sort([](LLCharacter* lhs, LLCharacter* rhs) - { - return ((LLVOAvatar*)lhs)->mVisibilityPreference > ((LLVOAvatar*)rhs)->mVisibilityPreference; - }); + F64 current_time = LLFrameTimer::getElapsedSeconds(); + bool needs_resort = sAvatarCullNeedsUpdate || ((current_time - sLastCullUpdateTime) >= 1.0); - // Update the avatars that have changed status - U32 rank = 2; // Rank 1 is reserved for self. - for (LLCharacter* character : LLCharacter::sInstances) + if (needs_resort) { - LLVOAvatar* inst = (LLVOAvatar*)character; - bool culled = !inst->isSelf() && !inst->isFullyBaked(); - - if (inst->mCulled != culled) + LLCharacter::sInstances.sort([](LLCharacter* lhs, LLCharacter* rhs) { - inst->mCulled = culled; - LL_DEBUGS() << "avatar " << inst->getID() << (culled ? " start culled" : " start not culled" ) << LL_ENDL; - inst->updateMeshTextures(); - } + LLVOAvatar* lhs_av = (LLVOAvatar*)lhs; + LLVOAvatar* rhs_av = (LLVOAvatar*)rhs; + if (lhs_av->mVisible != rhs_av->mVisible) + { + return lhs_av->mVisible; + } + // Sort by pixel area in descending order (larger pixel area = higher priority) + return lhs_av->getPixelArea() > rhs_av->getPixelArea(); + }); - if (inst->isSelf()) - { - inst->setVisibilityRank(1); - } - else if (inst->mDrawable.notNull() && inst->mDrawable->isVisible()) + // Update the avatars that have changed status + U32 rank = 2; // Rank 1 is reserved for self. + for (LLCharacter* character : LLCharacter::sInstances) { - inst->setVisibilityRank(rank++); + LLVOAvatar* inst = (LLVOAvatar*)character; + bool culled = !inst->isSelf() && !inst->isFullyBaked(); + + if (inst->mCulled != culled) + { + inst->mCulled = culled; + LL_DEBUGS() << "avatar " << inst->getID() << (culled ? " start culled" : " start not culled" ) << LL_ENDL; + inst->updateMeshTextures(); + } + + if (inst->isSelf()) + { + inst->setVisibilityRank(1); + } + else if (inst->mDrawable.notNull() && inst->mDrawable->isVisible()) + { + inst->setVisibilityRank(rank++); + } + else + { + inst->setVisibilityRank(sMaxNonImpostors * 5); + } + inst->mLastCulledPixelArea = inst->getPixelArea(); } - else + sAvatarCullNeedsUpdate = false; + sLastCullUpdateTime = current_time; + } + else + { + for (LLCharacter* character : LLCharacter::sInstances) { - inst->setVisibilityRank(sMaxNonImpostors * 5); + // Todo: this can be optimized by tracking baked's callbacks + LLVOAvatar* inst = (LLVOAvatar*)character; + bool culled = !inst->isSelf() && !inst->isFullyBaked(); + + if (inst->mCulled != culled) + { + inst->mCulled = culled; + LL_DEBUGS() << "avatar " << inst->getID() << (culled ? " start culled" : " start not culled") << LL_ENDL; + inst->updateMeshTextures(); + } } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 580d6ec911b..3f33dc54b81 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -697,7 +697,6 @@ class LLVOAvatar : protected: void updateVisibility(); private: - F32 mVisibilityPreference; U32 mVisibilityRank; bool mVisible; @@ -765,10 +764,13 @@ class LLVOAvatar : // Culling //-------------------------------------------------------------------- public: + static void setCullNeedsUpdate() { sAvatarCullNeedsUpdate = true; } static void cullAvatarsByPixelArea(); bool isCulled() const { return mCulled; } private: bool mCulled; + static bool sAvatarCullNeedsUpdate; + static F64 sLastCullUpdateTime; // Time of last cull update //-------------------------------------------------------------------- // Constants @@ -1277,7 +1279,8 @@ class LLVOAvatar : private: F32 mMinPixelArea; F32 mMaxPixelArea; - F32 mAdjustedPixelArea; + F32 mAdjustedPixelArea = 0.f; + F32 mLastCulledPixelArea = -1.f; // Pixel area when last culled, for tracking significant changes std::string mDebugText; std::string mBakedTextureDebugText; diff --git a/indra/newview/llvoicechannel.cpp b/indra/newview/llvoicechannel.cpp index 0852258994a..6a5e80365b0 100644 --- a/indra/newview/llvoicechannel.cpp +++ b/indra/newview/llvoicechannel.cpp @@ -128,6 +128,12 @@ void LLVoiceChannel::onChange(EStatusType type, const LLSD& channelInfo, bool pr { mChannelInfo = channelInfo; } + + if (!LLVoiceClient::instanceExists()) + { + return; + } + if (!LLVoiceClient::getInstance()->compareChannels(mChannelInfo, channelInfo)) { return; diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 3b98ca49ef0..62cda9d8356 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -1678,6 +1678,10 @@ void LLWebRTCVoiceClient::setVoiceVolume(F32 volume) void LLWebRTCVoiceClient::predSetSpeakerVolume(const LLWebRTCVoiceClient::sessionStatePtr_t &session, F32 volume) { + if (session->mShuttingDown) + { + return; + } session->setSpeakerVolume(volume); } @@ -1707,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; @@ -1906,6 +1918,10 @@ LLWebRTCVoiceClient::sessionState::sessionState() : void LLWebRTCVoiceClient::predUpdateOwnVolume(const LLWebRTCVoiceClient::sessionStatePtr_t &session, F32 audio_level) { + if (session->mShuttingDown) + { + return; + } participantStatePtr_t participant = session->findParticipantByID(gAgentID); if (participant) { @@ -1934,9 +1950,16 @@ void LLWebRTCVoiceClient::sessionState::sendData(const std::string &data) void LLWebRTCVoiceClient::sessionState::setMuteMic(bool muted) { mMuted = muted; + if (mShuttingDown) + { + return; + } for (auto &connection : mWebRTCConnections) { - connection->setMuteMic(muted); + if (!connection->isShuttingDown()) + { + connection->setMuteMic(muted); + } } } @@ -1945,7 +1968,10 @@ void LLWebRTCVoiceClient::sessionState::setSpeakerVolume(F32 volume) mSpeakerVolume = volume; for (auto &connection : mWebRTCConnections) { - connection->setSpeakerVolume(volume); + if (!connection->isShuttingDown()) + { + connection->setSpeakerVolume(volume); + } } } @@ -1957,7 +1983,10 @@ void LLWebRTCVoiceClient::sessionState::setUserVolume(const LLUUID &id, F32 volu } for (auto &connection : mWebRTCConnections) { - connection->setUserVolume(id, volume); + if (!connection->isShuttingDown()) + { + connection->setUserVolume(id, volume); + } } } @@ -1969,7 +1998,10 @@ void LLWebRTCVoiceClient::sessionState::setUserMute(const LLUUID &id, bool mute) } for (auto &connection : mWebRTCConnections) { - connection->setUserMute(id, mute); + if (!connection->isShuttingDown()) + { + connection->setUserMute(id, mute); + } } } /*static*/ diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 3b41ccb6fc3..076e5eb22e8 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -4445,7 +4445,7 @@ U32 LLVOVolume::getTriangleCount(S32* vcount) const return count; } -U32 LLVOVolume::getHighLODTriangleCount() +U32 LLVOVolume::getLODTriangleCount(S32 lod) { U32 ret = 0; @@ -4453,16 +4453,16 @@ U32 LLVOVolume::getHighLODTriangleCount() if (!isSculpted()) { - LLVolume* ref = LLPrimitive::getVolumeManager()->refVolume(volume->getParams(), 3); + LLVolume* ref = LLPrimitive::getVolumeManager()->refVolume(volume->getParams(), lod); ret = ref->getNumTriangles(); LLPrimitive::getVolumeManager()->unrefVolume(ref); } else if (isMesh()) { - LLVolume* ref = LLPrimitive::getVolumeManager()->refVolume(volume->getParams(), 3); + LLVolume* ref = LLPrimitive::getVolumeManager()->refVolume(volume->getParams(), lod); if (!ref->isMeshAssetLoaded() || ref->getNumVolumeFaces() == 0) { - gMeshRepo.loadMesh(this, volume->getParams(), LLModel::LOD_HIGH); + gMeshRepo.loadMesh(this, volume->getParams(), lod); } ret = ref->getNumTriangles(); LLPrimitive::getVolumeManager()->unrefVolume(ref); @@ -4475,6 +4475,11 @@ U32 LLVOVolume::getHighLODTriangleCount() return ret; } +U32 LLVOVolume::getHighLODTriangleCount() +{ + return getLODTriangleCount(LLModel::LOD_HIGH); +} + //static void LLVOVolume::preUpdateGeom() { diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h index b6044bc3190..00d6138512e 100644 --- a/indra/newview/llvovolume.h +++ b/indra/newview/llvovolume.h @@ -155,6 +155,7 @@ class LLVOVolume : public LLViewerObject /*virtual*/ bool getCostData(LLMeshCostData& costs) const override; /*virtual*/ U32 getTriangleCount(S32* vcount = NULL) const override; + /*virtual*/ U32 getLODTriangleCount(S32 lod) override; /*virtual*/ U32 getHighLODTriangleCount() override; /*virtual*/ bool lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end, S32 face = -1, // which face to check, -1 = ALL_SIDES diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp index d02694de7dc..b09c2d00bb2 100644 --- a/indra/newview/llworld.cpp +++ b/indra/newview/llworld.cpp @@ -794,10 +794,10 @@ void LLWorld::updateNetStats() S32 packets_in = gMessageSystem->mPacketsIn - mLastPacketsIn; S32 packets_out = gMessageSystem->mPacketsOut - mLastPacketsOut; - S32 packets_lost = gMessageSystem->mDroppedPackets - mLastPacketsLost; + S32 packets_lost = gMessageSystem->mLostPackets - mLastPacketsLost; - F64Bits actual_in_bits(gMessageSystem->mPacketRing.getAndResetActualInBits()); - F64Bits actual_out_bits(gMessageSystem->mPacketRing.getAndResetActualOutBits()); + F64Bits actual_in_bits(gMessageSystem->getAndResetActualInBits()); + F64Bits actual_out_bits(gMessageSystem->getAndResetActualOutBits()); add(LLStatViewer::MESSAGE_SYSTEM_DATA_IN, actual_in_bits); add(LLStatViewer::MESSAGE_SYSTEM_DATA_OUT, actual_out_bits); @@ -806,16 +806,17 @@ void LLWorld::updateNetStats() add(LLStatViewer::PACKETS_OUT, packets_out); add(LLStatViewer::PACKETS_LOST, packets_lost); - F32 total_packets_in = (F32)LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_IN); + LLTrace::Recording& recording = LLViewerStats::instance().getRecording(); + F32 total_packets_in = (F32)recording.getSum(LLStatViewer::PACKETS_IN); if (total_packets_in > 0.f) { - F32 total_packets_lost = (F32)LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_LOST); + F32 total_packets_lost = (F32)recording.getSum(LLStatViewer::PACKETS_LOST); sample(LLStatViewer::PACKETS_LOST_PERCENT, LLUnits::Ratio::fromValue((F32)total_packets_lost/(F32)total_packets_in)); } mLastPacketsIn = gMessageSystem->mPacketsIn; mLastPacketsOut = gMessageSystem->mPacketsOut; - mLastPacketsLost = gMessageSystem->mDroppedPackets; + mLastPacketsLost = gMessageSystem->mLostPackets; } @@ -838,7 +839,7 @@ void LLWorld::printPacketsLost() << " packets lost: " << cdp->getPacketsLost() << LL_ENDL; } } - LL_INFOS() << "Packets dropped by Packet Ring: " << gMessageSystem->mPacketRing.getNumDroppedPackets() << LL_ENDL; + LL_INFOS() << "Packets dropped by Packet Ring: " << gMessageSystem->getTotalNumDroppedPackets() << LL_ENDL; } void LLWorld::processCoarseUpdate(LLMessageSystem* msg, void** user_data) diff --git a/indra/newview/res/viewerRes.rc b/indra/newview/res/viewerRes.rc index dc2ba5f1719..c9ca7585558 100755 --- a/indra/newview/res/viewerRes.rc +++ b/indra/newview/res/viewerRes.rc @@ -157,7 +157,7 @@ BEGIN VALUE "FileDescription", "Second Life" VALUE "FileVersion", "${VIEWER_VERSION_MAJOR}.${VIEWER_VERSION_MINOR}.${VIEWER_VERSION_PATCH}.${VIEWER_VERSION_REVISION}" VALUE "InternalName", "Second Life" - VALUE "LegalCopyright", "Copyright (c) 2020, Linden Research, Inc." + VALUE "LegalCopyright", "Copyright (c) 2026, Linden Research, Inc." VALUE "OriginalFilename", "SecondLife.exe" VALUE "ProductName", "Second Life" VALUE "ProductVersion", "${VIEWER_VERSION_MAJOR}.${VIEWER_VERSION_MINOR}.${VIEWER_VERSION_PATCH}.${VIEWER_VERSION_REVISION}" diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml index 1f4b12fc914..9a83176f261 100644 --- a/indra/newview/skins/default/textures/textures.xml +++ b/indra/newview/skins/default/textures/textures.xml @@ -143,7 +143,6 @@ with the same filename but different name - diff --git a/indra/newview/skins/default/textures/toolbar_icons/howto.png b/indra/newview/skins/default/textures/toolbar_icons/howto.png deleted file mode 100644 index 8594d711133..00000000000 Binary files a/indra/newview/skins/default/textures/toolbar_icons/howto.png and /dev/null differ diff --git a/indra/newview/skins/default/xui/da/floater_live_lsleditor.xml b/indra/newview/skins/default/xui/da/floater_live_lsleditor.xml index 0cc13fd7361..c6a78fff4e7 100644 --- a/indra/newview/skins/default/xui/da/floater_live_lsleditor.xml +++ b/indra/newview/skins/default/xui/da/floater_live_lsleditor.xml @@ -1,7 +1,7 @@ - Du kan ikke se eller redigere dette script, da det er sat til "no copy". Du skal bruge fulde rettigheder for at kunne se og redigere et script i dette objekt. + Du kan ikke se eller redigere dette script, da det er sat til "no copy" eller "no modify". Du skal have disse tilladelser for at kunne se eller redigere et script. Kører diff --git a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml index 7be9a9d5552..981c35800f0 100644 --- a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml @@ -45,4 +45,12 @@ + + Forhindr OS i at gå i dvale, hvis du ikke er væk: + + + + + + diff --git a/indra/newview/skins/default/xui/da/panel_script_ed.xml b/indra/newview/skins/default/xui/da/panel_script_ed.xml index 3dec4bf101b..ae073300b1e 100644 --- a/indra/newview/skins/default/xui/da/panel_script_ed.xml +++ b/indra/newview/skins/default/xui/da/panel_script_ed.xml @@ -4,7 +4,7 @@ Henter... - Du kan ikke se eller rette dette script, da det er sat til "no copy". Du skal have fulde rettigheder for at se eller rette et script i et objekt. + Du kan ikke se eller redigere dette script, da det er sat til "no copy" eller "no modify". Du skal have disse tilladelser for at kunne se eller redigere et script. Offentlige objekter kan ikke afvikle scripts diff --git a/indra/newview/skins/default/xui/de/floater_how_to.xml b/indra/newview/skins/default/xui/de/floater_how_to.xml deleted file mode 100644 index caea221f83f..00000000000 --- a/indra/newview/skins/default/xui/de/floater_how_to.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/indra/newview/skins/default/xui/de/floater_live_lsleditor.xml b/indra/newview/skins/default/xui/de/floater_live_lsleditor.xml index ae2dd4db678..5b44c44b289 100644 --- a/indra/newview/skins/default/xui/de/floater_live_lsleditor.xml +++ b/indra/newview/skins/default/xui/de/floater_live_lsleditor.xml @@ -1,7 +1,7 @@ - Dieses Skript kann nicht angezeigt oder bearbeitet werden, da als Berechtigung „kein kopieren" festgelegt wurde. Um ein Skript innerhalb eines Objektes anzuzeigen oder zu bearbeiten, benötigen Sie die vollständige Berechtigung. + Dieses Skript kann nicht angezeigt oder bearbeitet werden, da als Berechtigung „kein kopieren" oder „keine Änderungen" festgelegt wurde. Um ein Skript anzuzeigen oder zu bearbeiten, benötigen Sie diese Berechtigungen. Läuft diff --git a/indra/newview/skins/default/xui/de/notifications.xml b/indra/newview/skins/default/xui/de/notifications.xml index 6ad71e0ad16..40b34d99baa 100644 --- a/indra/newview/skins/default/xui/de/notifications.xml +++ b/indra/newview/skins/default/xui/de/notifications.xml @@ -1127,7 +1127,6 @@ Falls Sie [SECOND_LIFE] zum ersten Mal verwenden, müssen Sie zuerst ein Konto e Ihr Avatar erscheint jeden Moment. Benutzen Sie die Pfeiltasten, um sich fortzubewegen. -Drücken Sie F1 für Hilfe oder für weitere Informationen über [SECOND_LIFE]. Bitte wählen Sie einen männlichen oder weiblichen Avatar. Sie können sich später noch umentscheiden. diff --git a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml index a8509cabac4..ef7ac09dbae 100644 --- a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml @@ -36,4 +36,12 @@ Proxy-Einstellungen: + + Prevent OS from hibernating if not Away: + + + + + + diff --git a/indra/newview/skins/default/xui/en/panel_preferences_sound.xml b/indra/newview/skins/default/xui/en/panel_preferences_sound.xml index 1dcbf4e6660..18fd7fe2d32 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_sound.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_sound.xml @@ -35,7 +35,7 @@ name="System Volume" show_text="false" slider_label.halign="right" - top="10" + top="12" volume="true" width="300"> @@ -379,6 +380,7 @@ layout="topleft" height="15" left="23" + top_delta="32" width="180" name="media_firstinteract_label" halign="right"> @@ -435,11 +437,12 @@ enabled_control="AudioStreamingMedia" value="true" follows="left|top" + layout="topleft" tool_tip="Uncheck this to hide media attached to other avatars nearby" label="Play media attached to other avatars" left="23" width="15" - top_delta="30" + top_delta="35" height="15"/> - - Microphone Noise Suppression - - - - - - - - - - Hear voice from - - - - - - - - - - - - - diff --git a/indra/newview/skins/default/xui/en/panel_preferences_voice.xml b/indra/newview/skins/default/xui/en/panel_preferences_voice.xml new file mode 100644 index 00000000000..6d8e39e03cf --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_preferences_voice.xml @@ -0,0 +1,222 @@ + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/panel_script_ed.xml b/indra/newview/skins/default/xui/en/panel_script_ed.xml index f8761d2b24d..ef84a25a502 100644 --- a/indra/newview/skins/default/xui/en/panel_script_ed.xml +++ b/indra/newview/skins/default/xui/en/panel_script_ed.xml @@ -14,7 +14,7 @@ - You can not view or edit this script, since it has been set as "no copy". You need full permissions to view or edit a script inside an object. + You can not view or edit this script, since it has been set as "no copy" or "no modify". You need these permissions to view or edit a script. diff --git a/indra/newview/skins/default/xui/en/panel_settings_sky_clouds.xml b/indra/newview/skins/default/xui/en/panel_settings_sky_clouds.xml index 23bbf45e884..f39f8cb782d 100644 --- a/indra/newview/skins/default/xui/en/panel_settings_sky_clouds.xml +++ b/indra/newview/skins/default/xui/en/panel_settings_sky_clouds.xml @@ -138,7 +138,9 @@ min_val_x="-30" max_val_x="30" min_val_y="-30" - max_val_y="30" + max_val_y="30" + increment_x="0.01f" + increment_y="0.01f" logarithmic="true"/> Device not loaded - - Input @@ -64,7 +42,7 @@ control_name="VoiceInputAudioDevice" follows="left|top" layout="topleft" - left_pad="0" + left_pad="25" max_chars="128" name="voice_input_device" top_delta="-5" @@ -76,9 +54,9 @@ follows="left|top" height="15" layout="topleft" - left_pad="30" name="Output" - top_delta="5" + top_delta="35" + left="18" width="60"> Output @@ -87,7 +65,7 @@ height="23" follows="left|top" layout="topleft" - left_pad="0" + left_pad="25" max_chars="128" name="voice_output_device" top_delta="-4" @@ -99,7 +77,7 @@ follows="left|top" height="16" layout="topleft" - left_delta="-300" + left="18" name="My volume label" top_pad="14" width="200"> diff --git a/indra/newview/skins/default/xui/en/sidepanel_item_info.xml b/indra/newview/skins/default/xui/en/sidepanel_item_info.xml index 40a88d41212..5f97f27db66 100644 --- a/indra/newview/skins/default/xui/en/sidepanel_item_info.xml +++ b/indra/newview/skins/default/xui/en/sidepanel_item_info.xml @@ -212,7 +212,7 @@ TestString PleaseIgnore Description: Toy Camera + Drag to reorder layers, or use the arrows Person (no name) Owner: @@ -598,7 +599,7 @@ http://secondlife.com/support for help fixing this problem. Invalid character [NR]: '[CH]' (should only be a digit or an alpha-numeric ASCII character or a punctuation with no space) Invalid character [NR]: '[CH]' (should only be a digit with no space) Invalid character [NR]: '[CH]' (should only be an ASCII character) - Invalid character [NR]: '[CH]' (should only be an ASCII character or a new line) + Character [CH] at position [NR] should be printable ASCII (excluding pipe) or newline - Didn't find what you're looking for? Try [secondlife:///app/search/all/[SEARCH_TERM] Search]. + No matching items. Didn't find what you're looking for? Try [secondlife:///app/inventory/filters Show filters]. You haven't marked any items as favorites. To add a place to your landmarks, click the star to the right of the location name. @@ -4207,7 +4208,6 @@ name="Command_360_Capture_Label">360 snapshot My Environments Gestures Grid status - Guidebook Inventory Map Marketplace @@ -4240,7 +4240,6 @@ name="Command_360_Capture_Tooltip">Capture a 360 equirectangular image My Environments Gestures for your avatar Show current Grid status - How to do common tasks View and use your belongings Map of the world Go shopping diff --git a/indra/newview/skins/default/xui/es/floater_how_to.xml b/indra/newview/skins/default/xui/es/floater_how_to.xml deleted file mode 100644 index 4a57dc36437..00000000000 --- a/indra/newview/skins/default/xui/es/floater_how_to.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/indra/newview/skins/default/xui/es/floater_live_lsleditor.xml b/indra/newview/skins/default/xui/es/floater_live_lsleditor.xml index 9672f910eae..9686dc8c2b3 100644 --- a/indra/newview/skins/default/xui/es/floater_live_lsleditor.xml +++ b/indra/newview/skins/default/xui/es/floater_live_lsleditor.xml @@ -1,7 +1,7 @@ - No puedes ver ni editar este script. Ha sido configurado como "no copiable". Necesitas todos los permisos para ver o editar un script que está dentro de un objeto. + No puedes ver ni editar este script. Ha sido configurado como "no copiable" o "no modificable". Necesitas estos permisos para ver o editar un script. Ejecutándose diff --git a/indra/newview/skins/default/xui/es/notifications.xml b/indra/newview/skins/default/xui/es/notifications.xml index 739391b9653..5dee9eb3a86 100644 --- a/indra/newview/skins/default/xui/es/notifications.xml +++ b/indra/newview/skins/default/xui/es/notifications.xml @@ -1119,7 +1119,6 @@ Puedes revisar tu conexión a Internet y volver a intentarlo en unos minutos, pu Tu personaje aparecerá en un momento. Para caminar, usa las teclas del cursor. -En cualquier momento, puedes pulsar la tecla F1 para conseguir ayuda o para aprender más acerca de [SECOND_LIFE]. Por favor, elige el avatar masculino o femenino. Puedes cambiar más adelante tu elección. diff --git a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml index 36888850c4e..27b3c407cfe 100644 --- a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml @@ -36,4 +36,12 @@ Configuración de proxy: