diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2101b9b5e83..d98fd4b531b 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,6 +2,14 @@ name: Build
on:
workflow_dispatch:
+ inputs:
+ installer_type:
+ description: 'Windows installer type'
+ type: choice
+ options:
+ - velopack
+ - nsis
+ default: 'velopack'
pull_request:
push:
branches: ["main", "release/*", "project/*"]
@@ -53,6 +61,20 @@ jobs:
relnotes: ${{ steps.which-branch.outputs.relnotes }}
imagename: ${{ steps.build.outputs.imagename }}
configuration: ${{ matrix.configuration }}
+ # Windows Velopack outputs (passed to sign-pkg-windows)
+ velopack_pack_id: ${{ steps.build.outputs.velopack_pack_id }}
+ velopack_pack_version: ${{ steps.build.outputs.velopack_pack_version }}
+ velopack_pack_title: ${{ steps.build.outputs.velopack_pack_title }}
+ velopack_main_exe: ${{ steps.build.outputs.velopack_main_exe }}
+ velopack_exclude: ${{ steps.build.outputs.velopack_exclude }}
+ velopack_icon: ${{ steps.build.outputs.velopack_icon }}
+ velopack_installer_base: ${{ steps.build.outputs.velopack_installer_base }}
+ # macOS Velopack outputs (passed to sign-pkg-mac)
+ velopack_mac_pack_id: ${{ steps.build.outputs.velopack_mac_pack_id }}
+ velopack_mac_pack_version: ${{ steps.build.outputs.velopack_mac_pack_version }}
+ velopack_mac_pack_title: ${{ steps.build.outputs.velopack_mac_pack_title }}
+ velopack_mac_main_exe: ${{ steps.build.outputs.velopack_mac_main_exe }}
+ velopack_mac_bundle_id: ${{ steps.build.outputs.velopack_mac_bundle_id }}
env:
AUTOBUILD_ADDRSIZE: 64
AUTOBUILD_BUILD_ID: ${{ github.run_id }}
@@ -84,6 +106,8 @@ jobs:
# Only set variants to the one configuration: don't let build.sh loop
# over variants, let GitHub distribute variants over multiple hosts.
variants: ${{ matrix.configuration }}
+ # Pass USE_VELOPACK to CMake when using Velopack installer (default) - Windows and macOS
+ autobuild_configure_parameters: ${{ (contains(matrix.runner, 'windows') || contains(matrix.runner, 'macos')) && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-- -DUSE_VELOPACK:BOOL=ON' || '' }}
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -126,6 +150,17 @@ jobs:
with:
token: ${{ github.token }}
+ - name: Setup .NET for Velopack
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Install Velopack CLI
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ shell: bash
+ run: dotnet tool install -g vpk
+
- name: Build
id: build
shell: bash
@@ -265,7 +300,7 @@ jobs:
- name: Upload executable
if: steps.build.outputs.viewer_app
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: "${{ steps.build.outputs.artifact }}-app"
path: |
@@ -275,13 +310,13 @@ jobs:
# artifact for that too.
- name: Upload symbol file
if: steps.build.outputs.symbolfile
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: "${{ steps.build.outputs.artifact }}-symbols"
path: ${{ steps.build.outputs.symbolfile }}
- name: Upload metadata
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: "${{ steps.build.outputs.artifact }}-metadata"
# emitted by build.sh, possibly multiple lines
@@ -289,7 +324,7 @@ jobs:
${{ steps.build.outputs.metadata }}
- name: Upload physics package
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
# should only be set for viewer-private
if: matrix.configuration == 'Release' && steps.build.outputs.physicstpv
with:
@@ -310,13 +345,21 @@ jobs:
steps:
- name: Sign and package Windows viewer
if: env.AZURE_KEY_VAULT_URI && env.AZURE_CERT_NAME && env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET && env.AZURE_TENANT_ID
- uses: secondlife/viewer-build-util/sign-pkg-windows@v2.0.4
+ uses: secondlife/viewer-build-util/sign-pkg-windows@v2.1.0
with:
vault_uri: "${{ env.AZURE_KEY_VAULT_URI }}"
cert_name: "${{ env.AZURE_CERT_NAME }}"
client_id: "${{ env.AZURE_CLIENT_ID }}"
client_secret: "${{ env.AZURE_CLIENT_SECRET }}"
tenant_id: "${{ env.AZURE_TENANT_ID }}"
+ installer_type: "${{ github.event.inputs.installer_type || 'velopack' }}"
+ velopack_pack_id: "${{ needs.build.outputs.velopack_pack_id }}"
+ velopack_pack_version: "${{ needs.build.outputs.velopack_pack_version }}"
+ velopack_pack_title: "${{ needs.build.outputs.velopack_pack_title }}"
+ velopack_main_exe: "${{ needs.build.outputs.velopack_main_exe }}"
+ velopack_exclude: "${{ needs.build.outputs.velopack_exclude }}"
+ velopack_icon: "${{ needs.build.outputs.velopack_icon }}"
+ velopack_installer_base: "${{ needs.build.outputs.velopack_installer_base }}"
sign-and-package-mac:
env:
@@ -349,7 +392,7 @@ jobs:
- name: Sign and package Mac viewer
if: env.SIGNING_CERT_MACOS && env.SIGNING_CERT_MACOS_IDENTITY && env.SIGNING_CERT_MACOS_PASSWORD && steps.note-creds.outputs.note_user && steps.note-creds.outputs.note_pass && steps.note-creds.outputs.note_team
- uses: secondlife/viewer-build-util/sign-pkg-mac@v2
+ uses: secondlife/viewer-build-util/sign-pkg-mac@v2.1.0
with:
channel: ${{ needs.build.outputs.viewer_channel }}
imagename: ${{ needs.build.outputs.imagename }}
@@ -359,6 +402,11 @@ jobs:
note_user: ${{ steps.note-creds.outputs.note_user }}
note_pass: ${{ steps.note-creds.outputs.note_pass }}
note_team: ${{ steps.note-creds.outputs.note_team }}
+ velopack_pack_id: "${{ needs.build.outputs.velopack_mac_pack_id }}"
+ velopack_pack_version: "${{ needs.build.outputs.velopack_mac_pack_version }}"
+ velopack_pack_title: "${{ needs.build.outputs.velopack_mac_pack_title }}"
+ velopack_main_exe: "${{ needs.build.outputs.velopack_mac_main_exe }}"
+ velopack_bundle_id: "${{ needs.build.outputs.velopack_mac_bundle_id }}"
post-windows-symbols:
env:
@@ -370,13 +418,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download viewer exe
- uses: actions/download-artifact@v7
+ uses: actions/download-artifact@v8
with:
name: Windows-app
path: _artifacts
- name: Download Windows Symbols
if: env.BUGSPLAT_DATABASE && env.SYMBOL_UPLOAD_CLIENT_ID
- uses: actions/download-artifact@v7
+ uses: actions/download-artifact@v8
with:
name: Windows-symbols
- name: Extract viewer pdb
@@ -386,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@2a0d2b8cf9c54c494144048f25da863d93a02ccd
with:
clientId: "${{ env.SYMBOL_UPLOAD_CLIENT_ID }}"
clientSecret: "${{ env.SYMBOL_UPLOAD_CLIENT_SECRET }}"
@@ -409,12 +457,12 @@ jobs:
steps:
- name: Download Mac Symbols
if: env.BUGSPLAT_DATABASE && env.SYMBOL_UPLOAD_CLIENT_ID
- uses: actions/download-artifact@v7
+ uses: actions/download-artifact@v8
with:
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@2a0d2b8cf9c54c494144048f25da863d93a02ccd
with:
clientId: "${{ env.SYMBOL_UPLOAD_CLIENT_ID }}"
clientSecret: "${{ env.SYMBOL_UPLOAD_CLIENT_SECRET }}"
@@ -431,14 +479,18 @@ jobs:
runs-on: ubuntu-latest
if: needs.setup.outputs.release_run
steps:
- - uses: actions/download-artifact@v7
+ - uses: actions/download-artifact@v8
with:
pattern: "*-installer"
- - uses: actions/download-artifact@v7
+ - uses: actions/download-artifact@v8
with:
pattern: "*-metadata"
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: "*-releases"
+
- name: Rename metadata
run: |
cp Windows-metadata/autobuild-package.xml Windows-autobuild-package.xml
@@ -464,12 +516,14 @@ jobs:
generate_release_notes: true
target_commitish: ${{ github.sha }}
append_body: true
- fail_on_unmatched_files: true
+ fail_on_unmatched_files: false
files: |
macOS-installer/*.dmg
Windows-installer/*.exe
*-autobuild-package.xml
*-viewer_version.txt
+ Windows-releases/*
+ macOS-releases/*
- name: post release URL
run: |
diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml
index 800f3c42d1c..1b6333a0b8d 100644
--- a/.github/workflows/cla.yaml
+++ b/.github/workflows/cla.yaml
@@ -23,4 +23,4 @@ jobs:
path-to-signatures: signatures.json
remote-organization-name: secondlife
remote-repository-name: cla-signatures
- allowlist: callum@mbp.localdomain,rye@lindenlab.com,rye,signal@lindenlab.com,dependabot*,bot*
+ allowlist: callum@mbp.localdomain,rye@lindenlab.com,rye,signal@lindenlab.com,dependabot*,bot*,copilot-swe-agent*
diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml
index 2922065f995..0f826222a05 100644
--- a/.github/workflows/tag-release.yaml
+++ b/.github/workflows/tag-release.yaml
@@ -21,7 +21,9 @@ on:
project:
description: "Project Name (used for channel name in project builds, and tag name for all builds)"
default: "hippo"
- # TODO - add an input for selecting another sha to build other than head of branch
+ tag_override:
+ description: "Override the tag name (optional). If the tag already exists, a numeric suffix is appended."
+ required: false
jobs:
tag-release:
@@ -34,7 +36,7 @@ jobs:
NIGHTLY_DATE=$(date --rfc-3339=date)
echo NIGHTLY_DATE=${NIGHTLY_DATE} >> ${GITHUB_ENV}
echo TAG_ID="$(echo ${{ github.sha }} | cut -c1-8)-${{ inputs.project || '${NIGHTLY_DATE}' }}" >> ${GITHUB_ENV}
- - name: Update Tag
+ - name: Create Tag
uses: actions/github-script@v8
with:
# use a real access token instead of GITHUB_TOKEN default.
@@ -44,9 +46,27 @@ jobs:
# this token will need to be renewed anually in January
github-token: ${{ secrets.LL_TAG_RELEASE_TOKEN }}
script: |
- github.rest.git.createRef({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref: "refs/tags/${{ env.VIEWER_CHANNEL }}#${{ env.TAG_ID }}",
- sha: context.sha
- })
+ const override = `${{ inputs.tag_override }}`.trim();
+ const baseTag = override || `${{ env.VIEWER_CHANNEL }}#${{ env.TAG_ID }}`;
+
+ // Try the base tag first, then append -2, -3, etc. if it already exists
+ let tag = baseTag;
+ for (let attempt = 1; ; attempt++) {
+ try {
+ await github.rest.git.createRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `refs/tags/${tag}`,
+ sha: context.sha
+ });
+ core.info(`Created tag: ${tag}`);
+ break;
+ } catch (e) {
+ if (e.status === 422 && attempt < 10) {
+ core.info(`Tag '${tag}' already exists, trying next suffix...`);
+ tag = `${baseTag}-${attempt + 1}`;
+ } else {
+ throw e;
+ }
+ }
+ }
diff --git a/autobuild.xml b/autobuild.xml
index 6e48cbf5093..e33bbf0f220 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -441,6 +441,64 @@
description
Spell checking dictionaries to bundled into the viewer
+ discord_sdk
+
dullahan
platforms
@@ -450,11 +508,11 @@
archive
hash
- 126e0fa4c16dfd433c9fb7d1d242da98f213d933
+ 61944d4471578eb9292a30c4f5c3eba1ef05059a
hash_algorithm
sha1
url
- https://github.com/secondlife/dullahan/releases/download/v1.24.0-CEF_139.0.40/dullahan-1.24.0.202510081737_139.0.40_g465474a_chromium-139.0.7258.139-darwin64-18353103947.tar.zst
+ https://github.com/secondlife/dullahan/releases/download/v1.29.0-CEF_146.0.12/dullahan-1.29.0.202604232348_146.0.12_g6214c8e_chromium-146.0.7680.179-darwin64-24864575166.tar.zst
name
darwin64
@@ -478,11 +536,11 @@
archive
hash
- 20de62c9e57d9e6539c1e2437ec4b46c3ca237bc
+ b80352ac3bb738dd38a55193850bb257b39903c3
hash_algorithm
sha1
url
- https://github.com/secondlife/dullahan/releases/download/v1.24.0-CEF_139.0.40/dullahan-1.24.0.202510081738_139.0.40_g465474a_chromium-139.0.7258.139-windows64-18353103947.tar.zst
+ https://github.com/secondlife/dullahan/releases/download/v1.29.0-CEF_146.0.12/dullahan-1.29.0.202604232349_146.0.12_g6214c8e_chromium-146.0.7680.179-windows64-24864575166.tar.zst
name
windows64
@@ -495,7 +553,7 @@
copyright
Copyright (c) 2017, Linden Research, Inc.
version
- 1.24.0.202510081737_139.0.40_g465474a_chromium-139.0.7258.139
+ 1.29.0.202604232348_146.0.12_g6214c8e_chromium-146.0.7680.179
name
dullahan
description
@@ -967,69 +1025,89 @@
name
jpegencoderbasic
- libjpeg-turbo
+ kdu
platforms
- windows64
+ darwin64
archive
+ creds
+ github
hash
- 10f14875ce5c7f5028217c8b7468733190fd333d
+ da318f0813e4126d90e35b22a8dce235e908707a
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-windows64-11968659895.tar.zst
+ https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381808
name
- windows64
+ darwin64
+
+ linux
+
+ archive
+
+ creds
+ github
+ hash
+ 85e294becce8b2ac5d2e5e052b0e21ff865d1108
+ hash_algorithm
+ sha1
+ url
+ https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381806
+
+ name
+ linux
linux64
archive
+ creds
+ github
hash
- d3b1b0fde28c8cf0c33fed167dba87bba5c6cc64
+ 1ba58cf884726dfdf02a7662d52f1befe3f16d44
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-linux64-11968659895.tar.zst
+ https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381812
name
linux64
- darwin64
+ windows64
archive
+ creds
+ github
hash
- 79e78cbaaec9a99c0ae4a5cdd4a98535c8fa3c6d
+ e8d693089b9ecd15b6644f13ada7ae7c317944df
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-darwin64-11968659895.tar.zst
+ https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381814
name
- darwin64
+ windows64
license
- libjpeg-turbo
+ Kakadu
license_file
- LICENSES/libjpeg-turbo.txt
+ LICENSES/kdu.txt
copyright
- Copyright (C)2009-2024 D. R. Commander. All Rights Reserved. Copyright (C)2015 Viktor Szathmáry. All Rights Reserved.
+ Kakadu software
version
- 3.0.4-r1
+ 8.4.1.11976899217
name
- libjpeg-turbo
- canonical_repo
- https://github.com/secondlife/3p-libjpeg-turbo
+ kdu
description
- JPEG encoding, decoding library
+ JPEG2000 library by Kakadu
- kdu
+ libhunspell
platforms
@@ -1037,14 +1115,12 @@
archive
- creds
- github
hash
- da318f0813e4126d90e35b22a8dce235e908707a
+ 91acd05f450162b07ca2f68094778c483d28128d
hash_algorithm
sha1
url
- https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381808
+ https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-darwin64-11968900321.tar.zst
name
darwin64
@@ -1053,14 +1129,12 @@
archive
- creds
- github
hash
- 1ba58cf884726dfdf02a7662d52f1befe3f16d44
+ 10e5b5d793c3c5cb5335dea89734302bda5a9f59
hash_algorithm
sha1
url
- https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381812
+ https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-linux64-11968900321.tar.zst
name
linux64
@@ -1069,49 +1143,31 @@
archive
- creds
- github
hash
- e8d693089b9ecd15b6644f13ada7ae7c317944df
+ 0f7b9c46dc4e81a6296e4836467f5fe52aa5761d
hash_algorithm
sha1
url
- https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381814
+ https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-windows64-11968900321.tar.zst
name
windows64
- linux
-
- archive
-
- creds
- github
- hash
- 85e294becce8b2ac5d2e5e052b0e21ff865d1108
- hash_algorithm
- sha1
- url
- https://api.github.com/repos/secondlife/3p-kdu/releases/assets/208381806
-
- name
- linux
-
license
- Kakadu
+ LGPL
license_file
- LICENSES/kdu.txt
+ LICENSES/hunspell.txt
copyright
- Kakadu software
+ LGPL 2.1
version
- 8.4.1.11976899217
+ 1.7.2.11968900321
name
- kdu
+ libhunspell
description
- JPEG2000 library by Kakadu
+ Spell checking library
- libhunspell
+ libjpeg-turbo
platforms
@@ -1120,11 +1176,11 @@
archive
hash
- 91acd05f450162b07ca2f68094778c483d28128d
+ 79e78cbaaec9a99c0ae4a5cdd4a98535c8fa3c6d
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-darwin64-11968900321.tar.zst
+ https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-darwin64-11968659895.tar.zst
name
darwin64
@@ -1134,11 +1190,11 @@
archive
hash
- 10e5b5d793c3c5cb5335dea89734302bda5a9f59
+ d3b1b0fde28c8cf0c33fed167dba87bba5c6cc64
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-linux64-11968900321.tar.zst
+ https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-linux64-11968659895.tar.zst
name
linux64
@@ -1148,28 +1204,30 @@
archive
hash
- 0f7b9c46dc4e81a6296e4836467f5fe52aa5761d
+ 10f14875ce5c7f5028217c8b7468733190fd333d
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-libhunspell/releases/download/v1.7.2-r2/libhunspell-1.7.2.11968900321-windows64-11968900321.tar.zst
+ https://github.com/secondlife/3p-libjpeg-turbo/releases/download/v3.0.4-r1/libjpeg_turbo-3.0.4-r1-windows64-11968659895.tar.zst
name
windows64
license
- LGPL
+ libjpeg-turbo
license_file
- LICENSES/hunspell.txt
+ LICENSES/libjpeg-turbo.txt
copyright
- LGPL 2.1
+ Copyright (C)2009-2024 D. R. Commander. All Rights Reserved. Copyright (C)2015 Viktor Szathmáry. All Rights Reserved.
version
- 1.7.2.11968900321
+ 3.0.4-r1
name
- libhunspell
+ libjpeg-turbo
+ canonical_repo
+ https://github.com/secondlife/3p-libjpeg-turbo
description
- Spell checking library
+ JPEG encoding, decoding library
libndofdev
@@ -2134,19 +2192,19 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
platforms
- windows64
+ darwin64
archive
hash
- 3cccc3e3f3137066c286270b35abc00ee0c0bb0c
+ a9bfabec63a987bd34bcfdc295b928bd0696e1d7
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-openxr/releases/download/v1.1.40-r1/openxr-1.1.40-r1-windows64-10710818432.tar.zst
+ https://github.com/secondlife/3p-openxr/releases/download/v1.1.40-r1/openxr-1.1.40-r1-darwin64-10710818432.tar.zst
name
- windows64
+ darwin64
linux64
@@ -2162,19 +2220,19 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
name
linux64
- darwin64
+ windows64
archive
hash
- a9bfabec63a987bd34bcfdc295b928bd0696e1d7
+ 3cccc3e3f3137066c286270b35abc00ee0c0bb0c
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-openxr/releases/download/v1.1.40-r1/openxr-1.1.40-r1-darwin64-10710818432.tar.zst
+ https://github.com/secondlife/3p-openxr/releases/download/v1.1.40-r1/openxr-1.1.40-r1-windows64-10710818432.tar.zst
name
- darwin64
+ windows64
license
@@ -2314,6 +2372,46 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
name
threejs
+ tinyexr
+
+ platforms
+
+ common
+
+ archive
+
+ hash
+ a9649f85e20c0b83acfd7b02ca19c1dcdd15f01e
+ hash_algorithm
+ sha1
+ url
+ https://github.com/secondlife/3p-tinyexr/releases/download/v1.0.9-5e8947c/tinyexr-1.0.9-5e8947c-common-10475846787.tar.zst
+
+ name
+ common
+
+
+ license
+ 3-clause BSD
+ license_file
+ LICENSES/tinyexr_license.txt
+ copyright
+ Copyright (c) 2014 - 2021, Syoyo Fujita and many contributors.
+ version
+ 1.0.9-5e8947c
+ name
+ tinyexr
+ vcs_branch
+ dependabot/github_actions/secondlife/action-autobuild-4
+ vcs_revision
+ 4dc4d1d90d82a22843e2adf5130f9ecb5ee5769e
+ vcs_url
+ https://github.com/secondlife/3p-tinyexr
+ description
+ tinyexr import library
+ source_type
+ git
+
tinygltf
platforms
@@ -2370,33 +2468,33 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
name
darwin64
- windows64
+ linux64
archive
hash
- b46cef5646a8d0471ab6256fe5119220fa238772
+ beab04c9ea6036b1851a485b65c66cf6a38f0be4
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-tracy/releases/download/v0.11.1-r1/tracy-v0.11.1.11706699176-windows64-11706699176.tar.zst
+ https://github.com/secondlife/3p-tracy/releases/download/v0.11.1-r1/tracy-v0.11.1.11706699176-linux64-11706699176.tar.zst
name
- windows64
+ linux64
- linux64
+ windows64
archive
hash
- beab04c9ea6036b1851a485b65c66cf6a38f0be4
+ b46cef5646a8d0471ab6256fe5119220fa238772
hash_algorithm
sha1
url
- https://github.com/secondlife/3p-tracy/releases/download/v0.11.1-r1/tracy-v0.11.1.11706699176-linux64-11706699176.tar.zst
+ https://github.com/secondlife/3p-tracy/releases/download/v0.11.1-r1/tracy-v0.11.1.11706699176-windows64-11706699176.tar.zst
name
- linux64
+ windows64
license
@@ -2450,6 +2548,38 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
description
TUT is a small and portable unit test framework for C++.
+ vhacd
+
+ platforms
+
+ common
+
+ archive
+
+ hash
+ 140d8fc952a10edb5f2d72ab405336019ef32cadfa64f0cfce76c9de4bc6268cbc87cc8cd89d3417fb78b531d441701afc8d016bafe4bd275df2707f7daf1387
+ hash_algorithm
+ blake2b
+ url
+ https://github.com/AlchemyViewer/3p-vhacd/releases/download/v4.1.0-r2/vhacd-4.1.0-r2-common-18166921729.tar.zst
+
+ name
+ common
+
+
+ license
+ BSD
+ license_file
+ LICENSES/vhacd.txt
+ copyright
+ Copyright (c) 2011, Khaled Mamou
+ version
+ 4.1.0-r2
+ name
+ vhacd
+ description
+ Voxelized Hierarchical Approximate Convex Decomposition
+
viewer-fonts
platforms
@@ -2784,47 +2914,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
description
zlib data compression library for the next generation systems
- tinyexr
-
- platforms
-
- common
-
- archive
-
- hash
- a9649f85e20c0b83acfd7b02ca19c1dcdd15f01e
- hash_algorithm
- sha1
- url
- https://github.com/secondlife/3p-tinyexr/releases/download/v1.0.9-5e8947c/tinyexr-1.0.9-5e8947c-common-10475846787.tar.zst
-
- name
- common
-
-
- license
- 3-clause BSD
- license_file
- LICENSES/tinyexr_license.txt
- copyright
- Copyright (c) 2014 - 2021, Syoyo Fujita and many contributors.
- version
- 1.0.9-5e8947c
- name
- tinyexr
- vcs_branch
- dependabot/github_actions/secondlife/action-autobuild-4
- vcs_revision
- 4dc4d1d90d82a22843e2adf5130f9ecb5ee5769e
- vcs_url
- https://github.com/secondlife/3p-tinyexr
- description
- tinyexr import library
- source_type
- git
-
- discord_sdk
+ velopack
platforms
@@ -2832,14 +2922,12 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
archive
- creds
- github
hash
- e11571bf76b27d15c244069988ae372eaa5afae9
+ df2260187110aa51c20101183e18a55f86e71090
hash_algorithm
sha1
url
- https://api.github.com/repos/secondlife/3p-discord-sdk/releases/assets/279333720
+ https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-windows64-24685318450.tar.zst
name
windows64
@@ -2848,71 +2936,29 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
archive
- creds
- github
hash
- dc21df8b051c425163acf3eff8f06e32f407c9e0
+ ead94386d7b9a143824d7ccedc86433127028a91
hash_algorithm
sha1
url
- https://api.github.com/repos/secondlife/3p-discord-sdk/releases/assets/279333706
+ https://github.com/secondlife-3p/3p-velopack/releases/download/v0.0.1535-r2/velopack-40232ef.24685318450-darwin64-24685318450.tar.zst
name
darwin64
license
- discord_sdk
- license_file
- LICENSES/discord_sdk.txt
- copyright
- Discord Inc.
- version
- 1.4.9649.16733550144
- name
- discord_sdk
- vcs_branch
- main
- vcs_revision
- ef5c7c4a490ceac2df2b2f046788b1daf1bbb392
- vcs_url
- https://github.com/secondlife/3p-discord-sdk
- canonical_repo
- https://github.com/secondlife/3p-discord-sdk
- description
- Discord Social SDK
-
- vhacd
-
- platforms
-
- common
-
- archive
-
- hash
- 140d8fc952a10edb5f2d72ab405336019ef32cadfa64f0cfce76c9de4bc6268cbc87cc8cd89d3417fb78b531d441701afc8d016bafe4bd275df2707f7daf1387
- hash_algorithm
- blake2b
- url
- https://github.com/AlchemyViewer/3p-vhacd/releases/download/v4.1.0-r2/vhacd-4.1.0-r2-common-18166921729.tar.zst
-
- name
- common
-
-
- license
- BSD
+ MIT
license_file
- LICENSES/vhacd.txt
+ LICENSES/velopack.txt
copyright
- Copyright (c) 2011, Khaled Mamou
+ Velopack Ltd.
version
- 4.1.0-r2
+ 40232ef.24685318450
name
- vhacd
+ velopack
description
- Voxelized Hierarchical Approximate Convex Decomposition
+ Velopack C/C++ Library
package_description
@@ -3366,7 +3412,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/CMakeLists.txt b/indra/cmake/CMakeLists.txt
index 2ba282bdb78..c10f6ec934b 100644
--- a/indra/cmake/CMakeLists.txt
+++ b/indra/cmake/CMakeLists.txt
@@ -62,6 +62,7 @@ set(cmake_SOURCE_FILES
UI.cmake
UnixInstall.cmake
Variables.cmake
+ Velopack.cmake
VHACD.cmake
ViewerMiscLibs.cmake
VisualLeakDetector.cmake
diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake
new file mode 100644
index 00000000000..a1dbe2cbe92
--- /dev/null
+++ b/indra/cmake/Velopack.cmake
@@ -0,0 +1,68 @@
+# -*- cmake -*-
+# Velopack installer and update framework integration
+# https://velopack.io/
+
+include_guard()
+
+# USE_VELOPACK controls whether to use Velopack for installer packaging (instead of NSIS/DMG)
+option(USE_VELOPACK "Use Velopack for installer packaging" OFF)
+
+if (WINDOWS)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/velopack_libc.lib
+ )
+
+ # Windows system libraries required by Velopack
+ target_link_libraries(ll::velopack INTERFACE
+ winhttp
+ ole32
+ shell32
+ shlwapi
+ version
+ userenv
+ ws2_32
+ bcrypt
+ ntdll
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+elseif (DARWIN)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/libvelopack_libc.a
+ )
+
+ # macOS system frameworks required by Velopack (Rust static library dependencies)
+ target_link_libraries(ll::velopack INTERFACE
+ "-framework Foundation"
+ "-framework Security"
+ "-framework SystemConfiguration"
+ "-framework AppKit"
+ "-framework CoreFoundation"
+ "-framework CoreServices"
+ "-framework IOKit"
+ "-liconv"
+ "-lresolv"
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+endif()
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index 1bd65eb57df..0ad0b6b1a90 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -157,7 +157,8 @@ def get_default_platform(dummy):
for use by a .bat file.""",
default=None),
dict(name='versionfile',
- description="""The name of a file containing the full version number."""),
+ description="""The name of a file containing the full version number.""",
+ default=None),
]
def usage(arguments, srctree=""):
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/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp
index 1bc1283d0bf..d3242a6c96c 100644
--- a/indra/llcommon/llwatchdog.cpp
+++ b/indra/llcommon/llwatchdog.cpp
@@ -87,7 +87,7 @@ void LLWatchdogEntry::start()
void LLWatchdogEntry::stop()
{
// this can happen very late in the shutdown sequence
- if (!LLWatchdog::wasDeleted())
+ if (LLWatchdog::instanceExists())
{
LLWatchdog::getInstance()->remove(this);
}
diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h
index fded881bb8d..2100a908798 100644
--- a/indra/llcommon/llwatchdog.h
+++ b/indra/llcommon/llwatchdog.h
@@ -83,12 +83,12 @@ class LLWatchdogTimeout : public LLWatchdogEntry
};
class LLWatchdogTimerThread; // Defined in the cpp
-class LLWatchdog : public LLSingleton
+class LLWatchdog : public LLSimpleton
{
- LLSINGLETON(LLWatchdog);
+public:
+ LLWatchdog();
~LLWatchdog();
-public:
// Add an entry to the watchdog.
void add(LLWatchdogEntry* e);
void remove(LLWatchdogEntry* e);
diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp
index 67c23358edf..fbd92d089e1 100644
--- a/indra/llcommon/workqueue.cpp
+++ b/indra/llcommon/workqueue.cpp
@@ -38,7 +38,8 @@ LL::WorkQueueBase::WorkQueueBase(const std::string& name, bool auto_shutdown)
{
// Register for "LLApp" events so we can implicitly close() on viewer shutdown
std::string listener_name = "WorkQueue:" + getKey();
- LLEventPumps::instance().obtain("LLApp").listen(
+ LLEventPumps* pump = LLEventPumps::getInstance();
+ pump->obtain("LLApp").listen(
listener_name,
[this](const LLSD& stat)
{
@@ -54,14 +55,25 @@ LL::WorkQueueBase::WorkQueueBase(const std::string& name, bool auto_shutdown)
// Store the listener name so we can unregister in the destructor
mListenerName = listener_name;
+ mPumpHandle = pump->getHandle();
}
}
LL::WorkQueueBase::~WorkQueueBase()
{
- if (!mListenerName.empty() && !LLEventPumps::wasDeleted())
+ if (!mListenerName.empty() && !mPumpHandle.isDead())
{
- LLEventPumps::instance().obtain("LLApp").stopListening(mListenerName);
+ // Due to shutdown order issues, use handle, not a singleton
+ // and ignore fiber issue.
+ try
+ {
+ LLEventPumps* pump = mPumpHandle.get();
+ pump->obtain("LLApp").stopListening(mListenerName);
+ }
+ catch (const boost::fibers::lock_error&)
+ {
+ // Likely mutex is down, ignore
+ }
}
}
diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h
index 573203a5b35..69f3286c1b9 100644
--- a/indra/llcommon/workqueue.h
+++ b/indra/llcommon/workqueue.h
@@ -14,6 +14,7 @@
#include "llcoros.h"
#include "llexception.h"
+#include "llhandle.h"
#include "llinstancetracker.h"
#include "llinstancetrackersubclass.h"
#include "threadsafeschedule.h"
@@ -22,6 +23,9 @@
#include // std::function
#include
+class LLEventPumps;
+
+
namespace LL
{
@@ -202,6 +206,8 @@ namespace LL
// Name used for the LLApp event listener (empty if not registered)
std::string mListenerName;
+ // Due to shutdown order issues, store by handle
+ LLHandle mPumpHandle;
};
/*****************************************************************************
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/llmessage/llassetstorage.cpp b/indra/llmessage/llassetstorage.cpp
index 4c3acb27f47..b6a98575f92 100644
--- a/indra/llmessage/llassetstorage.cpp
+++ b/indra/llmessage/llassetstorage.cpp
@@ -453,6 +453,7 @@ bool LLAssetStorage::findInCacheAndInvokeCallback(const LLUUID& uuid, LLAssetTyp
bool exists = LLFileSystem::getExists(uuid, type);
if (exists)
{
+ LL_PROFILE_ZONE_SCOPED;
LLFileSystem file(uuid, type);
U32 size = file.getSize();
if (size > 0)
@@ -562,7 +563,7 @@ void LLAssetStorage::getAssetData(const LLUUID uuid,
if (callback == tmp->mDownCallback && user_data == tmp->mUserData)
{
// this is a duplicate from the same subsystem - throw it away
- LL_WARNS("AssetStorage") << "Discarding duplicate request for asset " << uuid
+ LL_DEBUGS("AssetStorage") << "Discarding duplicate request for asset " << uuid
<< "." << LLAssetType::lookup(type) << LL_ENDL;
return;
}
diff --git a/indra/llmessage/lldatapacker.cpp b/indra/llmessage/lldatapacker.cpp
index e911150787c..ecd0b4ee8de 100644
--- a/indra/llmessage/lldatapacker.cpp
+++ b/indra/llmessage/lldatapacker.cpp
@@ -289,32 +289,46 @@ bool LLDataPackerBinaryBuffer::packBinaryData(const U8 *value, S32 size, const c
}
-bool LLDataPackerBinaryBuffer::unpackBinaryData(U8 *value, S32 &size, const char *name)
+bool LLDataPackerBinaryBuffer::unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name)
{
if (!verifyLength(4, name))
{
LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData would unpack invalid data, aborting!" << LL_ENDL;
+ out_size = 0;
return false;
}
- htolememcpy(&size, mCurBufferp, MVT_S32, 4);
+ if (value_size < 0)
+ {
+ LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData passed negative buffer size, aborting!" << LL_ENDL;
+ out_size = 0;
+ return false;
+ }
+
+ htolememcpy(&out_size, mCurBufferp, MVT_S32, 4);
- if (size < 0)
+ if (out_size < 0)
{
LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData unpacked invalid size, aborting!" << LL_ENDL;
+ out_size = 0;
return false;
}
mCurBufferp += 4;
- if (!verifyLength(size, name))
+ if (!verifyLength(out_size, name))
{
LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData would unpack invalid data, aborting!" << LL_ENDL;
return false;
}
+ S32 copy_size = llmin(out_size, value_size);
+ htolememcpy(value, mCurBufferp, MVT_VARIABLE, copy_size);
+ mCurBufferp += out_size;
- htolememcpy(value, mCurBufferp, MVT_VARIABLE, size);
- mCurBufferp += size;
+ if (value_size < out_size)
+ {
+ LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData buffer too small for data, truncating!" << LL_ENDL;
+ }
return true;
}
@@ -836,21 +850,34 @@ bool LLDataPackerAsciiBuffer::packBinaryData(const U8 *value, S32 size, const ch
}
-bool LLDataPackerAsciiBuffer::unpackBinaryData(U8 *value, S32 &size, const char *name)
+bool LLDataPackerAsciiBuffer::unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name)
{
bool success = true;
char valuestr[DP_BUFSIZE]; /* Flawfinder: ignore */
if (!getValueStr(name, valuestr, DP_BUFSIZE))
{
+ out_size = 0;
+ return false;
+ }
+
+ if (value_size < 0)
+ {
+ LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData passed negative buffer size, aborting!" << LL_ENDL;
+ out_size = 0;
return false;
}
char *cur_pos = &valuestr[0];
- sscanf(valuestr,"%010d", &size);
+ sscanf(valuestr,"%010d", &out_size);
cur_pos += 11;
+ S32 max_bytes = llmin(out_size, value_size);
+ if (max_bytes != out_size)
+ {
+ LL_WARNS() << "LLDataPackerAsciiBuffer::unpackBinaryData: buffer too small for data, truncating!" << LL_ENDL;
+ }
S32 i;
- for (i = 0; i < size; i++)
+ for (i = 0; i < max_bytes; i++)
{
S32 val;
sscanf(cur_pos,"%02x", &val);
@@ -1634,28 +1661,40 @@ bool LLDataPackerAsciiFile::packBinaryData(const U8 *value, S32 size, const char
}
-bool LLDataPackerAsciiFile::unpackBinaryData(U8 *value, S32 &size, const char *name)
+bool LLDataPackerAsciiFile::unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name)
{
- bool success = true;
char valuestr[DP_BUFSIZE]; /*Flawfinder: ignore*/
if (!getValueStr(name, valuestr, DP_BUFSIZE))
{
+ out_size = 0;
+ return false;
+ }
+
+ if (value_size < 0)
+ {
+ LL_WARNS() << "LLDataPackerBinaryBuffer::unpackBinaryData passed negative buffer size, aborting!" << LL_ENDL;
+ out_size = 0;
return false;
}
char *cur_pos = &valuestr[0];
- sscanf(valuestr,"%010d", &size);
+ sscanf(valuestr,"%010d", &out_size);
cur_pos += 11;
+ S32 max_bytes = llmin(out_size, value_size);
+ if (max_bytes != out_size)
+ {
+ LL_WARNS() << "LLDataPackerAsciiBuffer::unpackBinaryData: buffer too small for data, truncating!" << LL_ENDL;
+ }
S32 i;
- for (i = 0; i < size; i++)
+ for (i = 0; i < max_bytes; i++)
{
S32 val;
sscanf(cur_pos,"%02x", &val);
value[i] = val;
cur_pos += 3;
}
- return success;
+ return true;
}
diff --git a/indra/llmessage/lldatapacker.h b/indra/llmessage/lldatapacker.h
index 167c102b438..5ac45356cfe 100644
--- a/indra/llmessage/lldatapacker.h
+++ b/indra/llmessage/lldatapacker.h
@@ -49,7 +49,7 @@ class LLDataPacker
virtual bool unpackString(std::string& value, const char *name) = 0;
virtual bool packBinaryData(const U8 *value, S32 size, const char *name) = 0;
- virtual bool unpackBinaryData(U8 *value, S32 &size, const char *name) = 0;
+ virtual bool unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name) = 0;
// Constant size binary data packing
virtual bool packBinaryDataFixed(const U8 *value, S32 size, const char *name) = 0;
@@ -135,7 +135,7 @@ class LLDataPackerBinaryBuffer : public LLDataPacker
/*virtual*/ bool unpackString(std::string& value, const char *name);
/*virtual*/ bool packBinaryData(const U8 *value, S32 size, const char *name);
- /*virtual*/ bool unpackBinaryData(U8 *value, S32 &size, const char *name);
+ /*virtual*/ bool unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name);
// Constant size binary data packing
/*virtual*/ bool packBinaryDataFixed(const U8 *value, S32 size, const char *name);
@@ -246,7 +246,7 @@ class LLDataPackerAsciiBuffer : public LLDataPacker
/*virtual*/ bool unpackString(std::string& value, const char *name);
/*virtual*/ bool packBinaryData(const U8 *value, S32 size, const char *name);
- /*virtual*/ bool unpackBinaryData(U8 *value, S32 &size, const char *name);
+ /*virtual*/ bool unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name);
// Constant size binary data packing
/*virtual*/ bool packBinaryDataFixed(const U8 *value, S32 size, const char *name);
@@ -378,7 +378,7 @@ class LLDataPackerAsciiFile : public LLDataPacker
/*virtual*/ bool unpackString(std::string& value, const char *name);
/*virtual*/ bool packBinaryData(const U8 *value, S32 size, const char *name);
- /*virtual*/ bool unpackBinaryData(U8 *value, S32 &size, const char *name);
+ /*virtual*/ bool unpackBinaryData(U8 *value, S32 value_size, S32 &out_size, const char *name);
/*virtual*/ bool packBinaryDataFixed(const U8 *value, S32 size, const char *name);
/*virtual*/ bool unpackBinaryDataFixed(U8 *value, S32 size, const char *name);
diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp
index 77a4b08af55..88d428c2334 100644
--- a/indra/llplugin/llpluginclassmedia.cpp
+++ b/indra/llplugin/llpluginclassmedia.cpp
@@ -168,6 +168,7 @@ void LLPluginClassMedia::reset()
void LLPluginClassMedia::idle(void)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA;
if(mPlugin)
{
mPlugin->idle();
diff --git a/indra/llprimitive/llprimitive.cpp b/indra/llprimitive/llprimitive.cpp
index c5d6076b984..7a638dd6254 100644
--- a/indra/llprimitive/llprimitive.cpp
+++ b/indra/llprimitive/llprimitive.cpp
@@ -1515,7 +1515,7 @@ S32 LLPrimitive::unpackTEMessage(LLDataPacker &dp)
S32 size;
U32 face_count = 0;
- if (!dp.unpackBinaryData(packed_buffer, size, "TextureEntry"))
+ if (!dp.unpackBinaryData(packed_buffer, MAX_TE_BUFFER, size, "TextureEntry"))
{
retval = TEM_INVALID;
LL_WARNS() << "Bad texture entry block! Abort!" << LL_ENDL;
diff --git a/indra/llprimitive/lltextureanim.cpp b/indra/llprimitive/lltextureanim.cpp
index 579538075ac..963b7456b87 100644
--- a/indra/llprimitive/lltextureanim.cpp
+++ b/indra/llprimitive/lltextureanim.cpp
@@ -155,7 +155,7 @@ void LLTextureAnim::unpackTAMessage(LLDataPacker &dp)
{
S32 size;
U8 data[TA_BLOCK_SIZE];
- dp.unpackBinaryData(data, size, "TextureAnimation");
+ dp.unpackBinaryData(data, TA_BLOCK_SIZE, size, "TextureAnimation");
if (size != TA_BLOCK_SIZE)
{
if (size)
diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp
index d37b16ce0ca..d9857a7ad58 100644
--- a/indra/llrender/llfontfreetype.cpp
+++ b/indra/llrender/llfontfreetype.cpp
@@ -871,30 +871,39 @@ namespace ll
U8 const* LLFontManager::loadFont( std::string const &aFilename, long &a_Size)
{
- a_Size = 0;
- std::map< std::string, std::shared_ptr >::iterator itr = m_LoadedFonts.find( aFilename );
- if( itr != m_LoadedFonts.end() )
+ try
{
- ++itr->second->mRefs;
- // A possible overflow cannot happen here, as it is asserted that the size is less than std::numeric_limits::max() a few lines below.
- a_Size = static_cast(itr->second->mSize);
- return reinterpret_cast(itr->second->mAddress.c_str());
- }
+ a_Size = 0;
+ std::map< std::string, std::shared_ptr >::iterator itr = m_LoadedFonts.find(aFilename);
+ if (itr != m_LoadedFonts.end())
+ {
+ ++itr->second->mRefs;
+ // A possible overflow cannot happen here, as it is asserted that the size is less than std::numeric_limits::max() a few lines below.
+ a_Size = static_cast(itr->second->mSize);
+ return reinterpret_cast(itr->second->mAddress.c_str());
+ }
- auto strContent = LLFile::getContents(aFilename);
+ auto strContent = LLFile::getContents(aFilename);
- if( strContent.empty() )
- return nullptr;
+ if (strContent.empty())
+ return nullptr;
- // For fontconfig a type of long is required, std::string::size() returns size_t. I think it is safe to limit this to 2GiB and not support fonts that huge (can that even be a thing?)
- llassert_always( strContent.size() < std::numeric_limits::max() );
+ // For fontconfig a type of long is required, std::string::size() returns size_t. I think it is safe to limit this to 2GiB and not support fonts that huge (can that even be a thing?)
+ llassert_always(strContent.size() < std::numeric_limits::max());
- a_Size = static_cast(strContent.size());
+ a_Size = static_cast(strContent.size());
- auto pCache = std::make_shared( aFilename, strContent, a_Size );
- itr = m_LoadedFonts.insert( std::make_pair( aFilename, pCache ) ).first;
+ auto pCache = std::make_shared(aFilename, strContent, a_Size);
+ itr = m_LoadedFonts.insert(std::make_pair(aFilename, pCache)).first;
- return reinterpret_cast(itr->second->mAddress.c_str());
+ return reinterpret_cast(itr->second->mAddress.c_str());
+ }
+ catch (const std::bad_alloc&)
+ {
+ LLError::LLUserWarningMsg::showOutOfMemory();
+ LL_ERRS() << "Failed to load font. Out of memory." << LL_ENDL;
+ }
+ return nullptr;
}
void LLFontManager::unloadAllFonts()
diff --git a/indra/llrender/llfontvertexbuffer.cpp b/indra/llrender/llfontvertexbuffer.cpp
index a223509d30e..2a0115265fb 100644
--- a/indra/llrender/llfontvertexbuffer.cpp
+++ b/indra/llrender/llfontvertexbuffer.cpp
@@ -237,3 +237,80 @@ void LLFontVertexBuffer::renderBuffers()
gGL.popUIMatrix();
}
+// LLFontWidthBuffer
+bool LLFontWidthBuffer::sEnableBufferCollection = true;
+
+LLFontWidthBuffer::LLFontWidthBuffer()
+{
+}
+
+LLFontWidthBuffer::~LLFontWidthBuffer()
+{
+}
+
+void LLFontWidthBuffer::reset()
+{
+ mLastFont = nullptr;
+ mLastOffset = 0;
+ mLastMaxChars = 0;
+ mLastNoPadding = false;
+ mWidth = -1.f;
+ mLastScaleX = 1.f;
+ mLastScaleY = 1.f;
+ mLastVertDPI = 0.f;
+ mLastHorizDPI = 0.f;
+ mLastResGeneration = 0;
+ mLastFontCacheGen = 0;
+}
+
+F32 LLFontWidthBuffer::getWidth(
+ const LLFontGL* fontp,
+ const llwchar* wchars,
+ S32 begin_offset,
+ S32 max_chars,
+ bool no_padding)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+ if (!fontp || !wchars)
+ {
+ return 0.f;
+ }
+
+ if (!sEnableBufferCollection)
+ {
+ return fontp->getWidthF32(wchars, begin_offset, max_chars, no_padding);
+ }
+
+ // Check if we can use cached width
+ bool needs_recalc = (mWidth < 0.f)
+ || (mLastFont != fontp)
+ || (mLastOffset != begin_offset)
+ || (mLastMaxChars != max_chars)
+ || (mLastNoPadding != no_padding)
+ || (mLastScaleX != LLFontGL::sScaleX)
+ || (mLastScaleY != LLFontGL::sScaleY)
+ || (mLastVertDPI != LLFontGL::sVertDPI)
+ || (mLastHorizDPI != LLFontGL::sHorizDPI)
+ || (mLastResGeneration != LLFontGL::sResolutionGeneration)
+ || (mLastFontCacheGen != fontp->getCacheGeneration());
+
+ if (needs_recalc)
+ {
+ // Calculate width using the font
+ mWidth = fontp->getWidthF32(wchars, begin_offset, max_chars, no_padding);
+
+ // Cache the parameters
+ mLastFont = fontp;
+ mLastOffset = begin_offset;
+ mLastMaxChars = max_chars;
+ mLastNoPadding = no_padding;
+ mLastScaleX = LLFontGL::sScaleX;
+ mLastScaleY = LLFontGL::sScaleY;
+ mLastVertDPI = LLFontGL::sVertDPI;
+ mLastHorizDPI = LLFontGL::sHorizDPI;
+ mLastResGeneration = LLFontGL::sResolutionGeneration;
+ mLastFontCacheGen = fontp->getCacheGeneration();
+ }
+
+ return mWidth;
+}
diff --git a/indra/llrender/llfontvertexbuffer.h b/indra/llrender/llfontvertexbuffer.h
index a9e1e2337c2..94b833d2275 100644
--- a/indra/llrender/llfontvertexbuffer.h
+++ b/indra/llrender/llfontvertexbuffer.h
@@ -32,6 +32,11 @@
class LLVertexBufferData;
+// Rendering fonts is expensive, this class is intended to store
+// vertex buffers for rendered text, so that they can be reused.
+// LLFontVertexBuffer tracks font and rendering parameters, but
+// expects caller to track text changes and call reset() when
+// text changes.
class LLFontVertexBuffer
{
public:
@@ -127,4 +132,45 @@ class LLFontVertexBuffer
static bool sEnableBufferCollection;
};
+// Extracting width from a font is expensive, and due to
+// mechanics of font rendering, we need width separately
+// and usually before rendering.
+// LLFontWidthBuffer tracks font and rendering parameters,
+// but expects caller to track text changes and call reset()
+// when text changes.
+class LLFontWidthBuffer
+{
+public:
+ LLFontWidthBuffer();
+ ~LLFontWidthBuffer();
+
+ void reset();
+
+ F32 getWidth(const LLFontGL* fontp,
+ const llwchar* wchars,
+ S32 begin_offset,
+ S32 max_chars,
+ bool no_padding);
+
+ static void enableBufferCollection(bool enable) { sEnableBufferCollection = enable; }
+private:
+ const LLFontGL* mLastFont = nullptr;
+ S32 mLastOffset = 0;
+ S32 mLastMaxChars = 0;
+ bool mLastNoPadding = false;
+ F32 mWidth = -1.f;
+
+ // LLFontGL's values that affect width calculation
+ F32 mLastScaleX = 1.f;
+ F32 mLastScaleY = 1.f;
+ F32 mLastVertDPI = 0.f;
+ F32 mLastHorizDPI = 0.f;
+ S32 mLastResGeneration = 0;
+
+ // Cache generation tracking
+ S32 mLastFontCacheGen = 0;
+
+ static bool sEnableBufferCollection;
+};
+
#endif
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index 2c35a6acaec..b8545b3ed9c 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1024,8 +1024,23 @@ void LLShaderMgr::initShaderCache(bool enabled, const LLUUID& old_cache_version,
llifstream instream(meta_out_path, std::ifstream::in | std::ifstream::binary);
LLSD in_data;
- // todo: this is likely very expensive to parse, should use binary
- LLSDSerialize::fromBinary(in_data, instream, LLSDSerialize::SIZE_UNLIMITED);
+ try
+ {
+ LLSDSerialize::fromBinary(in_data, instream, LLSDSerialize::SIZE_UNLIMITED);
+ }
+ catch( std::bad_alloc& )
+ {
+ // Try to get a bit more memory back before we try to clear the cache.
+ in_data.clear();
+ // Just in case it was somehow the cause, clear cache.
+ clearShaderCache();
+ // If user run out of memory this early in init,
+ // we don't want to keep going just to crash again.
+ // Notify user and close.
+ LLError::LLUserWarningMsg::showOutOfMemory();
+ LL_ERRS("ShaderMgr") << "Failed to parse shader cache metadata, potentially due to size. Purged cache." << LL_ENDL;
+ return;
+ }
instream.close();
if (old_cache_version == current_cache_version
diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp
index ae676251ffc..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);
@@ -1323,6 +1323,39 @@ bool LLComboBox::selectItemRange( S32 first, S32 last )
return mList->selectItemRange(first, last);
}
+void LLComboBox::addInfo(LLSD& info)
+{
+ LLUICtrl::addInfo(info);
+
+ if (mList && mList->getItemCount() > 0)
+ {
+ LLSD items_array;
+ std::vector item_list = mList->getAllData();
+ for (std::vector::iterator iter = item_list.begin(); iter != item_list.end(); ++iter)
+ {
+ if (LLScrollListItem* item = *iter)
+ {
+ LLSD item_info;
+ item_info["value"] = item->getValue();
+ if (item->getNumColumns() > 0)
+ {
+ if (LLScrollListCell* cell = item->getColumn(0))
+ {
+ item_info["label"] = cell->getValue();
+ }
+ }
+ items_array.append(item_info);
+ }
+ }
+ info["items"] = items_array;
+ info["item_count"] = mList->getItemCount();
+ info["current_selection"] = getSelectedItemLabel();
+ }
+ else
+ {
+ info["item_count"] = 0;
+ }
+}
static LLDefaultChildRegistry::Register register_icons_combo_box("icons_combo_box");
diff --git a/indra/llui/llcombobox.h b/indra/llui/llcombobox.h
index d6ea1202d39..dad60ffb65b 100644
--- a/indra/llui/llcombobox.h
+++ b/indra/llui/llcombobox.h
@@ -219,6 +219,10 @@ class LLComboBox
void setButtonVisible(bool visible);
+ // Populates the provided LLSD with combo box-specific information(list of items, item count, current selection label)
+ // also includes base LLUICtrl information via parent class
+ void addInfo(LLSD & info);
+
void onButtonMouseDown();
void onListMouseUp();
void onItemSelected(const LLSD& data);
diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp
index 17641b83754..7cf09369ec2 100644
--- a/indra/llui/llfloaterreglistener.cpp
+++ b/indra/llui/llfloaterreglistener.cpp
@@ -60,6 +60,10 @@ LLFloaterRegListener::LLFloaterRegListener():
"Ask to toggle the state of the floater specified in [\"name\"]",
&LLFloaterRegListener::toggleInstance,
requiredName);
+ add("toggleInstanceOrBringToFront",
+ "Ask to toggle the state of the floater specified in [\"name\"] or bring it to front if already opened",
+ &LLFloaterRegListener::toggleInstance,
+ requiredName);
add("instanceVisible",
"Return on [\"reply\"] an event whose [\"visible\"] indicates the visibility "
"of the floater specified in [\"name\"]",
@@ -107,6 +111,11 @@ void LLFloaterRegListener::toggleInstance(const LLSD& event) const
LLFloaterReg::toggleInstance(event["name"].asString(), event["key"]);
}
+void LLFloaterRegListener::toggleInstanceOrBringToFront(const LLSD& event) const
+{
+ LLFloaterReg::toggleInstanceOrBringToFront(event["name"].asString(), event["key"]);
+}
+
void LLFloaterRegListener::instanceVisible(const LLSD& event) const
{
sendReply(LLSDMap("visible", LLFloaterReg::instanceVisible(event["name"].asString(), event["key"])),
diff --git a/indra/llui/llfloaterreglistener.h b/indra/llui/llfloaterreglistener.h
index 28f6e7c66b9..610a2831dca 100644
--- a/indra/llui/llfloaterreglistener.h
+++ b/indra/llui/llfloaterreglistener.h
@@ -46,6 +46,7 @@ class LLFloaterRegListener: public LLEventAPI
void showInstance(const LLSD& event) const;
void hideInstance(const LLSD& event) const;
void toggleInstance(const LLSD& event) const;
+ void toggleInstanceOrBringToFront(const LLSD& event) const;
void instanceVisible(const LLSD& event) const;
void clickButton(const LLSD& event) const;
};
diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp
index df99c4f6362..e36fd45bb48 100644
--- a/indra/llui/llscrollcontainer.cpp
+++ b/indra/llui/llscrollcontainer.cpp
@@ -480,6 +480,7 @@ void LLScrollContainer::calcVisibleSize( S32 *visible_width, S32 *visible_height
void LLScrollContainer::draw()
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
static LLUICachedControl scrollbar_size_control ("UIScrollbarSize", 0);
S32 scrollbar_size = (mSize == -1 ? scrollbar_size_control : mSize);
diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp
index 24ae5c09e98..9fcf2a5f10b 100644
--- a/indra/llui/lltextbase.cpp
+++ b/indra/llui/lltextbase.cpp
@@ -222,6 +222,7 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)
mTrustedContent(p.trusted_content),
mAlwaysShowIcons(p.always_show_icons),
mTrackEnd( p.track_end ),
+ mTrackValueChange(true),
mScrollIndex(-1),
mSelectionStart( 0 ),
mSelectionEnd( 0 ),
@@ -964,7 +965,10 @@ void LLTextBase::drawText()
S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::segment_vec_t* segments )
{
- beforeValueChange();
+ if (mTrackValueChange)
+ {
+ beforeValueChange();
+ }
S32 old_len = getLength(); // length() returns character length
S32 insert_len = static_cast(wstr.length());
@@ -1087,12 +1091,14 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s
getViewModel()->getEditableDisplay().insert(pos, wstr);
- if ( truncate() )
+ if (mTrackValueChange)
{
- insert_len = getLength() - old_len;
+ if (truncate())
+ {
+ insert_len = getLength() - old_len;
+ }
+ onValueChange(pos, pos + insert_len);
}
-
- onValueChange(pos, pos + insert_len);
needsReflow(pos);
return insert_len;
@@ -1108,7 +1114,10 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length)
// Clamp length to not go past the end of the text
length = std::min(length, text_length - pos);
- beforeValueChange();
+ if (mTrackValueChange)
+ {
+ beforeValueChange();
+ }
segment_set_t::iterator seg_iter = getSegIterContaining(pos);
while(seg_iter != mSegments.end())
{
@@ -1159,7 +1168,10 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length)
// recreate default segment in case we erased everything
createDefaultSegment();
- onValueChange(pos, pos);
+ if (mTrackValueChange)
+ {
+ onValueChange(pos, pos);
+ }
needsReflow(pos);
return -length; // This will be wrong if someone calls removeStringNoUndo with an excessive length
@@ -1167,7 +1179,10 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length)
S32 LLTextBase::overwriteCharNoUndo(S32 pos, llwchar wc)
{
- beforeValueChange();
+ if (mTrackValueChange)
+ {
+ beforeValueChange();
+ }
if (pos > (S32)getLength())
{
@@ -1175,7 +1190,10 @@ S32 LLTextBase::overwriteCharNoUndo(S32 pos, llwchar wc)
}
getViewModel()->getEditableDisplay()[pos] = wc;
- onValueChange(pos, pos + 1);
+ if (mTrackValueChange)
+ {
+ onValueChange(pos, pos + 1);
+ }
needsReflow(pos);
return 1;
@@ -1622,7 +1640,7 @@ void LLTextBase::deselect()
bool LLTextBase::getSpellCheck() const
{
- return (LLSpellChecker::getUseSpellCheck()) && (!mReadOnly) && (mSpellCheck);
+ return (!mReadOnly) && (LLSpellChecker::getUseSpellCheck()) && (mSpellCheck);
}
const std::string& LLTextBase::getSuggestion(U32 index) const
@@ -2330,6 +2348,10 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_url)
void LLTextBase::setText(const LLStringExplicit &utf8str, const LLStyle::Params& input_params)
{
+ beforeValueChange();
+ // Can insert a lot of different segments, don't want to spam events.
+ mTrackValueChange = false;
+
// clear out the existing text and segments
getViewModel()->setDisplay(LLWStringUtil::null);
@@ -2350,6 +2372,8 @@ void LLTextBase::setText(const LLStringExplicit &utf8str, const LLStyle::Params&
startOfDoc();
}
+ truncate(); // was postponed to avoid micro truncations and expensive checks
+ mTrackValueChange = true;
onValueChange(0, getLength());
}
@@ -2380,6 +2404,10 @@ void LLTextBase::appendTextImpl(const std::string& new_text, const LLStyle::Para
LLStyle::Params style_params(getStyleParams());
style_params.overwriteFrom(input_params);
+ // todo: this does not check for maximum size, might
+ // want to stop once maximum size was reached to avoid
+ // expensive findUrl, replaceUrl calls.
+
S32 part = (S32)LLTextParser::WHOLE;
if ((mParseHTML || force_slurl) && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358).
{
@@ -2553,6 +2581,10 @@ void LLTextBase::copyContents(const LLTextBase* source)
beforeValueChange();
deselect();
+ // Can insert a lot of different segments, don't want to spam events.
+ // Do one full length onValueChange() at the end of this function.
+ mTrackValueChange = false;
+
mSegments.clear();
for (const LLTextSegmentPtr& segp : source->mSegments)
{
@@ -2567,6 +2599,8 @@ void LLTextBase::copyContents(const LLTextBase* source)
getViewModel()->setDisplay(source->getViewModel()->getDisplay());
+ truncate(); // was postponed to avoid micro truncations and expensive checks
+ mTrackValueChange = true;
onValueChange(0, getLength());
needsReflow();
}
@@ -2821,7 +2855,7 @@ S32 LLTextBase::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, bool round,
line_seg_iter != mSegments.end();
++line_seg_iter, line_seg_offset = 0)
{
- const LLTextSegmentPtr segmentp = *line_seg_iter;
+ LLTextSegmentPtr segmentp = *line_seg_iter;
S32 segment_line_start = segmentp->getStart() + line_seg_offset;
S32 segment_line_length = llmin(segmentp->getEnd(), line_iter->mDocIndexEnd) - segment_line_start;
@@ -2912,7 +2946,7 @@ LLRect LLTextBase::getDocRectFromDocIndex(S32 pos) const
while(line_seg_iter != mSegments.end())
{
- const LLTextSegmentPtr segmentp = *line_seg_iter;
+ LLTextSegmentPtr segmentp = *line_seg_iter;
if (line_seg_iter == cursor_seg_iter)
{
@@ -3463,8 +3497,8 @@ LLStyleSP LLTextSegment::cloneStyle(LLTextBase& target, const LLStyle* source)
}
-bool LLTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const { width = 0; height = 0; return false; }
-bool LLTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height) const
+bool LLTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) { width = 0; height = 0; return false; }
+bool LLTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height)
{
F32 fwidth = 0;
bool result = getDimensionsF32(first_char, num_chars, fwidth, height);
@@ -3561,6 +3595,7 @@ F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selec
mFontBufferPreSelection.reset();
mFontBufferSelection.reset();
mFontBufferPostSelection.reset();
+ mFontWidthBuffer.reset();
}
return draw_rect.mLeft;
}
@@ -3586,6 +3621,7 @@ F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 sele
mFontBufferPreSelection.reset();
mFontBufferSelection.reset();
mFontBufferPostSelection.reset();
+ mFontWidthBuffer.reset();
}
const LLFontGL* font = mStyle->getFont();
@@ -3817,17 +3853,19 @@ LLTextSegmentPtr LLNormalTextSegment::clone(LLTextBase& target) const
return new LLNormalTextSegment(sp, mStart, mEnd, target);
}
-bool LLNormalTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+bool LLNormalTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
{
height = 0;
width = 0;
if (num_chars > 0 && (mStart + first_char >= 0))
{
height = mFontHeight;
- const LLWString &text = getWText();
- // if last character is a newline, then return true, forcing line break
- width = mStyle->getFont()->getWidthF32(text.c_str(), mStart + first_char, num_chars, true);
+
+ const LLWString& text = getWText();
+ const LLFontGL* font = mStyle->getFont();
+ width += mFontWidthBuffer.getWidth(font, text.c_str(), mStart + first_char, num_chars, true);
}
+ // if last character is a newline, then return true, forcing line break
return false;
}
@@ -3909,6 +3947,7 @@ void LLNormalTextSegment::updateLayout(const class LLTextBase& editor)
mFontBufferPreSelection.reset();
mFontBufferSelection.reset();
mFontBufferPostSelection.reset();
+ mFontWidthBuffer.reset();
}
void LLNormalTextSegment::dump() const
@@ -4060,7 +4099,7 @@ LLTextSegmentPtr LLInlineViewSegment::clone(LLTextBase& target) const
return nullptr;
}
-bool LLInlineViewSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+bool LLInlineViewSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
{
if (first_char == 0 && num_chars == 0)
{
@@ -4152,7 +4191,7 @@ LLTextSegmentPtr LLLineBreakTextSegment::clone(LLTextBase& target) const
copy->mFontHeight = mFontHeight;
return copy;
}
-bool LLLineBreakTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+bool LLLineBreakTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
{
width = 0;
height = mFontHeight;
@@ -4189,7 +4228,7 @@ LLTextSegmentPtr LLImageTextSegment::clone(LLTextBase& target) const
static const S32 IMAGE_HPAD = 3;
// virtual
-bool LLImageTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+bool LLImageTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
{
width = 0;
height = mStyle->getFont()->getLineHeight();
diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h
index 3ab5e905e3a..9206b3facff 100644
--- a/indra/llui/lltextbase.h
+++ b/indra/llui/lltextbase.h
@@ -68,10 +68,10 @@ class LLTextSegment
virtual LLTextSegmentPtr clone(LLTextBase& terget) const { return new LLTextSegment(mStart, mEnd); }
static LLStyleSP cloneStyle(LLTextBase& target, const LLStyle* source);
- bool getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height) const;
+ bool getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height);
bool getPermitsEmoji() const { return mPermitsEmoji; };
- virtual bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const;
+ virtual bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height);
virtual S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
/**
@@ -139,7 +139,7 @@ class LLNormalTextSegment : public LLTextSegment
virtual ~LLNormalTextSegment();
/*virtual*/ LLTextSegmentPtr clone(LLTextBase& target) const;
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const;
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height);
/*virtual*/ S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
/*virtual*/ S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars, S32 line_ind) const;
/*virtual*/ void updateLayout(const class LLTextBase& editor);
@@ -182,6 +182,7 @@ class LLNormalTextSegment : public LLTextSegment
LLFontVertexBuffer mFontBufferPreSelection;
LLFontVertexBuffer mFontBufferSelection;
LLFontVertexBuffer mFontBufferPostSelection;
+ LLFontWidthBuffer mFontWidthBuffer;
S32 mLastGeneration = -1;
};
@@ -254,7 +255,7 @@ class LLInlineViewSegment : public LLTextSegment
~LLInlineViewSegment();
/*virtual*/ LLTextSegmentPtr clone(LLTextBase& target) const;
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const;
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height);
/*virtual*/ S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars, S32 line_ind) const;
/*virtual*/ void updateLayout(const class LLTextBase& editor);
/*virtual*/ F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect);
@@ -280,7 +281,7 @@ class LLLineBreakTextSegment : public LLTextSegment
LLLineBreakTextSegment(S32 pos);
~LLLineBreakTextSegment();
/*virtual*/ LLTextSegmentPtr clone(LLTextBase& target) const;
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const;
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height);
S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars, S32 line_ind) const;
F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect);
@@ -295,7 +296,7 @@ class LLImageTextSegment : public LLTextSegment
~LLImageTextSegment();
/*virtual*/ LLTextSegmentPtr clone(LLTextBase& target) const;
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const;
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height);
S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 char_offset, S32 max_chars, S32 line_ind) const;
F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect);
@@ -761,6 +762,7 @@ class LLTextBase
bool mUseEmoji;
bool mUseColor;
bool mTrackEnd; // if true, keeps scroll position at end of document during resize
+ bool mTrackValueChange; // if true, send out onValueChange() from low level text modification methods
bool mReadOnly;
bool mBGVisible; // render background?
bool mClip; // clip text to widget rect
diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp
index cb101d325d0..8a21222b3cd 100644
--- a/indra/llui/llurlregistry.cpp
+++ b/indra/llui/llurlregistry.cpp
@@ -149,12 +149,77 @@ static bool stringHasUrl(const std::string &text)
// fast heuristic test for a URL in a string. This is used
// to avoid lots of costly regex calls, BUT it needs to be
// kept in sync with the LLUrlEntry regexes we support.
- return (text.find("://") != std::string::npos ||
- text.find("www.") != std::string::npos ||
- text.find(".com") != std::string::npos ||
- text.find("") != std::string::npos ||
- text.find("= text.length())
+ {
+ // Nothing else is going to match or fit if we don't
+ // have at least 4 characters left
+ // Ex: expectation is that there is something after protocol delimiter
+ // and .com takes 4 characters.
+ return false;
+ }
+
+ // Check for protocol delimiter
+ if (c == ':' && text[i + 1] == '/' && text[i + 2] == '/')
+ {
+ return true;
+ }
+
+ // Check for www. at start of word
+ if (c == 'w'
+ && text[i + 1] == 'w'
+ && text[i + 2] == 'w'
+ && text[i + 3] == '.')
+ {
+ return true;
+ }
+
+ // Check for .com (and similar)
+ if (c == '.')
+ {
+ const char* suffix = text.c_str() + i + 1;
+ if ((suffix[0] == 'c' && suffix[1] == 'o' && suffix[2] == 'm') ||
+ (suffix[0] == 'n' && suffix[1] == 'e' && suffix[2] == 't') ||
+ (suffix[0] == 'o' && suffix[1] == 'r' && suffix[2] == 'g') ||
+ (suffix[0] == 'e' && suffix[1] == 'd' && suffix[2] == 'u'))
+ {
+ return true;
+ }
+ }
+
+ // Check for or = 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/llwindow/llwindowmacosx-objc.h b/indra/llwindow/llwindowmacosx-objc.h
index b302a705da6..ec9afb18448 100644
--- a/indra/llwindow/llwindowmacosx-objc.h
+++ b/indra/llwindow/llwindowmacosx-objc.h
@@ -78,6 +78,8 @@ void initMainLoop();
void cleanupViewer();
void handleUrl(const char* url);
void dispatchUrl(std::string url);
+void startWatchdog(std::string_view state);
+void stopWatchdog();
/* Defined in llwindowmacosx-objc.mm: */
int createNSApp(int argc, const char **argv);
diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp
index 2bd9dd053c0..3cb2911f9a9 100644
--- a/indra/llwindow/llwindowwin32.cpp
+++ b/indra/llwindow/llwindowwin32.cpp
@@ -65,6 +65,7 @@
#include // std::pair
#include
+#include
#include
#include
@@ -115,7 +116,15 @@ static std::thread::id sMainThreadId;
LPWSTR gIconResource = IDI_APPLICATION;
LPWSTR gIconSmallResource = IDI_APPLICATION;
-LPDIRECTINPUT8 gDirectInput8;
+
+namespace
+{
+ LPDIRECTINPUT8 gDirectInput8;
+ ID3D11Device* gD3D11Device = nullptr;
+ ID3D11DeviceContext* gD3D11Context = nullptr;
+ LUID gExpectedAdapterLUID;
+ HMODULE gD3D11Library;
+}
LLW32MsgCallback gAsyncMsgCallback = NULL;
@@ -508,6 +517,10 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks,
//MAINT-516 -- force a load of opengl32.dll just in case windows went sideways
LoadLibrary(L"opengl32.dll");
+ // Request high-performance GPU before creating OpenGL context
+ // This increases probability of discrete GPU being used when
+ // the context is created.
+ requestHighPerformanceGPU();
if (mMaxCores != 0)
{
@@ -523,6 +536,8 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks,
SetProcessAffinityMask(hProcess, mask);
}
+ setThreadPriorityHigh();
+
#if 0 // this is probably a bad idea, but keep it in your back pocket if you see what looks like
// process deprioritization during profiles
// force high thread priority
@@ -545,41 +560,6 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks,
}
#endif
-#if 0 // this is also probably a bad idea, but keep it in your back pocket for getting main thread off of background thread cores (see also LLThread::threadRun)
- HANDLE hThread = GetCurrentThread();
-
- SYSTEM_INFO sysInfo;
-
- GetSystemInfo(&sysInfo);
- U32 core_count = sysInfo.dwNumberOfProcessors;
-
- if (max_cores != 0)
- {
- core_count = llmin(core_count, max_cores);
- }
-
- if (hThread)
- {
- int priority = GetThreadPriority(hThread);
-
- if (priority < THREAD_PRIORITY_TIME_CRITICAL)
- {
- if (SetThreadPriority(hThread, THREAD_PRIORITY_TIME_CRITICAL))
- {
- LL_INFOS() << "Set thread priority to THREAD_PRIORITY_TIME_CRITICAL" << LL_ENDL;
- }
- else
- {
- LL_INFOS() << "Failed to set thread priority: " << std::hex << GetLastError() << LL_ENDL;
- }
-
- // tell main thread to prefer core 0
- SetThreadIdealProcessor(hThread, 0);
- }
- }
-#endif
-
-
mFSAASamples = fsaa_samples;
mIconResource = gIconResource;
mIconSmallResource = gIconSmallResource;
@@ -1007,6 +987,7 @@ void LLWindowWin32::close()
}
mDragDrop->reset();
+ clearHighPerformanceGPURequest();
// Go back to screen mode written in the registry.
@@ -1071,6 +1052,52 @@ bool LLWindowWin32::isValid()
return (mWindowHandle != NULL);
}
+void LLWindowWin32::setThreadPriorityHigh()
+{
+ // Threads start at normal priority. But this is our main window/rendering thread,
+ // even if window handle belongs to another thread. So we can raise its priority
+ // to ensure better responsiveness and less blocking by lack of resources.
+ HANDLE hThread = GetCurrentThread();
+ if (hThread)
+ {
+ int priority = GetThreadPriority(hThread);
+
+ if (priority == THREAD_PRIORITY_ERROR_RETURN)
+ {
+ LL_WARNS_ONCE("Window") << "Failed to get thread priority: " << std::hex << GetLastError() << LL_ENDL;
+ }
+ else if (priority > THREAD_PRIORITY_HIGHEST)
+ {
+ // At the moment nothing should be setting 'critical' priority,
+ // but if that happens for some reason, we don't want to mess with it.
+ LL_WARNS("Window") << "setThreadPriorityHigh ignored, priority was " << (S32)priority << LL_ENDL;
+ }
+ else if (priority != THREAD_PRIORITY_HIGHEST)
+ {
+ if (SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST))
+ {
+ LL_DEBUGS("Window") << "Set thread priority to THREAD_PRIORITY_HIGHEST" << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("Window") << "Failed to set thread priority: " << std::hex << GetLastError() << LL_ENDL;
+ }
+ }
+ }
+}
+
+void LLWindowWin32::setThreadPriorityNormal()
+{
+ HANDLE hThread = GetCurrentThread();
+ if (hThread)
+ {
+ if (!SetThreadPriority(hThread, THREAD_PRIORITY_NORMAL))
+ {
+ LL_WARNS_ONCE("Window") << "Failed to set thread priority: " << std::hex << GetLastError() << LL_ENDL;
+ }
+ }
+}
+
bool LLWindowWin32::getVisible()
{
return (mWindowHandle && IsWindowVisible(mWindowHandle));
@@ -2578,6 +2605,150 @@ 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_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");
@@ -3041,19 +3212,31 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_
// means that the window was un-minimized.
if (w_param == SIZE_RESTORED && window_imp->mLastSizeWParam != SIZE_RESTORED)
{
- WINDOW_IMP_POST(window_imp->mCallbacks->handleActivate(window_imp, true));
+ window_imp->post([=]()
+ {
+ window_imp->setThreadPriorityHigh();
+ window_imp->mCallbacks->handleActivate(window_imp, true);
+ });
}
// handle case of window being maximized from fully minimized state
if (w_param == SIZE_MAXIMIZED && window_imp->mLastSizeWParam != SIZE_MAXIMIZED)
{
- WINDOW_IMP_POST(window_imp->mCallbacks->handleActivate(window_imp, true));
+ window_imp->post([=]()
+ {
+ window_imp->setThreadPriorityHigh();
+ window_imp->mCallbacks->handleActivate(window_imp, true);
+ });
}
// Also handle the minimization case
if (w_param == SIZE_MINIMIZED && window_imp->mLastSizeWParam != SIZE_MINIMIZED)
{
- WINDOW_IMP_POST(window_imp->mCallbacks->handleActivate(window_imp, false));
+ window_imp->post([=]()
+ {
+ window_imp->setThreadPriorityNormal();
+ window_imp->mCallbacks->handleActivate(window_imp, false);
+ });
}
// Actually resize all of our views
@@ -4618,6 +4801,238 @@ void LLWindowWin32::setDPIAwareness()
}
}
+void LLWindowWin32::requestHighPerformanceGPU() const
+{
+ // Try to load d3d11.dll and request high performance adapter
+ gD3D11Library = LoadLibraryA("d3d11.dll");
+ if (gD3D11Library)
+ {
+ typedef HRESULT(WINAPI* PFN_D3D11_CREATE_DEVICE)(
+ IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT,
+ const D3D_FEATURE_LEVEL*, UINT, UINT, ID3D11Device**,
+ D3D_FEATURE_LEVEL*, ID3D11DeviceContext**);
+
+ PFN_D3D11_CREATE_DEVICE pD3D11CreateDevice =
+ (PFN_D3D11_CREATE_DEVICE)GetProcAddress(gD3D11Library, "D3D11CreateDevice");
+
+ if (pD3D11CreateDevice)
+ {
+ // Try to enumerate adapters and select the best one
+ IDXGIFactory1* pFactory = nullptr;
+ IDXGIAdapter1* pSelectedAdapter = nullptr;
+ std::string selected_descr;
+ HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&pFactory);
+
+ if (SUCCEEDED(hr) && pFactory)
+ {
+ IDXGIAdapter1* pAdapter = nullptr;
+ SIZE_T maxDedicatedMemory = 0;
+ UINT adapterIndex = 0;
+ S32 adapter_count = 0;
+
+ // Enumerate all adapters and find the one with the most dedicated video memory
+ while (pFactory->EnumAdapters1(adapterIndex, &pAdapter) != DXGI_ERROR_NOT_FOUND)
+ {
+ DXGI_ADAPTER_DESC1 desc;
+ pAdapter->GetDesc1(&desc);
+
+ std::wstring description_w(desc.Description);
+ std::string description = ll_convert_wide_to_string(description_w);
+
+
+ // Skip software adapters
+ if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+ {
+ LL_DEBUGS("Window") << "Adapter " << adapterIndex << ": " << description
+ << ", Dedicated VRAM: " << (desc.DedicatedVideoMemory / 1024 / 1024) << " MB"
+ << ", Vendor: 0x" << std::hex << desc.VendorId << std::dec
+ << ", Flags: " << desc.Flags << LL_ENDL;
+ }
+ else
+ {
+ LL_INFOS("Window") << "Adapter " << adapterIndex << ": " << description
+ << ", Dedicated VRAM: " << (desc.DedicatedVideoMemory / 1024 / 1024) << " MB"
+ << ", Vendor: 0x" << std::hex << desc.VendorId << std::dec
+ << ", Flags: " << desc.Flags << LL_ENDL;
+
+ adapter_count++;
+ // Select adapter with most dedicated video memory (typically the discrete GPU)
+ if (desc.DedicatedVideoMemory > maxDedicatedMemory)
+ {
+ if (pSelectedAdapter)
+ {
+ pSelectedAdapter->Release();
+ }
+ pSelectedAdapter = pAdapter;
+ pSelectedAdapter->AddRef();
+ maxDedicatedMemory = desc.DedicatedVideoMemory;
+ gExpectedAdapterLUID = desc.AdapterLuid;
+ selected_descr = description;
+ }
+ }
+
+ pAdapter->Release();
+ adapterIndex++;
+ }
+ pFactory->Release();
+
+ if (adapter_count < 2)
+ {
+ // Only one adapter, no need to request high-performance GPU
+ if (pSelectedAdapter)
+ {
+ pSelectedAdapter->Release();
+ }
+ gExpectedAdapterLUID = { 0, 0 };
+ FreeLibrary(gD3D11Library);
+ gD3D11Library = nullptr;
+ return;
+ }
+
+ LL_INFOS("Window") << "Selected as preferred adapter (highest VRAM): " << selected_descr << LL_ENDL;
+ }
+
+ // Create a temporary device to ensure high-performance GPU is selected
+ // This initialization can help "wake up" the discrete GPU
+ D3D_FEATURE_LEVEL featureLevel;
+ D3D_FEATURE_LEVEL requestedLevels[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1 };
+
+ bool adapterSelected = (pSelectedAdapter != nullptr);
+ if (adapterSelected)
+ {
+ hr = pD3D11CreateDevice(
+ pSelectedAdapter,
+ D3D_DRIVER_TYPE_UNKNOWN,
+ nullptr,
+ 0,
+ requestedLevels,
+ _countof(requestedLevels),
+ D3D11_SDK_VERSION,
+ &gD3D11Device,
+ &featureLevel,
+ &gD3D11Context
+ );
+ pSelectedAdapter->Release();
+
+ if (!SUCCEEDED(hr))
+ {
+ LL_WARNS("Window") << "D3D11 failed to use preffered adapter " << selected_descr << LL_ENDL;
+ gExpectedAdapterLUID = { 0, 0 };
+ adapterSelected = false;
+ }
+ }
+
+ if (!adapterSelected)
+ {
+ // Either failed to select or didn't find an adapter.
+ hr = pD3D11CreateDevice(
+ nullptr,
+ D3D_DRIVER_TYPE_HARDWARE,
+ nullptr,
+ 0,
+ requestedLevels,
+ _countof(requestedLevels),
+ D3D11_SDK_VERSION,
+ &gD3D11Device,
+ &featureLevel,
+ &gD3D11Context
+ );
+ if (!SUCCEEDED(hr))
+ {
+ LL_WARNS("Window") << "D3D11 failed to use hardware adapter" << LL_ENDL;
+ FreeLibrary(gD3D11Library);
+ gD3D11Library = nullptr;
+ // These shouldn't be set, but make sure they are null.
+ gD3D11Device = nullptr;
+ gD3D11Context = nullptr;
+ }
+ }
+ }
+ else
+ {
+ LL_WARNS("Window") << "Failed to get D3D11CreateDevice function from d3d11.dll. High-performance GPU request failed." << LL_ENDL;
+ FreeLibrary(gD3D11Library);
+ gD3D11Library = nullptr;
+ }
+ }
+}
+
+bool LLWindowWin32::detectGPUChange() const
+{
+ if (!gD3D11Device)
+ {
+ // Can't detect without D3D11 device
+ return false;
+ }
+
+ if (gExpectedAdapterLUID.LowPart == 0 && gExpectedAdapterLUID.HighPart == 0)
+ {
+ // No specific adapter was selected, can't detect changes.
+ return false;
+ }
+
+ IDXGIDevice* pDXGIDevice = nullptr;
+ HRESULT hr = gD3D11Device->QueryInterface(__uuidof(IDXGIDevice), (void**)&pDXGIDevice);
+
+ if (SUCCEEDED(hr) && pDXGIDevice)
+ {
+ IDXGIAdapter* pCurrentAdapter = nullptr;
+ hr = pDXGIDevice->GetAdapter(&pCurrentAdapter);
+
+ if (SUCCEEDED(hr) && pCurrentAdapter)
+ {
+ DXGI_ADAPTER_DESC desc;
+ pCurrentAdapter->GetDesc(&desc);
+
+ std::wstring description_w(desc.Description);
+
+ bool changed = false;
+
+ // Check if LUID has changed
+ if (desc.AdapterLuid.LowPart != gExpectedAdapterLUID.LowPart ||
+ desc.AdapterLuid.HighPart != gExpectedAdapterLUID.HighPart)
+ {
+ changed = true;
+ std::string current_gpu_name = ll_convert_wide_to_string(description_w);
+ LL_WARNS("Window") << "GPU change detected! Current adapter: " << current_gpu_name << LL_ENDL;
+ }
+
+ pCurrentAdapter->Release();
+ pDXGIDevice->Release();
+
+ return changed;
+ }
+
+ if (pDXGIDevice)
+ {
+ pDXGIDevice->Release();
+ }
+ }
+
+ return false;
+}
+
+void LLWindowWin32::clearHighPerformanceGPURequest() const
+{
+ detectGPUChange();
+ gExpectedAdapterLUID = { 0, 0 };
+ if (gD3D11Context)
+ {
+ gD3D11Context->Release();
+ gD3D11Context = nullptr;
+ }
+ if (gD3D11Device)
+ {
+ gD3D11Device->Release();
+ gD3D11Device = nullptr;
+ }
+ if (gD3D11Library)
+ {
+ FreeLibrary(gD3D11Library);
+ gD3D11Library = nullptr;
+ }
+}
+
void* LLWindowWin32::getDirectInput8()
{
return &gDirectInput8;
@@ -4646,6 +5061,12 @@ bool LLWindowWin32::getInputDevices(U32 device_type_filter,
void LLWindowWin32::initWatchdog()
{
mWindowThread->initTimeout();
+
+ // Watchdog is effectively a 'login complete event', as the
+ // 'unstable' part is done and from now on we are tracking
+ // performance.
+ // No need to hold D3D11 context/device any more.
+ clearHighPerformanceGPURequest();
}
F32 LLWindowWin32::getSystemUISize()
diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h
index 8159092794a..afff3d5cb69 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
@@ -148,6 +154,8 @@ class LLWindowWin32 : public LLWindow
void initCursors();
HCURSOR loadColorCursor(LPCTSTR name);
bool isValid();
+ void setThreadPriorityHigh();
+ void setThreadPriorityNormal();
void moveWindow(const LLCoordScreen& position,const LLCoordScreen& size);
virtual LLSD getNativeKeyData();
@@ -170,6 +178,17 @@ class LLWindowWin32 : public LLWindow
void handleCompositionMessage(U32 indexes);
bool handleImeRequests(WPARAM request, LPARAM param, LRESULT *result);
+ // Additional function to request and hold a high-performance GPU on Windows 10+
+ //
+ // Laptops can dynamically switch between integrated and discrete GPUs.
+ // The Viewer has gpu-specific optimizations, and this switching can cause problems and crashes.
+ // The login screen requires low performance, which can lead to the OS deciding to switch to the integrated GPU.
+ // To avoid this, we request and hold a high-performance GPU using A D3D11 context until login.
+ // For diagnostics, we also log GPU changes.
+ void requestHighPerformanceGPU() const;
+ bool detectGPUChange() const;
+ void clearHighPerformanceGPURequest() const;
+
protected:
//
// Platform specific methods
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 05c71f5f5d8..17d344a3308 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -43,6 +43,7 @@ include(TinyEXR)
include(ThreeJS)
include(Tracy)
include(UI)
+include(Velopack)
include(ViewerMiscLibs)
include(ViewerManager)
include(VisualLeakDetector)
@@ -242,7 +243,6 @@ set(viewer_SOURCE_FILES
llfloaterhandler.cpp
llfloaterhelpbrowser.cpp
llfloaterhoverheight.cpp
- llfloaterhowto.cpp
llfloaterhud.cpp
llfloaterimagepreview.cpp
llfloaterimsessiontab.cpp
@@ -654,6 +654,7 @@ set(viewer_SOURCE_FILES
llurllineeditorctrl.cpp
llurlwhitelist.cpp
llversioninfo.cpp
+ llvvmquery.cpp
llviewchildren.cpp
llviewerassetstats.cpp
llviewerassetstorage.cpp
@@ -921,7 +922,6 @@ set(viewer_HEADER_FILES
llfloaterhandler.h
llfloaterhelpbrowser.h
llfloaterhoverheight.h
- llfloaterhowto.h
llfloaterhud.h
llfloaterimagepreview.h
llfloaterimnearbychat.h
@@ -1327,6 +1327,7 @@ set(viewer_HEADER_FILES
llurllineeditorctrl.h
llurlwhitelist.h
llversioninfo.h
+ llvvmquery.h
llviewchildren.h
llviewerassetstats.h
llviewerassetstorage.h
@@ -1446,6 +1447,8 @@ if (DARWIN)
LIST(APPEND viewer_SOURCE_FILES llappviewermacosx-objc.h)
LIST(APPEND viewer_SOURCE_FILES llfilepicker_mac.mm)
LIST(APPEND viewer_HEADER_FILES llfilepicker_mac.h)
+ LIST(APPEND viewer_SOURCE_FILES llvelopack.cpp)
+ LIST(APPEND viewer_HEADER_FILES llvelopack.h)
set_source_files_properties(
llappviewermacosx-objc.mm
@@ -1508,16 +1511,19 @@ if (WINDOWS)
list(APPEND viewer_SOURCE_FILES
llappviewerwin32.cpp
+ llvelopack.cpp
llwindebug.cpp
)
set_source_files_properties(
llappviewerwin32.cpp
+ llvelopack.cpp
PROPERTIES
COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
)
list(APPEND viewer_HEADER_FILES
llappviewerwin32.h
+ llvelopack.h
llwindebug.h
)
@@ -1931,6 +1937,7 @@ if (WINDOWS)
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
@@ -2054,6 +2061,10 @@ if (USE_DISCORD)
target_link_libraries(${VIEWER_BINARY_NAME} ll::discord_sdk )
endif ()
+if (TARGET ll::velopack)
+ target_link_libraries(${VIEWER_BINARY_NAME} ll::velopack )
+endif ()
+
if( TARGET ll::intel_memops )
target_link_libraries(${VIEWER_BINARY_NAME} ll::intel_memops )
endif()
@@ -2251,9 +2262,11 @@ if (DARWIN)
--arch=${ARCH}
--artwork=${ARTWORK_DIR}
"--bugsplat=${BUGSPLAT_DB}"
+ --bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
@@ -2481,4 +2494,3 @@ if (LL_TESTS)
endif (LL_TESTS)
check_message_template(${VIEWER_BINARY_NAME})
-
diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt
index 2aaedf99442..124b7a2cd06 100644
--- a/indra/newview/VIEWER_VERSION.txt
+++ b/indra/newview/VIEWER_VERSION.txt
@@ -1 +1 @@
-26.1.0
+26.1.1
diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml
index 7bcfecf9faa..952818144d7 100644
--- a/indra/newview/app_settings/commands.xml
+++ b/indra/newview/app_settings/commands.xml
@@ -83,16 +83,6 @@
is_running_function="Floater.IsOpen"
is_running_parameters="gestures"
/>
-
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
@@ -4248,7 +4226,17 @@
Value
0.0.0
-
+ PreviousInstallChecked
+
+ Comment
+ Whether viewer checked previous install on the same channel for NSIS
+ Persist
+ 1
+ Type
+ Boolean
+ Value
+ 0
+
LimitDragDistance
Comment
@@ -9009,6 +8997,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
@@ -13090,11 +13089,11 @@
UpdaterShowReleaseNotes
Comment
- Enables displaying of the Release notes in a web floater after update.
+ Enables displaying of the Release notes in a web floater after update. 0 - don't show, 1 - show, 2 - show even for test viewers
Persist
1
Type
- Boolean
+ S32
Value
1
@@ -13912,7 +13911,7 @@
Type
S32
Value
- 1
+ 0
WaterGLFogDensityScale
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..4e52c82e866 100644
--- a/indra/newview/gltf/llgltfloader.cpp
+++ b/indra/newview/gltf/llgltfloader.cpp
@@ -1551,6 +1551,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 +1572,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;
}
}
diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi
index ae40e8830fb..07ed0d0824b 100644
--- a/indra/newview/installers/windows/installer_template.nsi
+++ b/indra/newview/installers/windows/installer_template.nsi
@@ -939,21 +939,7 @@ Function .onInstSuccess
Call CheckWindowsServPack # Warn if not on the latest SP before asking to launch.
StrCmp $SKIP_AUTORUN "true" +2;
- # Assumes SetOutPath $INSTDIR
- # Run INSTEXE (our updater), passing VIEWER_EXE plus the command-line
- # arguments built into our shortcuts. This gives the updater a chance
- # to verify that the viewer we just installed is appropriate for the
- # running system -- or, if not, to download and install a different
- # viewer. For instance, if a user running 32-bit Windows installs a
- # 64-bit viewer, it cannot run on this system. But since the updater
- # is a 32-bit executable even in the 64-bit viewer package, the
- # updater can detect the problem and adapt accordingly.
- # Once everything is in order, the updater will run the specified
- # viewer with the specified params.
- # Quote the updater executable and the viewer executable because each
- # must be a distinct command-line token, but DO NOT quote the language
- # string because it must decompose into separate command-line tokens.
- Exec '"$INSTDIR\$INSTEXE" precheck "$INSTDIR\$VIEWER_EXE" $SHORTCUT_LANG_PARAM'
+ Exec '"$INSTDIR\$VIEWER_EXE" $SHORTCUT_LANG_PARAM'
#
FunctionEnd
diff --git a/indra/newview/llappdelegate-objc.mm b/indra/newview/llappdelegate-objc.mm
index 0b3d0355a27..d3aac5a43e7 100644
--- a/indra/newview/llappdelegate-objc.mm
+++ b/indra/newview/llappdelegate-objc.mm
@@ -395,11 +395,13 @@ @implementation LLApplication
- (void)sendEvent:(NSEvent *)event
{
+ startWatchdog("sendEvent"); // events are outside pumpMainLoop
[super sendEvent:event];
if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand))
{
[[self keyWindow] sendEvent:event];
}
+ stopWatchdog(); // leaving scope
}
@end
diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp
index f65aaccddc6..4f549314af5 100644
--- a/indra/newview/llappearancemgr.cpp
+++ b/indra/newview/llappearancemgr.cpp
@@ -703,12 +703,12 @@ class LLWearableHoldingPattern
void onFetchCompletion();
bool isFetchCompleted();
bool isTimedOut();
+ bool pollStopped();
void checkMissingWearables();
bool pollMissingWearables();
bool isMissingCompleted();
void recoverMissingWearable(LLWearableType::EType type);
- void clearCOFLinksForMissingWearables();
void onWearableAssetFetch(LLViewerWearable *wearable);
void onAllComplete();
@@ -717,7 +717,6 @@ class LLWearableHoldingPattern
found_list_t& getFoundList();
void eraseTypeToLink(LLWearableType::EType type);
void eraseTypeToRecover(LLWearableType::EType type);
- void setObjItems(const LLInventoryModel::item_array_t& items);
void setGestItems(const LLInventoryModel::item_array_t& items);
bool isMostRecent();
void handleLateArrivals();
@@ -727,7 +726,6 @@ class LLWearableHoldingPattern
private:
found_list_t mFoundList;
- LLInventoryModel::item_array_t mObjItems;
LLInventoryModel::item_array_t mGestItems;
typedef std::set type_set_t;
type_set_t mTypesToRecover;
@@ -804,11 +802,6 @@ void LLWearableHoldingPattern::eraseTypeToRecover(LLWearableType::EType type)
mTypesToRecover.erase(type);
}
-void LLWearableHoldingPattern::setObjItems(const LLInventoryModel::item_array_t& items)
-{
- mObjItems = items;
-}
-
void LLWearableHoldingPattern::setGestItems(const LLInventoryModel::item_array_t& items)
{
mGestItems = items;
@@ -914,55 +907,10 @@ void LLWearableHoldingPattern::onAllComplete()
if (isAgentAvatarValid())
{
- LL_DEBUGS("Avatar") << self_av_string() << "Updating " << mObjItems.size() << " attachments" << LL_ENDL;
- LLAgentWearables::llvo_vec_t objects_to_remove;
- LLAgentWearables::llvo_vec_t objects_to_retain;
- LLInventoryModel::item_array_t items_to_add;
-
- LLAgentWearables::findAttachmentsAddRemoveInfo(mObjItems,
- objects_to_remove,
- objects_to_retain,
- items_to_add);
-
- LL_DEBUGS("Avatar") << self_av_string() << "Removing " << objects_to_remove.size()
- << " attachments" << LL_ENDL;
-
- // Here we remove the attachment pos overrides for *all*
- // attachments, even those that are not being removed. This is
- // needed to get joint positions all slammed down to their
- // pre-attachment states.
- gAgentAvatarp->clearAttachmentOverrides();
-
- if (objects_to_remove.size() || items_to_add.size())
- {
- LL_DEBUGS("Avatar") << "ATT will remove " << objects_to_remove.size()
- << " and add " << items_to_add.size() << " items" << LL_ENDL;
- }
-
- // Take off the attachments that will no longer be in the outfit.
- LLAgentWearables::userRemoveMultipleAttachments(objects_to_remove);
-
// Update wearables.
LL_INFOS("Avatar") << self_av_string() << "HP " << index() << " updating agent wearables with "
<< mResolved << " wearable items " << LL_ENDL;
LLAppearanceMgr::instance().updateAgentWearables(this);
-
- // Restore attachment pos overrides for the attachments that
- // are remaining in the outfit.
- for (LLAgentWearables::llvo_vec_t::iterator it = objects_to_retain.begin();
- it != objects_to_retain.end();
- ++it)
- {
- LLViewerObject *objectp = *it;
- if (!objectp->isAnimatedObject())
- {
- gAgentAvatarp->addAttachmentOverridesForObject(objectp);
- }
- }
-
- // Add new attachments to match those requested.
- LL_DEBUGS("Avatar") << self_av_string() << "Adding " << items_to_add.size() << " attachments" << LL_ENDL;
- LLAgentWearables::userAttachMultipleAttachments(items_to_add);
}
if (isFetchCompleted() && isMissingCompleted())
@@ -1000,6 +948,10 @@ bool LLWearableHoldingPattern::pollFetchCompletion()
{
// 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
+ doOnIdleRepeating(boost::bind(&LLWearableHoldingPattern::pollStopped, this));
+ return true;
}
bool completed = isFetchCompleted();
@@ -1028,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;
@@ -1068,8 +1022,10 @@ 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
+ return;
}
LL_DEBUGS("Avatar") << self_av_string() << "Recovered item for type " << type << LL_ENDL;
@@ -1120,18 +1076,15 @@ bool LLWearableHoldingPattern::isMissingCompleted()
return mTypesToLink.size()==0 && mTypesToRecover.size()==0;
}
-void LLWearableHoldingPattern::clearCOFLinksForMissingWearables()
+bool LLWearableHoldingPattern::pollStopped()
{
- for (found_list_t::iterator it = getFoundList().begin(); it != getFoundList().end(); ++it)
+ // We have to keep on polling until we're sure that all callbacks have completed or they'll cause a crash
+ if (isFetchCompleted() && isMissingCompleted())
{
- LLFoundData &data = *it;
- if ((data.mWearableType < LLWearableType::WT_COUNT) && (!data.mWearable))
- {
- // Wearable link that was never resolved; remove links to it from COF
- LL_INFOS("Avatar") << self_av_string() << "HP " << index() << " removing link for unresolved item " << data.mItemID.asString() << LL_ENDL;
- LLAppearanceMgr::instance().removeCOFItemLinks(data.mItemID);
- }
+ delete this;
+ return true;
}
+ return false;
}
bool LLWearableHoldingPattern::pollMissingWearables()
@@ -1140,6 +1093,10 @@ bool LLWearableHoldingPattern::pollMissingWearables()
{
// 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
+ doOnIdleRepeating(boost::bind(&LLWearableHoldingPattern::pollStopped, this));
+ return true;
}
bool timed_out = isTimedOut();
@@ -1164,14 +1121,6 @@ bool LLWearableHoldingPattern::pollMissingWearables()
gAgentAvatarp->debugWearablesLoaded();
- // BAP - if we don't call clearCOFLinksForMissingWearables()
- // here, we won't have to add the link back in later if the
- // wearable arrives late. This is to avoid corruption of
- // wearable ordering info. Also has the effect of making
- // unworn item links visible in the COF under some
- // circumstances.
-
- //clearCOFLinksForMissingWearables();
onAllComplete();
}
return done;
@@ -1218,13 +1167,6 @@ void LLWearableHoldingPattern::handleLateArrivals()
replaced_types.insert(data.mWearableType);
- // BAP - if we didn't call
- // clearCOFLinksForMissingWearables() earlier, we
- // don't need to restore the link here. Fixes
- // wearable ordering problems.
-
- // LLAppearanceMgr::instance().addCOFItemLink(data.mItemID,false);
-
// BAP failing this means inventory or asset server
// are corrupted in a way we don't handle.
llassert((data.mWearableType < LLWearableType::WT_COUNT) && (wearable->getType() == data.mWearableType));
@@ -2143,15 +2085,23 @@ void LLAppearanceMgr::purgeBaseOutfitLink(const LLUUID& category, LLPointer 0) && (items.size() > max_total))
{
- LLInventoryModel::item_array_t items_to_keep;
- for (S32 i=0; igetType() == LLAssetType::AT_BODYPART)
+ {
+ items_to_keep.push_back(item);
+ }
+ else if (non_body_kept < max_total)
+ {
+ items_to_keep.push_back(item);
+ non_body_kept++;
+ }
}
items = items_to_keep;
}
@@ -2636,6 +2586,11 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions,
remove_non_link_items(wear_items);
remove_non_link_items(obj_items);
remove_non_link_items(gest_items);
+ // Since we're following folder links we might have picked up new duplicates, or exceeded MAX_CLOTHING_LAYERS
+ removeDuplicateItems(wear_items);
+ removeDuplicateItems(obj_items);
+ removeDuplicateItems(gest_items);
+ filterWearableItems(wear_items, 0, LLAgentWearables::MAX_CLOTHING_LAYERS, true);
dumpItemArray(wear_items,"asset_dump: wear_item");
dumpItemArray(obj_items,"asset_dump: obj_item");
@@ -2647,6 +2602,77 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions,
<< " descendent_count " << cof->getDescendentCount()
<< " viewer desc count " << cof->getViewerDescendentCount() << LL_ENDL;
}
+
+ // Update attachments to match those requested.
+ if (isAgentAvatarValid())
+ {
+ // Include attachments which should be in COF but don't have their link created yet
+ std::set pendingAttachments;
+ LLAttachmentsMgr::instance().getPendingAttachments(pendingAttachments);
+ for (const LLUUID& idAttachItem : pendingAttachments)
+ {
+ if ( !gAgentAvatarp->isWearingAttachment(idAttachItem) || isLinkedInCOF(idAttachItem) )
+ {
+ LLAttachmentsMgr::instance().clearPendingAttachmentLink(idAttachItem);
+ continue;
+ }
+
+ if (LLViewerInventoryItem* pAttachItem = gInventory.getItem(idAttachItem))
+ {
+ obj_items.push_back(pAttachItem);
+ }
+ }
+
+ LL_DEBUGS("Avatar") << self_av_string() << "Updating " << obj_items.size() << " attachments" << LL_ENDL;
+ LLAgentWearables::llvo_vec_t objects_to_remove;
+ LLAgentWearables::llvo_vec_t objects_to_retain;
+ LLInventoryModel::item_array_t items_to_add;
+
+ LLAgentWearables::findAttachmentsAddRemoveInfo(obj_items,
+ objects_to_remove,
+ objects_to_retain,
+ items_to_add);
+
+ LL_DEBUGS("Avatar") << self_av_string() << "Removing " << objects_to_remove.size()
+ << " attachments" << LL_ENDL;
+
+ // Here we remove the attachment pos overrides for *all*
+ // attachments, even those that are not being removed. This is
+ // needed to get joint positions all slammed down to their
+ // pre-attachment states.
+ gAgentAvatarp->clearAttachmentOverrides();
+
+ if (objects_to_remove.size() || items_to_add.size())
+ {
+ LL_DEBUGS("Avatar") << "ATT will remove " << objects_to_remove.size()
+ << " and add " << items_to_add.size() << " items" << LL_ENDL;
+ }
+
+ // Take off the attachments that will no longer be in the outfit.
+ // (don't remove attachments until avatar is fully loaded - reduces random attaching/detaching/reattaching at log-on)
+ if (gAgentAvatarp->isFullyLoaded())
+ {
+ LLAgentWearables::userRemoveMultipleAttachments(objects_to_remove);
+ }
+
+ // Restore attachment pos overrides for the attachments that
+ // are remaining in the outfit.
+ for (LLAgentWearables::llvo_vec_t::iterator it = objects_to_retain.begin();
+ it != objects_to_retain.end();
+ ++it)
+ {
+ LLViewerObject *objectp = *it;
+ if (!objectp->isAnimatedObject())
+ {
+ gAgentAvatarp->addAttachmentOverridesForObject(objectp);
+ }
+ }
+
+ // Add new attachments to match those requested.
+ LL_DEBUGS("Avatar") << self_av_string() << "Adding " << items_to_add.size() << " attachments" << LL_ENDL;
+ LLAgentWearables::userAttachMultipleAttachments(items_to_add);
+ }
+
if(!wear_items.size())
{
LLNotificationsUtil::add("CouldNotPutOnOutfit");
@@ -2661,7 +2687,6 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions,
LLTimer hp_block_timer;
LLWearableHoldingPattern* holder = new LLWearableHoldingPattern;
- holder->setObjItems(obj_items);
holder->setGestItems(gest_items);
// Note: can't do normal iteration, because if all the
@@ -4201,6 +4226,7 @@ void LLAppearanceMgr::removeItemsFromAvatar(const uuid_vec_t& ids_to_remove, nul
continue;
}
removeCOFItemLinks(linked_item_id, cb);
+ LLAttachmentsMgr::instance().clearPendingAttachmentLink(linked_item_id);
addDoomedTempAttachment(linked_item_id);
}
}
diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h
index 131b6817edc..9d90be00045 100644
--- a/indra/newview/llappearancemgr.h
+++ b/indra/newview/llappearancemgr.h
@@ -151,6 +151,7 @@ class LLAppearanceMgr: public LLSingleton
// Attachment link management
void unregisterAttachment(const LLUUID& item_id);
void registerAttachment(const LLUUID& item_id);
+ bool getAttachmentInvLinkEnable() const { return mAttachmentInvLinkEnabled; }
void setAttachmentInvLinkEnable(bool val);
// Add COF link to individual item.
@@ -249,7 +250,7 @@ class LLAppearanceMgr: public LLSingleton
private:
- void filterWearableItems(LLInventoryModel::item_array_t& items, S32 max_per_type, S32 max_total);
+ void filterWearableItems(LLInventoryModel::item_array_t& items, S32 max_per_type, S32 max_total, bool skip_bodyparts = false);
void getDescendentsOfAssetType(const LLUUID& category,
LLInventoryModel::item_array_t& items,
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index bae9749772f..46e64c2a4ea 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -98,6 +98,11 @@
#include "llurlmatch.h"
#include "lltextutil.h"
#include "lllogininstance.h"
+#include "llvvmquery.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
#include "llprogressview.h"
#include "llvocache.h"
#include "lldiskcache.h"
@@ -379,9 +384,6 @@ const std::string ERROR_MARKER_FILE_NAME("SecondLife.error_marker");
const std::string LOGOUT_MARKER_FILE_NAME("SecondLife.logout_marker");
static std::string gLaunchFileOnQuit;
-// Used on Win32 for other apps to identify our window (eg, win_setup)
-const char* const VIEWER_WINDOW_CLASSNAME = "Second Life";
-
//----------------------------------------------------------------------------
// List of entries from strings.xml to always replace
@@ -653,7 +655,6 @@ LLAppViewer::LLAppViewer()
mPurgeCacheOnExit(false),
mPurgeUserDataOnExit(false),
mSecondInstance(false),
- mUpdaterNotFound(false),
mSavedFinalSnapshot(false),
mSavePerAccountSettings(false), // don't save settings on logout unless login succeeded.
mQuitRequested(false),
@@ -961,6 +962,7 @@ bool LLAppViewer::init()
// Initialize event recorder
LLViewerEventRecorder::createInstance();
+ LLWatchdog::createInstance();
//
// Initialize the window
@@ -1115,68 +1117,17 @@ bool LLAppViewer::init()
gGLActive = false;
-#if LL_RELEASE_FOR_DOWNLOAD
- // Skip updater if this is a non-interactive instance
+//#if LL_RELEASE_FOR_DOWNLOAD
+ // Launch VVM update check
if (!gSavedSettings.getBOOL("CmdLineSkipUpdater") && !gNonInteractive)
{
- LLProcess::Params updater;
- updater.desc = "updater process";
- // Because it's the updater, it MUST persist beyond the lifespan of the
- // viewer itself.
- updater.autokill = false;
- std::string updater_file;
-#if LL_WINDOWS
- updater_file = "SLVersionChecker.exe";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#elif LL_DARWIN
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->add(gDirUtilp->getAppRODataDir(), "updater", updater_file);
-#else
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#endif
- // add LEAP mode command-line argument to whichever of these we selected
- updater.args.add("leap");
- // UpdaterServiceSettings
- if (gSavedSettings.getBOOL("FirstLoginThisInstall"))
- {
- // Befor first login, treat this as 'manual' updates,
- // updater won't install anything, but required updates
- updater.args.add("0");
- }
- else
- {
- updater.args.add(stringize(gSavedSettings.getU32("UpdaterServiceSetting")));
- }
- // channel
- updater.args.add(LLVersionInfo::instance().getChannel());
- // testok
- updater.args.add(stringize(gSavedSettings.getBOOL("UpdaterWillingToTest")));
- // ForceAddressSize
- updater.args.add(stringize(gSavedSettings.getU32("ForceAddressSize")));
-
- try
- {
- // Run the updater. An exception from launching the updater should bother us.
- LLLeap::create(updater, true);
- mUpdaterNotFound = false;
- }
- catch (...)
- {
- LLUIString details = LLNotifications::instance().getGlobalString("LLLeapUpdaterFailure");
- details.setArg("[UPDATER_APP]", updater_file);
- OSMessageBox(
- details.getString(),
- LLStringUtil::null,
- OSMB_OK);
- mUpdaterNotFound = true;
- }
+ initVVMUpdateCheck();
}
else
{
LL_WARNS("InitInfo") << "Skipping updater check." << LL_ENDL;
}
-#endif //LL_RELEASE_FOR_DOWNLOAD
+//#endif //LL_RELEASE_FOR_DOWNLOAD
{
// Iterate over --leap command-line options. But this is a bit tricky: if
@@ -1714,6 +1665,16 @@ void LLAppViewer::flushLFSIO()
bool LLAppViewer::cleanup()
{
+#if LL_VELOPACK
+ // Apply any pending Velopack update before shutdown
+ if (velopack_is_update_pending())
+ {
+ LL_INFOS("AppInit") << "Applying pending Velopack update on shutdown..." << LL_ENDL;
+ velopack_apply_pending_update(velopack_should_restart_after_update());
+ }
+ velopack_cleanup();
+#endif
+
//ditch LLVOAvatarSelf instance
gAgentAvatarp = NULL;
@@ -3158,7 +3119,7 @@ bool LLAppViewer::initWindow()
LLViewerWindow::Params window_params;
window_params
.title(gWindowTitle)
- .name(VIEWER_WINDOW_CLASSNAME)
+ .name(sWindowClass)
.x(gSavedSettings.getS32("WindowX"))
.y(gSavedSettings.getS32("WindowY"))
.width(gSavedSettings.getU32("WindowWidth"))
@@ -3283,16 +3244,6 @@ bool LLAppViewer::initWindow()
return true;
}
-bool LLAppViewer::isUpdaterMissing()
-{
- return mUpdaterNotFound;
-}
-
-bool LLAppViewer::waitForUpdater()
-{
- return !gSavedSettings.getBOOL("CmdLineSkipUpdater") && !mUpdaterNotFound && !gNonInteractive;
-}
-
void LLAppViewer::writeDebugInfo(bool isStatic)
{
#if LL_WINDOWS && LL_BUGSPLAT
diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h
index e1119419af5..c977757e486 100644
--- a/indra/newview/llappviewer.h
+++ b/indra/newview/llappviewer.h
@@ -117,9 +117,6 @@ class LLAppViewer : public LLApp
bool quitRequested() { return mQuitRequested; }
bool logoutRequestSent() { return mLogoutRequestSent; }
bool isSecondInstance() { return mSecondInstance; }
- bool isUpdaterMissing(); // In use by tests
- bool waitForUpdater();
-
void writeDebugInfo(bool isStatic=true);
void setServerReleaseNotesURL(const std::string& url) { mServerReleaseNotesURL = url; }
@@ -287,6 +284,14 @@ class LLAppViewer : public LLApp
virtual void sendOutOfDiskSpaceNotification();
+protected:
+
+ // NSIS relies on this to detect if viewer is up.
+ // NSIS's method is somewhat unreliable since window
+ // can close long before cleanup is done.
+ // sendURLToOtherInstance also relies on this to detect if viewer is up.
+ static constexpr const char* sWindowClass = "Second Life";
+
private:
bool doFrame();
@@ -327,7 +332,6 @@ class LLAppViewer : public LLApp
static LLAppViewer* sInstance;
bool mSecondInstance; // Is this a second instance of the app?
- bool mUpdaterNotFound; // True when attempt to start updater failed
std::string mMarkerFileName;
LLAPRFile mMarkerFile; // A file created to indicate the app is running.
diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp
index b074c40c175..2bd9c8661a5 100644
--- a/indra/newview/llappviewermacosx.cpp
+++ b/indra/newview/llappviewermacosx.cpp
@@ -135,6 +135,16 @@ void cleanupViewer()
gViewerAppPtr = NULL;
}
+void startWatchdog(std::string_view state)
+{
+ gViewerAppPtr->resumeMainloopTimeout(state);
+}
+
+void stopWatchdog()
+{
+ gViewerAppPtr->pauseMainloopTimeout();
+}
+
void clearDumpLogsDir()
{
if (!LLAppViewer::instance()->isSecondInstance())
diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp
index 0620b625d9b..0dee17010ce 100644
--- a/indra/newview/llappviewerwin32.cpp
+++ b/indra/newview/llappviewerwin32.cpp
@@ -72,6 +72,11 @@
#include
#include
+// Velopack installer and update framework
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
// Bugsplat (http://bugsplat.com) crash reporting tool
#ifdef LL_BUGSPLAT
#include "BugSplat.h"
@@ -220,7 +225,6 @@ LONG WINAPI catchallCrashHandler(EXCEPTION_POINTERS * /*ExceptionInfo*/)
return 0;
}
-const std::string LLAppViewerWin32::sWindowClass = "Second Life";
/*
This function is used to print to the command line a text message
@@ -424,6 +428,31 @@ int APIENTRY WINMAIN(HINSTANCE hInstance,
PWSTR pCmdLine,
int nCmdShow)
{
+#if LL_VELOPACK
+ // Velopack MUST be initialized first - it may handle install/uninstall
+ // commands and exit the process before we do anything else.
+ if (!velopack_initialize())
+ {
+ // Velopack handled the invocation (install/uninstall hook)
+
+ // Drop install related settings
+ gDirUtilp->initAppDirs("SecondLife");
+
+ std::string user_settings_path = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "settings.xml");
+ LLControlGroup settings("global");
+ if (settings.loadFromFile(user_settings_path))
+ {
+ // If user reinstalls or updates, we want to recheck for nsis leftovers.
+ if (settings.controlExists("PreviousInstallChecked"))
+ {
+ settings.setBOOL("PreviousInstallChecked", false);
+ }
+ settings.saveToFile(user_settings_path, true);
+ }
+ return 0;
+ }
+#endif
+
// Call Tracy first thing to have it allocate memory
// https://github.com/wolfpld/tracy/issues/196
LL_PROFILER_FRAME_END;
@@ -933,14 +962,14 @@ bool LLAppViewerWin32::restoreErrorTrap()
bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url)
{
wchar_t window_class[256]; /* Flawfinder: ignore */ // Assume max length < 255 chars.
- mbstowcs(window_class, sWindowClass.c_str(), 255);
+ mbstowcs(window_class, sWindowClass, 255);
window_class[255] = 0;
// Use the class instead of the window name.
HWND other_window = FindWindow(window_class, NULL);
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;
@@ -948,13 +977,231 @@ 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;
}
+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 3fad53ec72b..d0a3f2f5ce2 100644
--- a/indra/newview/llappviewerwin32.h
+++ b/indra/newview/llappviewerwin32.h
@@ -45,6 +45,9 @@ class LLAppViewerWin32 : public LLAppViewer
bool reportCrashToBugsplat(void* pExcepInfo) 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.
@@ -59,8 +62,6 @@ class LLAppViewerWin32 : public LLAppViewer
std::string generateSerialNumber();
- static const std::string sWindowClass;
-
private:
void disableWinErrorReporting();
diff --git a/indra/newview/llattachmentsmgr.cpp b/indra/newview/llattachmentsmgr.cpp
index 8b5db2c0fa7..f73d4241c9a 100644
--- a/indra/newview/llattachmentsmgr.cpp
+++ b/indra/newview/llattachmentsmgr.cpp
@@ -42,10 +42,19 @@ const F32 MAX_ATTACHMENT_REQUEST_LIFETIME = 30.0F;
const F32 MIN_RETRY_REQUEST_TIME = 5.0F;
const F32 MAX_BAD_COF_TIME = 30.0F;
+class LLRegisterAttachmentCallback : public LLRequestServerAppearanceUpdateOnDestroy
+{
+public:
+ void fire(const LLUUID& item_id) override
+ {
+ LLAttachmentsMgr::instance().onRegisterAttachmentComplete(item_id);
+ LLRequestServerAppearanceUpdateOnDestroy::fire(item_id);
+ }
+};
+
LLAttachmentsMgr::LLAttachmentsMgr():
mAttachmentRequests("attach",MIN_RETRY_REQUEST_TIME),
- mDetachRequests("detach",MIN_RETRY_REQUEST_TIME),
- mQuestionableCOFLinks("badcof",MAX_BAD_COF_TIME)
+ mDetachRequests("detach",MIN_RETRY_REQUEST_TIME)
{
}
@@ -80,6 +89,9 @@ void LLAttachmentsMgr::addAttachmentRequest(const LLUUID& item_id,
void LLAttachmentsMgr::onAttachmentRequested(const LLUUID& item_id)
{
+ if (item_id.isNull())
+ return;
+
LLViewerInventoryItem *item = gInventory.getItem(item_id);
LL_DEBUGS("Avatar") << "ATT attachment was requested "
<< (item ? item->getName() : "UNKNOWN") << " id " << item_id << LL_ENDL;
@@ -113,8 +125,6 @@ void LLAttachmentsMgr::onIdle()
expireOldDetachRequests();
- checkInvalidCOFLinks();
-
spamStatusInfo();
}
@@ -222,6 +232,11 @@ void LLAttachmentsMgr::linkRecentlyArrivedAttachments()
{
if (mRecentlyArrivedAttachments.size())
{
+ if (!LLAppearanceMgr::instance().getAttachmentInvLinkEnable())
+ {
+ return;
+ }
+
// One or more attachments have arrived but have not yet been
// processed for COF links
if (mAttachmentRequests.empty())
@@ -268,17 +283,49 @@ void LLAttachmentsMgr::linkRecentlyArrivedAttachments()
}
if (ids_to_link.size())
{
- LLPointer cb = new LLRequestServerAppearanceUpdateOnDestroy();
- for (uuid_vec_t::const_iterator uuid_it = ids_to_link.begin();
- uuid_it != ids_to_link.end(); ++uuid_it)
+ LLPointer cb = new LLRegisterAttachmentCallback();
+ for (const LLUUID& id_item: ids_to_link)
{
- LLAppearanceMgr::instance().addCOFItemLink(*uuid_it, cb);
+ if (std::find(mPendingAttachLinks.begin(), mPendingAttachLinks.end(), id_item) == mPendingAttachLinks.end())
+ {
+ LLAppearanceMgr::instance().addCOFItemLink(id_item, cb);
+ mPendingAttachLinks.insert(id_item);
+ }
}
}
mRecentlyArrivedAttachments.clear();
}
}
+bool LLAttachmentsMgr::getPendingAttachments(std::set& ids) const
+{
+ ids.clear();
+
+ // Returns the combined set of attachments that are pending link creation and those that currently have an ongoing link creation process.
+ set_union(mRecentlyArrivedAttachments.begin(), mRecentlyArrivedAttachments.end(), mPendingAttachLinks.begin(), mPendingAttachLinks.end(), std::inserter(ids, ids.begin()));
+
+ return !ids.empty();
+}
+
+void LLAttachmentsMgr::clearPendingAttachmentLink(const LLUUID& idItem)
+{
+ mPendingAttachLinks.erase(idItem);
+}
+
+void LLAttachmentsMgr::onRegisterAttachmentComplete(const LLUUID& id_item_link)
+{
+ if (const LLUUID& id_item = gInventory.getLinkedItemID(id_item_link); id_item != id_item_link)
+ {
+ clearPendingAttachmentLink(id_item);
+
+ // It may have been detached already in which case we should remove the COF link
+ if ( isAgentAvatarValid() && !gAgentAvatarp->isWearingAttachment(id_item) )
+ {
+ LLAppearanceMgr::instance().removeCOFItemLinks(id_item);
+ }
+ }
+}
+
LLAttachmentsMgr::LLItemRequestTimes::LLItemRequestTimes(const std::string& op_name, F32 timeout):
mOpName(op_name),
mTimeout(timeout)
@@ -407,6 +454,8 @@ void LLAttachmentsMgr::onDetachRequested(const LLUUID& inv_item_id)
void LLAttachmentsMgr::onDetachCompleted(const LLUUID& inv_item_id)
{
+ clearPendingAttachmentLink(inv_item_id);
+
LLTimer timer;
LLInventoryItem *item = gInventory.getItem(inv_item_id);
if (mDetachRequests.getTime(inv_item_id, timer))
@@ -428,10 +477,6 @@ void LLAttachmentsMgr::onDetachCompleted(const LLUUID& inv_item_id)
{
LL_DEBUGS("Avatar") << "ATT detach on shutdown for " << (item ? item->getName() : "UNKNOWN") << " " << inv_item_id << LL_ENDL;
}
-
- LL_DEBUGS("Avatar") << "ATT detached item flagging as questionable for COF link checking "
- << (item ? item->getName() : "UNKNOWN") << " id " << inv_item_id << LL_ENDL;
- mQuestionableCOFLinks.addTime(inv_item_id);
}
bool LLAttachmentsMgr::isAttachmentStateComplete() const
@@ -440,81 +485,8 @@ bool LLAttachmentsMgr::isAttachmentStateComplete() const
&& mAttachmentRequests.empty()
&& mDetachRequests.empty()
&& mRecentlyArrivedAttachments.empty()
- && mQuestionableCOFLinks.empty();
-}
-
-// Check for attachments that are (a) linked in COF and (b) not
-// attached to the avatar. This is a rotten function to have to
-// include, because it runs the risk of either repeatedly spamming out
-// COF link removals if they're failing for some reason, or getting
-// into a tug of war with some other sequence of events that's in the
-// process of adding the attachment in question. However, it's needed
-// because we have no definitive source of authority for what things
-// are actually supposed to be attached. Scripts, run on the server
-// side, can remove an attachment without our expecting it. If this
-// happens to an attachment that's just been added, then the COF link
-// creation may still be in flight, and we will have to delete the
-// link after it shows up.
-//
-// Note that we only flag items for possible link removal if they have
-// been previously detached. This means that an attachment failure
-// will leave the link in the COF, where it will hopefully resolve
-// correctly on relog.
-//
-// See related: MAINT-5070, MAINT-4409
-//
-void LLAttachmentsMgr::checkInvalidCOFLinks()
-{
- if (!gInventory.isInventoryUsable() || mQuestionableCOFLinks.empty())
- {
- return;
- }
- LLInventoryModel::cat_array_t cat_array;
- LLInventoryModel::item_array_t item_array;
- gInventory.collectDescendents(LLAppearanceMgr::instance().getCOF(),
- cat_array,item_array,LLInventoryModel::EXCLUDE_TRASH);
- for (S32 i=0; igetLinkedUUID();
- if (inv_item->getType() == LLAssetType::AT_OBJECT)
- {
- LLTimer timer;
- bool is_flagged_questionable = mQuestionableCOFLinks.getTime(item_id,timer);
- bool is_wearing_attachment = isAgentAvatarValid() && gAgentAvatarp->isWearingAttachment(item_id);
- if (is_wearing_attachment && is_flagged_questionable)
- {
- LL_DEBUGS("Avatar") << "ATT was flagged questionable but is now "
- << (is_wearing_attachment ? "attached " : "")
- <<"removing flag after "
- << timer.getElapsedTimeF32() << " item "
- << inv_item->getName() << " id " << item_id << LL_ENDL;
- mQuestionableCOFLinks.removeTime(item_id);
- }
- }
- }
-
- for(LLItemRequestTimes::iterator it = mQuestionableCOFLinks.begin();
- it != mQuestionableCOFLinks.end(); )
- {
- LLItemRequestTimes::iterator curr_it = it;
- ++it;
- const LLUUID& item_id = curr_it->first;
- LLViewerInventoryItem *inv_item = gInventory.getItem(item_id);
- if (curr_it->second.getElapsedTimeF32() > MAX_BAD_COF_TIME)
- {
- if (LLAppearanceMgr::instance().isLinkedInCOF(item_id))
- {
- LL_DEBUGS("Avatar") << "ATT Linked in COF but not attached or requested, deleting link after "
- << curr_it->second.getElapsedTimeF32() << " seconds for "
- << (inv_item ? inv_item->getName() : "UNKNOWN") << " id " << item_id << LL_ENDL;
- LLAppearanceMgr::instance().removeCOFItemLinks(item_id);
- }
- mQuestionableCOFLinks.erase(curr_it);
- continue;
- }
- }
-}
+ && mPendingAttachLinks.empty();
+ }
void LLAttachmentsMgr::spamStatusInfo()
{
diff --git a/indra/newview/llattachmentsmgr.h b/indra/newview/llattachmentsmgr.h
index 2428acfb381..4ccd4d32249 100644
--- a/indra/newview/llattachmentsmgr.h
+++ b/indra/newview/llattachmentsmgr.h
@@ -85,8 +85,14 @@ class LLAttachmentsMgr: public LLSingleton
void onDetachRequested(const LLUUID& inv_item_id);
void onDetachCompleted(const LLUUID& inv_item_id);
+ void clearPendingAttachmentLink(const LLUUID& idItem);
+ bool getPendingAttachments(std::set& ids) const;
bool isAttachmentStateComplete() const;
+protected:
+ void onRegisterAttachmentComplete(const LLUUID& id_item_link);
+ friend class LLRegisterAttachmentCallback;
+
private:
class LLItemRequestTimes: public std::map
@@ -109,7 +115,6 @@ class LLAttachmentsMgr: public LLSingleton
void linkRecentlyArrivedAttachments();
void expireOldAttachmentRequests();
void expireOldDetachRequests();
- void checkInvalidCOFLinks();
void spamStatusInfo();
// Attachments that we are planning to rez but haven't requested from the server yet.
@@ -124,9 +129,8 @@ class LLAttachmentsMgr: public LLSingleton
// Attachments that have arrived but have not been linked in the COF yet.
std::set mRecentlyArrivedAttachments;
LLTimer mCOFLinkBatchTimer;
-
- // Attachments that are linked in the COF but may be invalid.
- LLItemRequestTimes mQuestionableCOFLinks;
+ // Attachments that have pending COF link creation
+ std::set mPendingAttachLinks;
};
#endif
diff --git a/indra/newview/llblocklist.cpp b/indra/newview/llblocklist.cpp
index 89516a8a843..05c882e3f1d 100644
--- a/indra/newview/llblocklist.cpp
+++ b/indra/newview/llblocklist.cpp
@@ -48,6 +48,7 @@ LLBlockList::LLBlockList(const Params& p)
LLMuteList::getInstance()->addObserver(this);
mMuteListSize = static_cast(LLMuteList::getInstance()->getMutes().size());
+ updateNoItemsCommentText();
// Set up context menu.
LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar;
@@ -101,8 +102,35 @@ BlockListActionType LLBlockList::getCurrentMuteListActionType()
return type;
}
+void LLBlockList::updateNoItemsCommentText()
+{
+ const LLMuteList* mute_list = LLMuteList::getInstance();
+ if (!mute_list->isLoaded() && !mute_list->isFailed())
+ {
+ setNoItemsCommentText(mLoadingItemsMsg);
+ }
+ else if (mute_list->isFailed())
+ {
+ setNoItemsCommentText(mFailedItemsMsg);
+ }
+ else
+ {
+ updateNoItemsMessage(mNameFilter);
+ }
+}
+
+void LLBlockList::onChange()
+{
+ // Something changed, not sure what so force a refresh.
+ mShouldAddAll = true;
+ mActionType = NONE;
+ updateNoItemsCommentText();
+ setDirty();
+}
+
void LLBlockList::onChangeDetailed(const LLMute &mute)
{
+ updateNoItemsCommentText();
mActionType = getCurrentMuteListActionType();
mCurItemId = mute.mID;
@@ -197,6 +225,7 @@ void LLBlockList::addNewItem(const LLMute* mute)
void LLBlockList::refresh()
{
+ updateNoItemsCommentText();
bool have_filter = !mNameFilter.empty();
// save selection to restore it after list rebuilt
@@ -208,6 +237,8 @@ void LLBlockList::refresh()
clear();
createList();
mShouldAddAll = false;
+ // Full rebuild supersedes any queued incremental action. This ensures list consistency.
+ mActionType = NONE;
}
else
{
@@ -246,7 +277,9 @@ void LLBlockList::refresh()
{
LLBlockedListItem * curItem = dynamic_cast (*it);
if(curItem)
- {
+ {
+ // Refresh item text styling each pass so filtering keeps highlight in sync.
+ curItem->highlightName(mNameFilter);
hideListItem(curItem, findInsensitive(curItem->getName(), mNameFilter));
}
}
diff --git a/indra/newview/llblocklist.h b/indra/newview/llblocklist.h
index 64e8246f439..92665842563 100644
--- a/indra/newview/llblocklist.h
+++ b/indra/newview/llblocklist.h
@@ -58,7 +58,7 @@ class LLBlockList: public LLFlatListViewEx, public LLMuteListObserver
LLToggleableMenu* getContextMenu() const { return mContextMenu.get(); }
LLBlockedListItem* getBlockedItem() const;
- virtual void onChange() { }
+ virtual void onChange();
virtual void onChangeDetailed(const LLMute& );
virtual void draw();
@@ -68,6 +68,8 @@ class LLBlockList: public LLFlatListViewEx, public LLMuteListObserver
void refresh();
U32 getMuteListSize() { return mMuteListSize; }
+ void setLoadingItemsMsg(const std::string& msg) { mLoadingItemsMsg = msg; updateNoItemsCommentText(); }
+ void setFailedItemsMsg(const std::string& msg) { mFailedItemsMsg = msg; updateNoItemsCommentText(); }
private:
@@ -83,6 +85,7 @@ class LLBlockList: public LLFlatListViewEx, public LLMuteListObserver
bool isMenuItemVisible(const LLSD& userdata);
void toggleMute(U32 flags);
void createList();
+ void updateNoItemsCommentText();
BlockListActionType getCurrentMuteListActionType();
@@ -100,6 +103,8 @@ class LLBlockList: public LLFlatListViewEx, public LLMuteListObserver
LLMute::EType mCurItemType;
U32 mCurItemFlags;
std::string mPrevNameFilter;
+ std::string mLoadingItemsMsg;
+ std::string mFailedItemsMsg;
};
diff --git a/indra/newview/llchannelmanager.cpp b/indra/newview/llchannelmanager.cpp
index 454991ab831..27e52639674 100644
--- a/indra/newview/llchannelmanager.cpp
+++ b/indra/newview/llchannelmanager.cpp
@@ -140,6 +140,7 @@ void LLChannelManager::onLoginCompleted()
}
else
{
+ mStartUpToastInited = true;
gViewerWindow->getRootView()->addChild(mStartUpChannel);
// init channel's position and size
diff --git a/indra/newview/llchannelmanager.h b/indra/newview/llchannelmanager.h
index 4db7f32b102..75b3060e66f 100644
--- a/indra/newview/llchannelmanager.h
+++ b/indra/newview/llchannelmanager.h
@@ -104,12 +104,15 @@ class LLChannelManager : public LLSingleton
*/
static LLNotificationsUI::LLScreenChannel* getNotificationScreenChannel();
+ bool getStartUpToastInited() const { return mStartUpToastInited; }
+
std::vector& getChannelList() { return mChannelList;}
private:
LLScreenChannel* createChannel(LLScreenChannelBase::Params& p);
LLScreenChannel* mStartUpChannel;
+ bool mStartUpToastInited{false};
std::vector mChannelList;
};
diff --git a/indra/newview/llconversationview.cpp b/indra/newview/llconversationview.cpp
index 2297fddf0ce..117bfbf64c7 100644
--- a/indra/newview/llconversationview.cpp
+++ b/indra/newview/llconversationview.cpp
@@ -217,6 +217,12 @@ bool LLConversationViewSession::postBuild()
LLFolderViewItem::postBuild();
mItemPanel = LLUICtrlFactory::getInstance()->createFromFile("panel_conversation_list_item.xml", NULL, LLPanel::child_registry_t::instance());
+
+ if (!mItemPanel)
+ {
+ LLError::LLUserWarningMsg::showMissingFiles();
+ LL_ERRS() << "Failed to construct mItemPanel from panel_conversation_list_item.xml" << LL_ENDL;
+ }
addChild(mItemPanel);
mCallIconLayoutPanel = mItemPanel->getChild("call_icon_panel");
diff --git a/indra/newview/lldebugview.cpp b/indra/newview/lldebugview.cpp
index 53da9826ed7..3941b82e75d 100644
--- a/indra/newview/lldebugview.cpp
+++ b/indra/newview/lldebugview.cpp
@@ -38,7 +38,6 @@
#include "llappviewer.h"
#include "llsceneview.h"
#include "llviewertexture.h"
-#include "llfloaterreg.h"
#include "llscenemonitor.h"
//
// Globals
@@ -53,7 +52,6 @@ static LLDefaultChildRegistry::Register r("debug_view");
LLDebugView::LLDebugView(const LLDebugView::Params& p)
: LLView(p),
- mFastTimerView(NULL),
mDebugConsolep(NULL),
mFloaterSnapRegion(NULL)
{}
@@ -89,8 +87,6 @@ void LLDebugView::init()
r.setLeftTopAndSize(25, rect.getHeight() - 50, (S32) (gViewerWindow->getWindowRectScaled().getWidth() * 0.75f),
(S32) (gViewerWindow->getWindowRectScaled().getHeight() * 0.75f));
- mFastTimerView = dynamic_cast(LLFloaterReg::getInstance("block_timers"));
-
gSceneView = new LLSceneView(r);
gSceneView->setFollowsTop();
gSceneView->setFollowsLeft();
diff --git a/indra/newview/lldebugview.h b/indra/newview/lldebugview.h
index 8fa2acc3c9d..ca274d6f91e 100644
--- a/indra/newview/lldebugview.h
+++ b/indra/newview/lldebugview.h
@@ -59,7 +59,6 @@ class LLDebugView : public LLView
void setStatsVisible(bool visible);
- LLFastTimerView* mFastTimerView;
LLConsole* mDebugConsolep;
LLView* mFloaterSnapRegion;
};
diff --git a/indra/newview/llexpandabletextbox.cpp b/indra/newview/llexpandabletextbox.cpp
index 5c46eb9d80f..41ae47b6452 100644
--- a/indra/newview/llexpandabletextbox.cpp
+++ b/indra/newview/llexpandabletextbox.cpp
@@ -52,7 +52,7 @@ class LLExpanderSegment : public LLTextSegment
return copy;
}
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
{
// more label always spans width of text box
if (num_chars == 0)
diff --git a/indra/newview/llfavoritesbar.cpp b/indra/newview/llfavoritesbar.cpp
index 98b3ca820b6..b3408f7861f 100644
--- a/indra/newview/llfavoritesbar.cpp
+++ b/indra/newview/llfavoritesbar.cpp
@@ -1375,6 +1375,11 @@ bool LLFavoritesBarCtrl::handleRightMouseDown(S32 x, S32 y, MASK mask)
}
void copy_slurl_to_clipboard_cb(std::string& slurl)
{
+ if (slurl.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
LLClipboard::instance().copyToClipboard(utf8str_to_wstring(slurl), 0, static_cast(slurl.size()));
LLSD args;
diff --git a/indra/newview/llfloaterchangeitemthumbnail.cpp b/indra/newview/llfloaterchangeitemthumbnail.cpp
index fb31361fd97..914d2135ba6 100644
--- a/indra/newview/llfloaterchangeitemthumbnail.cpp
+++ b/indra/newview/llfloaterchangeitemthumbnail.cpp
@@ -236,11 +236,13 @@ bool LLFloaterChangeItemThumbnail::handleDragAndDrop(
if (cargo_type == DAD_TEXTURE)
{
LLInventoryItem *item = (LLInventoryItem *)cargo_data;
- if (item->getAssetUUID().notNull())
+ const LLUUID& asset_id = item->getAssetUUID();
+ if (asset_id.notNull())
{
if (drop)
{
- assignAndValidateAsset(item->getAssetUUID());
+ LLPointer texturep = LLViewerTextureManager::getFetchedTexture(asset_id);
+ assignAndValidateTexture(asset_id, texturep);
}
*accept = ACCEPT_YES_SINGLE;
@@ -708,6 +710,81 @@ void LLFloaterChangeItemThumbnail::assignAndValidateAsset(const LLUUID &asset_id
}
}
}
+
+void LLFloaterChangeItemThumbnail::assignAndValidateTexture(const LLUUID& asset_id, LLPointer texturep)
+{
+ if (!texturep)
+ {
+ LL_WARNS() << "Image " << asset_id << " doesn't exist" << LL_ENDL;
+ return;
+ }
+
+ if (texturep->isMissingAsset())
+ {
+ LL_WARNS() << "Image " << asset_id << " is missing" << LL_ENDL;
+ return;
+ }
+
+ if (texturep->getFullWidth() != texturep->getFullHeight())
+ {
+ LLNotificationsUtil::add("ThumbnailDimentionsLimit");
+ return;
+ }
+
+ if (texturep->getFullWidth() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN
+ && texturep->getFullWidth() > 0)
+ {
+ LLNotificationsUtil::add("ThumbnailDimentionsLimit");
+ return;
+ }
+
+ if (texturep->getFullWidth() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX
+ || texturep->getFullWidth() == 0)
+ {
+ if (texturep->isFullyLoaded()
+ && (texturep->getRawImageLevel() == 0)
+ && (texturep->isRawImageValid()))
+ {
+ LLUUID task_id = mTaskId;
+ uuid_set_t inventory_ids = mItemList;
+ LLHandle handle = getHandle();
+ LLFloaterSimpleSnapshot::completion_t callback =
+ [inventory_ids, task_id, handle](const LLUUID& asset_id)
+ {
+ onUploadComplete(asset_id, task_id, inventory_ids, handle);
+ };
+ LLFloaterSimpleSnapshot::uploadThumbnail(texturep->getRawImage(),
+ *mItemList.begin(),
+ mTaskId,
+ callback);
+ }
+ else
+ {
+ ImageLoadedData* data = new ImageLoadedData();
+ data->mTaskId = mTaskId;
+ data->mItemIds = mItemList;
+ data->mThumbnailId = asset_id;
+ data->mFloaterHandle = getHandle();
+ data->mSilent = false;
+ data->mTexturep = texturep;
+
+ texturep->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
+ texturep->setMinDiscardLevel(0);
+ texturep->setLoadedCallback(onFullImageLoaded,
+ 0, // Need best quality
+ true,
+ false,
+ (void*)data,
+ NULL,
+ false);
+ texturep->forceToSaveRawImage(0);
+ }
+ return;
+ }
+
+ setThumbnailId(asset_id);
+}
+
bool LLFloaterChangeItemThumbnail::validateAsset(const LLUUID &asset_id)
{
if (asset_id.isNull())
@@ -922,76 +999,8 @@ void LLFloaterChangeItemThumbnail::onTexturePickerCommit()
}
LLPointer texturep = LLViewerTextureManager::findFetchedTexture(asset_id, TEX_LIST_STANDARD);
- if (!texturep)
- {
- LL_WARNS() << "Image " << asset_id << " doesn't exist" << LL_ENDL;
- return;
- }
-
- if (texturep->isMissingAsset())
- {
- LL_WARNS() << "Image " << asset_id << " is missing" << LL_ENDL;
- return;
- }
-
- if (texturep->getFullWidth() != texturep->getFullHeight())
- {
- LLNotificationsUtil::add("ThumbnailDimentionsLimit");
- return;
- }
-
- if (texturep->getFullWidth() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN
- && texturep->getFullWidth() > 0)
- {
- LLNotificationsUtil::add("ThumbnailDimentionsLimit");
- return;
- }
-
- if (texturep->getFullWidth() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX
- || texturep->getFullWidth() == 0)
- {
- if (texturep->isFullyLoaded()
- && (texturep->getRawImageLevel() == 0)
- && (texturep->isRawImageValid()))
- {
- LLUUID task_id = mTaskId;
- uuid_set_t inventory_ids = mItemList;
- LLHandle handle = getHandle();
- LLFloaterSimpleSnapshot::completion_t callback =
- [inventory_ids, task_id, handle](const LLUUID& asset_id)
- {
- onUploadComplete(asset_id, task_id, inventory_ids, handle);
- };
- LLFloaterSimpleSnapshot::uploadThumbnail(texturep->getRawImage(),
- *mItemList.begin(),
- mTaskId,
- callback);
- }
- else
- {
- ImageLoadedData* data = new ImageLoadedData();
- data->mTaskId = mTaskId;
- data->mItemIds = mItemList;
- data->mThumbnailId = asset_id;
- data->mFloaterHandle = getHandle();
- data->mSilent = false;
- data->mTexturep = texturep;
-
- texturep->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
- texturep->setMinDiscardLevel(0);
- texturep->setLoadedCallback(onFullImageLoaded,
- 0, // Need best quality
- true,
- false,
- (void*)data,
- NULL,
- false);
- texturep->forceToSaveRawImage(0);
- }
- return;
- }
- setThumbnailId(asset_id);
+ assignAndValidateTexture(asset_id, texturep);
}
}
diff --git a/indra/newview/llfloaterchangeitemthumbnail.h b/indra/newview/llfloaterchangeitemthumbnail.h
index 46f63801fe5..980b46b2983 100644
--- a/indra/newview/llfloaterchangeitemthumbnail.h
+++ b/indra/newview/llfloaterchangeitemthumbnail.h
@@ -82,7 +82,11 @@ class LLFloaterChangeItemThumbnail : public LLFloater, public LLInventoryObserve
static void onRemove(void*);
static void onRemovalConfirmation(const LLSD& notification, const LLSD& response, LLHandle handle);
+ // As needed, attempts to load image then validates and assigns, does no converting.
void assignAndValidateAsset(const LLUUID &asset_id, bool silent = false);
+ // As needed, attempts to load and convert(upload) image then validates and assigns.
+ void assignAndValidateTexture(const LLUUID& asset_id, LLPointer texturep);
+
static void onImageDataLoaded(bool success,
LLViewerFetchedTexture *src_vi,
LLImageRaw* src,
diff --git a/indra/newview/llfloatercolorpicker.cpp b/indra/newview/llfloatercolorpicker.cpp
index cd450938568..56e086502ab 100644
--- a/indra/newview/llfloatercolorpicker.cpp
+++ b/indra/newview/llfloatercolorpicker.cpp
@@ -233,7 +233,15 @@ bool LLFloaterColorPicker::postBuild()
childSetCommitCallback("sspin", onTextCommit, (void*)this );
childSetCommitCallback("lspin", onTextCommit, (void*)this );
- LLToolPipette::getInstance()->setToolSelectCallback(boost::bind(&LLFloaterColorPicker::onColorSelect, this, _1));
+ mPipetteConnection = LLToolPipette::getInstance()->setToolSelectCallback(
+ [this](LLPointer object, S32 te_index)
+ {
+ const LLTextureEntry* entry = object->getTE(te_index);
+ if (entry)
+ {
+ onColorSelect(entry->getColor());
+ }
+ });
return true;
}
@@ -460,10 +468,10 @@ void LLFloaterColorPicker::onImmediateCheck( LLUICtrl* ctrl, void* data)
}
}
-void LLFloaterColorPicker::onColorSelect( const LLTextureEntry& te )
+// From pipette
+void LLFloaterColorPicker::onColorSelect( const LLColor4 &color )
{
- // Pipete
- selectCurRgb(te.getColor().mV[VRED], te.getColor().mV[VGREEN], te.getColor().mV[VBLUE]);
+ selectCurRgb(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE]);
}
void LLFloaterColorPicker::onMouseCaptureLost()
diff --git a/indra/newview/llfloatercolorpicker.h b/indra/newview/llfloatercolorpicker.h
index 5c27fffd088..af199d92a36 100644
--- a/indra/newview/llfloatercolorpicker.h
+++ b/indra/newview/llfloatercolorpicker.h
@@ -122,7 +122,7 @@ class LLFloaterColorPicker
void onClickPipette ( );
static void onTextCommit ( LLUICtrl* ctrl, void* data );
static void onImmediateCheck ( LLUICtrl* ctrl, void* data );
- void onColorSelect( const class LLTextureEntry& te );
+ void onColorSelect(const LLColor4& color); // from pipette
private:
// mutators for color values, can raise event to preview changes at object
void selectCurRgb ( F32 curRIn, F32 curGIn, F32 curBIn );
@@ -197,6 +197,7 @@ class LLFloaterColorPicker
F32 mContextConeOutAlpha;
F32 mContextConeFadeTime;
+ boost::signals2::scoped_connection mPipetteConnection;
};
#endif // LL_LLFLOATERCOLORPICKER_H
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/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp
index c0fe7ad8962..163edf04269 100644
--- a/indra/newview/llfloaterinspect.cpp
+++ b/indra/newview/llfloaterinspect.cpp
@@ -100,6 +100,7 @@ void LLFloaterInspect::onOpen(const LLSD& key)
LLSelectMgr::getInstance()->setForceSelection(forcesel); // restore previouis value
mObjectSelection = LLSelectMgr::getInstance()->getSelection();
refresh();
+ mDirty = false;
}
void LLFloaterInspect::onClickCreatorProfile()
{
diff --git a/indra/newview/llfloatermarketplace.cpp b/indra/newview/llfloatermarketplace.cpp
index bb2378d8643..a80546595c0 100644
--- a/indra/newview/llfloatermarketplace.cpp
+++ b/indra/newview/llfloatermarketplace.cpp
@@ -101,6 +101,22 @@ void LLFloaterMarketplace::openMarketplaceURL(const std::string& url)
// static
bool LLFloaterMarketplace::isMarketplaceURL(const std::string& url)
{
+ auto trimURL = [](std::string_view url) -> std::string_view
+ {
+ if (url.starts_with("https://"))
+ url.remove_prefix(8);
+ else if (url.starts_with("http://"))
+ url.remove_prefix(7);
+
+ while (!url.empty() && url.back() == '/')
+ url.remove_suffix(1);
+
+ return url;
+ };
+
static LLCachedControl marketplace_url(gSavedSettings, "MarketplaceURL", "https://marketplace.secondlife.com/");
- return url.starts_with(marketplace_url());
+ std::string_view marketplace_url_trimmed = trimURL(marketplace_url());
+ std::string_view url_trimmed = trimURL(url);
+
+ return !marketplace_url_trimmed.empty() && url_trimmed.starts_with(marketplace_url_trimmed);
}
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/llimprocessing.cpp b/indra/newview/llimprocessing.cpp
index 779ed725acb..3f60dc5d26b 100644
--- a/indra/newview/llimprocessing.cpp
+++ b/indra/newview/llimprocessing.cpp
@@ -1485,6 +1485,7 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
LLNotification::Params params("OfferFriendship");
params.substitutions = args;
params.payload = payload;
+ params.offer_from_agent = true;
LLPostponedNotification::add(params, from_id, false);
}
}
@@ -1533,7 +1534,7 @@ void LLIMProcessing::requestOfflineMessages()
&& isAgentAvatarValid()
&& gAgent.getRegion()
&& gAgent.getRegion()->capabilitiesReceived()
- && (LLMuteList::getInstance()->isLoaded() || LLMuteList::getInstance()->getLoadFailed()))
+ && LLMuteList::getInstance()->updateLoadState())
{
std::string cap_url = gAgent.getRegionCapability("ReadOfflineMsgs");
diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp
index b4cce02bdac..c8ea14a11e9 100644
--- a/indra/newview/llinventorybridge.cpp
+++ b/indra/newview/llinventorybridge.cpp
@@ -1912,6 +1912,11 @@ void LLItemBridge::doShowOnMap(LLLandmark* landmark)
void copy_slurl_to_clipboard_callback_inv(const std::string& slurl)
{
+ if (slurl.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
gViewerWindow->getWindow()->copyTextToClipboard(utf8str_to_wstring(slurl));
LLSD args;
args["SLURL"] = slurl;
diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp
index 7522ea49079..a6fda946ef2 100644
--- a/indra/newview/llinventoryfunctions.cpp
+++ b/indra/newview/llinventoryfunctions.cpp
@@ -634,12 +634,6 @@ bool get_is_item_worn(const LLUUID& id, const LLViewerInventoryItem* item)
return false;
}
- // Consider the item as worn if it has links in COF.
- if (LLAppearanceMgr::instance().isLinkedInCOF(id))
- {
- return true;
- }
-
switch(item->getType())
{
case LLAssetType::AT_OBJECT:
diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp
index c2f9c483c0f..8b9aad46416 100644
--- a/indra/newview/llinventorymodel.cpp
+++ b/indra/newview/llinventorymodel.cpp
@@ -2383,10 +2383,22 @@ void LLInventoryModel::cache(
items,
INCLUDE_TRASH,
can_cache);
+
+ if (categories.empty() && items.empty())
+ {
+ LL_WARNS(LOG_INV) << "Nothing to cache for " << parent_folder_id << LL_ENDL;
+ return;
+ }
+
// Use temporary file to avoid potential conflicts with other
// instances (even a 'read only' instance unzips into a file)
std::string temp_file = gDirUtilp->getTempFilename();
- saveToFile(temp_file, categories, items);
+ if (!saveToFile(temp_file, categories, items))
+ {
+ LL_WARNS(LOG_INV) << "Failed to save inventory cache for " << parent_folder_id << LL_ENDL;
+ LLFile::remove(temp_file);
+ return;
+ }
std::string gzip_filename = getInvCacheAddres(agent_id);
gzip_filename.append(".gz");
if(gzip_file(temp_file, gzip_filename))
@@ -3537,6 +3549,11 @@ bool LLInventoryModel::saveToFile(const std::string& filename,
S32 cat_count = 0;
for (auto& cat : categories)
{
+ if (cat.isNull())
+ {
+ LL_WARNS(LOG_INV) << "Skipping null category during inventory save" << LL_ENDL;
+ continue;
+ }
if (cat->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN)
{
LLSD sd;
@@ -3551,6 +3568,11 @@ bool LLInventoryModel::saveToFile(const std::string& filename,
auto it_count = items.size();
for (auto& item : items)
{
+ if (item.isNull())
+ {
+ LL_WARNS(LOG_INV) << "Skipping null item during inventory save" << LL_ENDL;
+ continue;
+ }
LLSD sd;
item->asLLSD(sd);
item_array.append(sd);
@@ -3567,6 +3589,12 @@ bool LLInventoryModel::saveToFile(const std::string& filename,
LL_INFOS(LOG_INV) << "Inventory saved: " << (S32)cat_count << " categories, " << (S32)it_count << " items." << LL_ENDL;
}
+ catch(std::bad_alloc&)
+ {
+ // We are quiting, so just log an error and move on.
+ LL_WARNS(LOG_INV) << "Failed to save inventory to cache due to memory allocation failure." << LL_ENDL;
+ return false;
+ }
catch (...)
{
LOG_UNHANDLED_EXCEPTION("");
diff --git a/indra/newview/lllandmarkactions.cpp b/indra/newview/lllandmarkactions.cpp
index b9d1c7ba189..9b90e415497 100644
--- a/indra/newview/lllandmarkactions.cpp
+++ b/indra/newview/lllandmarkactions.cpp
@@ -473,6 +473,11 @@ void LLLandmarkActions::copySLURLtoClipboard(const LLUUID& landmarkInventoryItem
void copy_slurl_to_clipboard_callback(const std::string& slurl)
{
+ if (slurl.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
gViewerWindow->getWindow()->copyTextToClipboard(utf8str_to_wstring(slurl));
LLSD args;
args["SLURL"] = slurl;
diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp
index e9d68723d37..0358233637e 100644
--- a/indra/newview/lllogininstance.cpp
+++ b/indra/newview/lllogininstance.cpp
@@ -277,11 +277,6 @@ void LLLoginInstance::constructAuthParams(LLPointer user_credentia
mRequestData["params"] = request_params;
mRequestData["options"] = requested_options;
mRequestData["http_params"] = http_params;
-#if LL_RELEASE_FOR_DOWNLOAD
- mRequestData["wait_for_updater"] = LLAppViewer::instance()->waitForUpdater();
-#else
- mRequestData["wait_for_updater"] = false;
-#endif
}
bool LLLoginInstance::handleLoginEvent(const LLSD& event)
@@ -316,13 +311,6 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
// Login has failed.
// Figure out why and respond...
LLSD response = event["data"];
- LLSD updater = response["updater"];
-
- // Always provide a response to the updater, if in fact the updater
- // contacted us, if in fact the ping contains a 'reply' key. Most code
- // paths tell it not to proceed with updating.
- ResponsePtr resp(std::make_shared
- (LLSDMap("update", false), updater));
std::string reason_response = response["reason"].asString();
std::string message_response = response["message"].asString();
@@ -385,26 +373,15 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
}
else if(reason_response == "update")
{
- // This can happen if the user clicked Login quickly, before we heard
- // back from the Viewer Version Manager, but login failed because
- // login.cgi is insisting on a required update. We were called with an
- // event that bundles both the login.cgi 'response' and the
- // synchronization event from the 'updater'.
+ // login.cgi rejected login and requires an update. Since Velopack
+ // handles updates now, the best we can do here is tell the user
+ // to download the update manually via the release notes URL.
std::string login_version = response["message_args"]["VERSION"];
- std::string vvm_version = updater["VERSION"];
- std::string relnotes = updater["URL"];
LL_WARNS("LLLogin") << "Login failed because an update to version " << login_version << " is required." << LL_ENDL;
- // vvm_version might be empty because we might not have gotten
- // SLVersionChecker's LoginSync handshake. But if it IS populated, it
- // should (!) be the same as the version we got from login.cgi.
- if ((! vvm_version.empty()) && vvm_version != login_version)
- {
- LL_WARNS("LLLogin") << "VVM update version " << vvm_version
- << " differs from login version " << login_version
- << "; presenting VVM version to match release notes URL"
- << LL_ENDL;
- login_version = vvm_version;
- }
+
+ // Try to use the release notes URL from the VVM query if available,
+ // otherwise fall back to constructing one from the version.
+ std::string relnotes = LLVersionInfo::instance().getReleaseNotes();
if (relnotes.empty() || relnotes.find("://") == std::string::npos)
{
relnotes = LLTrans::getString("RELEASE_NOTES_BASE_URL");
@@ -420,32 +397,11 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
args["VERSION"] = login_version;
args["URL"] = relnotes;
- if (updater.isUndefined())
- {
- // If the updater failed to shake hands, better advise the user to
- // download the update him/herself.
- LLNotificationsUtil::add(
- "RequiredUpdate",
- args,
- updater,
- boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
- }
- else
- {
- // If we've heard from the updater that an update is required,
- // then display the prompt that assures the user we'll take care
- // of it. This is the one case in which we bind 'resp':
- // instead of destroying our Response object (and thus sending a
- // negative reply to the updater) as soon as we exit this
- // function, bind our shared_ptr so it gets passed into
- // syncWithUpdater. That ensures that the response is delayed
- // until the user has responded to the notification.
- LLNotificationsUtil::add(
- "PauseForUpdate",
- args,
- updater,
- boost::bind(&LLLoginInstance::syncWithUpdater, this, resp, _1, _2));
- }
+ LLNotificationsUtil::add(
+ "RequiredUpdate",
+ args,
+ LLSD(),
+ boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
}
else if(reason_response == "mfa_challenge")
{
@@ -479,19 +435,6 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
}
}
-void LLLoginInstance::syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response)
-{
- LL_INFOS("LLLogin") << "LLLoginInstance::syncWithUpdater" << LL_ENDL;
- // 'resp' points to an instance of LLEventAPI::Response that will be
- // destroyed as soon as we return and the notification response functor is
- // unregistered. Modify it so that it tells the updater to go ahead and
- // perform the update. Naturally, if we allowed the user a choice as to
- // whether to proceed or not, this assignment would reflect the user's
- // selection.
- (*resp)["update"] = true;
- attemptComplete();
-}
-
void LLLoginInstance::handleLoginDisallowed(const LLSD& notification, const LLSD& response)
{
attemptComplete();
diff --git a/indra/newview/lllogininstance.h b/indra/newview/lllogininstance.h
index 54ce51720f7..551ad92d336 100644
--- a/indra/newview/lllogininstance.h
+++ b/indra/newview/lllogininstance.h
@@ -28,8 +28,6 @@
#define LL_LLLOGININSTANCE_H
#include "lleventdispatcher.h"
-#include "lleventapi.h"
-#include // std::shared_ptr
#include "llsecapi.h"
class LLLogin;
class LLEventStream;
@@ -72,10 +70,7 @@ class LLLoginInstance : public LLSingleton
void saveMFAHash(LLSD const& response);
private:
- typedef std::shared_ptr ResponsePtr;
void constructAuthParams(LLPointer user_credentials);
- void updateApp(bool mandatory, const std::string& message);
- bool updateDialogCallback(const LLSD& notification, const LLSD& response);
bool handleLoginEvent(const LLSD& event);
void handleLoginFailure(const LLSD& event);
@@ -83,7 +78,6 @@ class LLLoginInstance : public LLSingleton
void handleDisconnect(const LLSD& event);
void handleIndeterminate(const LLSD& event);
void handleLoginDisallowed(const LLSD& notification, const LLSD& response);
- void syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response);
bool handleTOSResponse(bool v, const std::string& key);
void showMFAChallange(const std::string& message);
diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp
index b7bba02b9d8..36bcba2c5c5 100644
--- a/indra/newview/llmutelist.cpp
+++ b/indra/newview/llmutelist.cpp
@@ -92,7 +92,17 @@ class LLDispatchEmptyMuteList : public LLDispatchHandler
const LLUUID& invoice,
const sparam_t& strings)
{
- LLMuteList::getInstance()->setLoaded();
+ // We've gotten a message from the server that indicates our mute list is empty there.
+
+ // First we want to make sure that if we had a cached mute list, we clear it assuming it is outdated.
+ LLMuteList* mute_list = LLMuteList::getInstance();
+ if(mute_list->mLoadSource == LLMuteList::MLS_FALLBACK_CACHE && !mute_list->getMutes().empty())
+ {
+ LL_WARNS() << "Our current mute list is not empty, but the server says that it should be. Is our cache outdated, or did our mutes get lost?" << LL_ENDL;
+ mute_list->clearCachedMutes();
+ }
+ // Lastly we set the load state to loaded with the source of server empty. We are now in a clean and ready state.
+ mute_list->setLoaded(LLMuteList::MLS_SERVER_EMPTY);
return true;
}
};
@@ -155,7 +165,10 @@ std::string LLMute::getDisplayType() const
//-----------------------------------------------------------------------------
LLMuteList::LLMuteList() :
mLoadState(ML_INITIAL),
- mRequestStartTime(0.f)
+ mLoadSource(MLS_NONE),
+ mRequestStartTime(0.f),
+ mTriedCacheFallback(false),
+ mTriedRegionChangeRetry(false)
{
gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList);
@@ -178,6 +191,8 @@ LLMuteList::LLMuteList() :
// but this way is just more convinient
onAccountNameChanged(id, av_name.getUserName());
});
+ // Register our region change callback handler early, we'll clean it up if/when we don't need it anymore.
+ mRegionChangedCallback = gAgent.addRegionChangedCallback(boost::bind(&LLMuteList::onRegionChanged, this));
}
//-----------------------------------------------------------------------------
@@ -191,6 +206,10 @@ LLMuteList::~LLMuteList()
void LLMuteList::cleanupSingleton()
{
LLAvatarNameCache::getInstance()->setAccountNameChangedCallback(nullptr);
+ if (mRegionChangedCallback.connected())
+ {
+ mRegionChangedCallback.disconnect();
+ }
}
bool LLMuteList::isLinden(const std::string& name)
@@ -210,9 +229,9 @@ bool LLMuteList::isLinden(const std::string& name)
return last_name == "linden";
}
-bool LLMuteList::getLoadFailed() const
+bool LLMuteList::updateLoadState()
{
- if (mLoadState == ML_FAILED)
+ if (isLoaded() || isFailed())
{
return true;
}
@@ -221,9 +240,83 @@ bool LLMuteList::getLoadFailed() const
constexpr F64 WAIT_SECONDS = 30;
if (mRequestStartTime + WAIT_SECONDS < LLTimer::getTotalSeconds())
{
- return true;
+ LL_WARNS() << "Mute list request timed out; trying cache fallback once" << LL_ENDL;
+ tryLoadCacheFallback(gAgent.getID(), "request timeout");
+ return isLoaded() || isFailed();
+ }
+ }
+ return false;
+}
+
+void LLMuteList::clearCachedMutes()
+{
+ mMutes.clear();
+ mLegacyMutes.clear();
+ LL_WARNS() << "Cached mutes cleared" << LL_ENDL;
+}
+
+const char* LLMuteList::sourceToString(EMuteListSource source)
+{
+ switch (source)
+ {
+ case MLS_NONE:
+ return "none";
+ case MLS_SERVER:
+ return "server";
+ case MLS_SERVER_EMPTY:
+ return "server-empty";
+ case MLS_SERVER_CACHE:
+ return "server-cached";
+ case MLS_FALLBACK_CACHE:
+ return "fallback-cache";
+ default:
+ return "unknown";
+ }
+}
+
+std::string LLMuteList::getCacheFilename(const LLUUID& agent_id) const
+{
+ std::string agent_id_string;
+ agent_id.toString(agent_id_string);
+ return gDirUtilp->getExpandedFilename(LL_PATH_CACHE, agent_id_string) + ".cached_mute";
+}
+
+void LLMuteList::setFailed(const std::string& reason)
+{
+ mLoadState = ML_FAILED;
+ if (mLoadSource == MLS_NONE)
+ {
+ LL_WARNS() << "Mute list unavailable: " << reason << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS() << "Mute list unavailable: " << reason << " (last source=" << sourceToString(mLoadSource) << ")" << LL_ENDL;
+ }
+ notifyObservers();
+}
+
+bool LLMuteList::tryLoadCacheFallback(const LLUUID& agent_id, const std::string& reason)
+{
+ if (mTriedCacheFallback)
+ {
+ if (!isLoaded())
+ {
+ setFailed("cache fallback already attempted before " + reason);
}
+ return isLoaded();
}
+
+ mTriedCacheFallback = true;
+ const std::string filename = getCacheFilename(agent_id);
+ LL_INFOS() << "Trying mute list cache fallback due to " << reason << ": " << filename << LL_ENDL;
+
+ if (loadFromFile(filename, MLS_FALLBACK_CACHE))
+ {
+ LL_WARNS() << "Loaded mute list from cache fallback due to " << reason << LL_ENDL;
+ return true;
+ }
+
+ setFailed("cache fallback failed after " + reason);
return false;
}
@@ -580,14 +673,14 @@ std::vector LLMuteList::getMutes() const
//-----------------------------------------------------------------------------
// loadFromFile()
//-----------------------------------------------------------------------------
-bool LLMuteList::loadFromFile(const std::string& filename)
+bool LLMuteList::loadFromFile(const std::string& filename, EMuteListSource source)
{
LL_PROFILE_ZONE_SCOPED;
if(!filename.size())
{
LL_WARNS() << "Mute List Filename is Empty!" << LL_ENDL;
- mLoadState = ML_FAILED;
+ setFailed("empty filename");
return false;
}
@@ -595,10 +688,13 @@ bool LLMuteList::loadFromFile(const std::string& filename)
if (!fp)
{
LL_WARNS() << "Couldn't open mute list " << filename << LL_ENDL;
- mLoadState = ML_FAILED;
+ setFailed("cannot open " + filename);
return false;
}
+ // Replace previous server-backed state so fallback can be superseded by authoritative data.
+ clearCachedMutes();
+
// *NOTE: Changing the size of these buffers will require changes
// in the scanf below.
char id_buffer[MAX_STRING]; /*Flawfinder: ignore*/
@@ -627,7 +723,7 @@ bool LLMuteList::loadFromFile(const std::string& filename)
}
}
fclose(fp);
- setLoaded();
+ setLoaded(source);
// server does not maintain up-to date account names (not display names!)
// in this list, so it falls to viewer.
@@ -737,12 +833,16 @@ bool LLMuteList::isMuted(const std::string& username, U32 flags) const
//-----------------------------------------------------------------------------
void LLMuteList::requestFromServer(const LLUUID& agent_id)
{
- std::string agent_id_string;
- std::string filename;
- agent_id.toString(agent_id_string);
- filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
+ if(isLoadedFromServer())
+ {
+ LL_WARNS() << "Blocked attempt to request mute list from server when already loaded from server!" << LL_ENDL;
+ return;
+ }
+ const std::string filename = getCacheFilename(agent_id);
LLCRC crc;
crc.update(filename);
+ mTriedCacheFallback = false;
+ mLoadSource = MLS_NONE;
LLMessageSystem* msg = gMessageSystem;
msg->newMessageFast(_PREHASH_MuteListRequest);
@@ -755,17 +855,22 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)
if (gDisconnected)
{
LL_WARNS() << "Trying to request mute list when disconnected!" << LL_ENDL;
- mLoadState = ML_FAILED;
+ // 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())
{
LL_WARNS() << "No region for agent yet, skipping mute list request!" << LL_ENDL;
- mLoadState = ML_FAILED;
+ tryLoadCacheFallback(agent_id, "no region for request");
return;
}
mLoadState = ML_REQUESTED;
- mRequestStartTime = LLTimer::getElapsedSeconds();
+ mRequestStartTime = LLTimer::getTotalSeconds();
// Double amount of retries due to this request happening during busy stage
// Ideally this should be turned into a capability
gMessageSystem->sendReliable(gAgent.getRegionHost(), LL_DEFAULT_RELIABLE_RETRIES * 2, true, LL_PING_BASED_TIMEOUT_DUMMY, NULL, NULL);
@@ -777,15 +882,16 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)
void LLMuteList::cache(const LLUUID& agent_id)
{
- // Write to disk even if empty.
- if(isLoaded())
+ // Write to disk even if empty, but never from degraded fallback state.
+ if (isLoaded() && !isLoadedDegraded())
{
- std::string agent_id_string;
- std::string filename;
- agent_id.toString(agent_id_string);
- filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
+ const std::string filename = getCacheFilename(agent_id);
saveToFile(filename);
}
+ else if (isLoaded())
+ {
+ LL_WARNS() << "Skipping mute list cache write from fallback-only state" << LL_ENDL;
+ }
}
//-----------------------------------------------------------------------------
@@ -795,11 +901,16 @@ void LLMuteList::cache(const LLUUID& agent_id)
void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
{
LL_INFOS() << "LLMuteList::processMuteListUpdate()" << LL_ENDL;
+ LLMuteList* mute_list = getInstance();
+ if(mute_list->mTriedRegionChangeRetry)
+ {
+ LL_WARNS() << "Received mute list update after retrying region change; success!" << LL_ENDL;
+ }
LLUUID agent_id;
msg->getUUIDFast(_PREHASH_MuteData, _PREHASH_AgentID, agent_id);
if(agent_id != gAgent.getID())
{
- LL_WARNS() << "Got an mute list update for the wrong agent." << LL_ENDL;
+ LL_WARNS() << "Got a mute list update for the wrong agent." << LL_ENDL;
return;
}
std::string unclean_filename;
@@ -810,9 +921,8 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
LL_WARNS() << "Received empty mute list filename." << LL_ENDL;
}
- LLMuteList* mute_list = getInstance();
mute_list->mLoadState = ML_REQUESTED;
- mute_list->mRequestStartTime = LLTimer::getElapsedSeconds();
+ mute_list->mRequestStartTime = LLTimer::getTotalSeconds();
// Todo: Based of logs and testing, there is no callback
// from server if file doesn't exist server side.
@@ -831,12 +941,12 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
void LLMuteList::processUseCachedMuteList(LLMessageSystem* msg, void**)
{
LL_INFOS() << "LLMuteList::processUseCachedMuteList()" << LL_ENDL;
-
- std::string agent_id_string;
- gAgent.getID().toString(agent_id_string);
- std::string filename;
- filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
- LLMuteList::getInstance()->loadFromFile(filename);
+ LLMuteList* mute_list = LLMuteList::getInstance();
+ if(mute_list->mTriedRegionChangeRetry)
+ {
+ LL_WARNS() << "Received use cached mute list message after retrying region change; success!" << LL_ENDL;
+ }
+ mute_list->loadFromFile(mute_list->getCacheFilename(gAgent.getID()), MLS_SERVER_CACHE);
}
void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_status)
@@ -845,13 +955,13 @@ void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_
if(local_filename_and_path && !local_filename_and_path->empty() && (error_code == 0))
{
LL_INFOS() << "Received mute list from server" << LL_ENDL;
- LLMuteList::getInstance()->loadFromFile(*local_filename_and_path);
+ LLMuteList::getInstance()->loadFromFile(*local_filename_and_path, MLS_SERVER);
LLFile::remove(*local_filename_and_path);
}
else
{
LL_INFOS() << "LLMuteList xfer failed with code " << error_code << LL_ENDL;
- LLMuteList::getInstance()->mLoadState = ML_FAILED;
+ LLMuteList::getInstance()->tryLoadCacheFallback(gAgent.getID(), "xfer failure");
}
delete local_filename_and_path;
}
@@ -908,10 +1018,21 @@ void LLMuteList::removeObserver(LLMuteListObserver* observer)
mObservers.erase(observer);
}
-void LLMuteList::setLoaded()
+void LLMuteList::setLoaded(EMuteListSource source)
{
+ if(isLoaded())
+ {
+ LL_WARNS() << "Mute list was already loaded from " << sourceToString(mLoadSource) << ", switching to " << sourceToString(source) << LL_ENDL;
+ }
mLoadState = ML_LOADED;
+ mLoadSource = source;
notifyObservers();
+ LL_INFOS() << "Mute list loaded from " << sourceToString(source) << LL_ENDL;
+ if(isLoadedFromServer() && mRegionChangedCallback.connected())
+ {
+ LL_INFOS() << "Mute list loaded from server, disconnecting region change callback" << LL_ENDL;
+ mRegionChangedCallback.disconnect();
+ }
}
void LLMuteList::notifyObservers()
@@ -940,6 +1061,23 @@ void LLMuteList::notifyObserversDetailed(const LLMute& mute)
}
}
+void LLMuteList::onRegionChanged()
+{
+ // If we are in a degraded state, either some protocol messages got lost between us and our login region, or our login region was having a bad day.
+ // Since the previous region might've been unable to provide our mute list, we can try to request it again from our new region.
+ // This is limited to one retry per session.
+ if(isLoadedDegraded() && !mTriedRegionChangeRetry)
+ {
+ if(mRegionChangedCallback.connected())
+ {
+ mRegionChangedCallback.disconnect();
+ }
+ LL_WARNS() << "Region changed while mute list is in degraded state, queueing a retry of the mute list request" << LL_ENDL;
+ mTriedRegionChangeRetry = true;
+ requestFromServer(gAgent.getID());
+ }
+}
+
LLRenderMuteList::LLRenderMuteList()
{}
diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h
index b65fd61fccf..aff23c72d13 100644
--- a/indra/newview/llmutelist.h
+++ b/indra/newview/llmutelist.h
@@ -31,6 +31,8 @@
#include "lluuid.h"
#include "llextendedstatus.h"
+#include
+
class LLViewerObject;
class LLMessageSystem;
class LLMuteListObserver;
@@ -82,6 +84,15 @@ class LLMuteList : public LLSingleton
ML_LOADED,
ML_FAILED,
};
+
+ enum EMuteListSource
+ {
+ MLS_NONE,
+ MLS_SERVER,
+ MLS_SERVER_EMPTY,
+ MLS_SERVER_CACHE,
+ MLS_FALLBACK_CACHE,
+ };
public:
// reasons for auto-unmuting a resident
enum EAutoReason
@@ -115,8 +126,17 @@ class LLMuteList : public LLSingleton
static bool isLinden(const std::string& name);
- bool isLoaded() const { return mLoadState == ML_LOADED; }
- bool getLoadFailed() const;
+ // 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 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.
+ // Return value indicates mute list consumption readiness.
+ bool updateLoadState();
std::vector getMutes() const;
@@ -126,11 +146,19 @@ class LLMuteList : public LLSingleton
// call this method on logout to save everything.
void cache(const LLUUID& agent_id);
+ // Handler for region change event, used for server request retries if isLoadedDegraded() is true
+ void onRegionChanged();
+
private:
- bool loadFromFile(const std::string& filename);
+ void clearCachedMutes();
+ bool loadFromFile(const std::string& filename, EMuteListSource source);
bool saveToFile(const std::string& filename);
+ bool tryLoadCacheFallback(const LLUUID& agent_id, const std::string& reason);
+ void setFailed(const std::string& reason);
+ static const char* sourceToString(EMuteListSource source);
+ std::string getCacheFilename(const LLUUID& agent_id) const;
- void setLoaded();
+ void setLoaded(EMuteListSource source);
void notifyObservers();
void notifyObserversDetailed(const LLMute &mute);
@@ -177,7 +205,11 @@ class LLMuteList : public LLSingleton
observer_set_t mObservers;
EMuteListState mLoadState;
+ EMuteListSource mLoadSource;
F64 mRequestStartTime;
+ bool mTriedCacheFallback;
+ bool mTriedRegionChangeRetry;
+ boost::signals2::connection mRegionChangedCallback;
friend class LLDispatchEmptyMuteList;
};
diff --git a/indra/newview/llnotificationofferhandler.cpp b/indra/newview/llnotificationofferhandler.cpp
index b65da28bda5..27ef28f32a0 100644
--- a/indra/newview/llnotificationofferhandler.cpp
+++ b/indra/newview/llnotificationofferhandler.cpp
@@ -36,6 +36,7 @@
#include "llscriptfloater.h"
#include "llimview.h"
#include "llnotificationsutil.h"
+#include "llchannelmanager.h"
#include
@@ -135,7 +136,13 @@ bool LLOfferHandler::processNotification(const LLNotificationPtr& notification,
LLScreenChannel* channel = dynamic_cast(mChannel.get());
if(channel)
+ {
+ if (LLChannelManager::getInstance()->getStartUpToastInited() && notification->getOfferFromAgent())
+ {
+ LLChannelManager::getInstance()->onStartUpToastClose();
+ }
channel->addToast(p);
+ }
}
diff --git a/indra/newview/llpanelblockedlist.cpp b/indra/newview/llpanelblockedlist.cpp
index 69f51b03b67..178f994bebf 100644
--- a/indra/newview/llpanelblockedlist.cpp
+++ b/indra/newview/llpanelblockedlist.cpp
@@ -78,6 +78,10 @@ bool LLPanelBlockedList::postBuild()
{
mBlockedList = getChild("blocked");
mBlockedList->setCommitOnSelectionChange(true);
+ mBlockedList->setNoItemsMsg(getString("no_blocked"));
+ mBlockedList->setNoFilteredItemsMsg(getString("no_filtered_blocked"));
+ mBlockedList->setLoadingItemsMsg(getString("loading_blocked"));
+ mBlockedList->setFailedItemsMsg(getString("failed_blocked"));
this->setVisibleCallback(boost::bind(&LLPanelBlockedList::removePicker, this));
switch (gSavedSettings.getU32("BlockPeopleSortOrder"))
diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp
index 128ba42efd4..26f13927583 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/llpanellogin.cpp b/indra/newview/llpanellogin.cpp
index 868e02f28b6..0cfb3ec49af 100644
--- a/indra/newview/llpanellogin.cpp
+++ b/indra/newview/llpanellogin.cpp
@@ -37,6 +37,9 @@
#include "llappviewer.h"
#include "llbutton.h"
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
#include "llcheckboxctrl.h"
#include "llcommandhandler.h" // for secondlife:///app/login/
#include "llcombobox.h"
@@ -939,6 +942,19 @@ void LLPanelLogin::handleMediaEvent(LLPluginClassMedia* /*self*/, EMediaEvent ev
// static
void LLPanelLogin::onClickConnect(bool commit_fields)
{
+#if LL_VELOPACK
+ // In theory, you should never be able to get here.
+ // If there's a required update, try as you might you're not supposed to actually close the downloading update dialog.
+ // But just in case...
+ if (velopack_is_required_update_in_progress())
+ {
+ LLSD args;
+ args["VERSION"] = velopack_get_required_update_version();
+ LLNotificationsUtil::add("DownloadingUpdate", args);
+ return;
+ }
+#endif
+
if (sInstance && sInstance->mCallback && !sInstance->mAlertNotif)
{
if (commit_fields)
diff --git a/indra/newview/llpanelplaces.cpp b/indra/newview/llpanelplaces.cpp
index 5435a79e16e..5c866905d6a 100644
--- a/indra/newview/llpanelplaces.cpp
+++ b/indra/newview/llpanelplaces.cpp
@@ -1356,6 +1356,12 @@ static bool is_agent_in_selected_parcel(LLParcel* parcel)
static void onSLURLBuilt(std::string& slurl)
{
+ if (slurl.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
+
LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(slurl));
LLSD args;
diff --git a/indra/newview/llpanelplacestab.cpp b/indra/newview/llpanelplacestab.cpp
index 3916e34e318..e870ab7a2c8 100644
--- a/indra/newview/llpanelplacestab.cpp
+++ b/indra/newview/llpanelplacestab.cpp
@@ -67,6 +67,12 @@ void LLPanelPlacesTab::onRegionResponse(const LLVector3d& landmark_global_pos,
sl_url = "";
}
+ if (sl_url.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
+
LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(sl_url));
LLSD args;
diff --git a/indra/newview/llpanelteleporthistory.cpp b/indra/newview/llpanelteleporthistory.cpp
index bfdfa68e01e..0fac70dbd29 100644
--- a/indra/newview/llpanelteleporthistory.cpp
+++ b/indra/newview/llpanelteleporthistory.cpp
@@ -1012,6 +1012,11 @@ LLFlatListView* LLTeleportHistoryPanel::getFlatListViewFromTab(LLAccordionCtrlTa
void LLTeleportHistoryPanel::gotSLURLCallback(const std::string& slurl)
{
+ if (slurl.empty())
+ {
+ LLNotificationsUtil::add("LandmarkLocationUnknown");
+ return;
+ }
LLClipboard::instance().copyToClipboard(utf8str_to_wstring(slurl), 0, static_cast(slurl.size()));
LLSD args;
diff --git a/indra/newview/llreflectionmap.cpp b/indra/newview/llreflectionmap.cpp
index 7f5076bd561..f4f3e39f177 100644
--- a/indra/newview/llreflectionmap.cpp
+++ b/indra/newview/llreflectionmap.cpp
@@ -45,7 +45,8 @@ LLReflectionMap::~LLReflectionMap()
{
if (mOcclusionQuery)
{
- glDeleteQueries(1, &mOcclusionQuery);
+ gPipeline.mReflectionMapManager.recycleQuery(mOcclusionQuery);
+ mOcclusionQuery = 0;
}
}
@@ -341,7 +342,7 @@ void LLReflectionMap::doOcclusion(const LLVector4a& eye)
if (mOcclusionQuery == 0)
{ // no query was previously issued, allocate one and issue
LL_PROFILE_ZONE_NAMED_CATEGORY_PIPELINE("rmdo - glGenQueries");
- glGenQueries(1, &mOcclusionQuery);
+ mOcclusionQuery = gPipeline.mReflectionMapManager.allocateQuery();
do_query = true;
}
else
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index c6fa64753c3..d2e37fb40a5 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -523,6 +523,7 @@ void LLReflectionMapManager::refreshSettings()
mRenderReflectionProbeLevel = gSavedSettings.getS32("RenderReflectionProbeLevel");
mRenderReflectionProbeCount = gSavedSettings.getU32("RenderReflectionProbeCount");
mRenderReflectionProbeDynamicAllocation = gSavedSettings.getS32("RenderReflectionProbeDynamicAllocation");
+ cleanupQueryPool();
}
LLReflectionMap* LLReflectionMapManager::addProbe(LLSpatialGroup* group)
@@ -570,6 +571,25 @@ U32 LLReflectionMapManager::probeMemory()
return (mDynamicProbeCount * 6 * (mProbeResolution * mProbeResolution) * 4) / 1024 / 1024 + (mDynamicProbeCount * 6 * (LL_IRRADIANCE_MAP_RESOLUTION * LL_IRRADIANCE_MAP_RESOLUTION) * 4) / 1024 / 1024;
}
+GLuint LLReflectionMapManager::allocateQuery()
+{
+ if (mQueryPool.empty())
+ {
+ GLuint query;
+ glGenQueries(1, &query);
+ return query;
+ }
+
+ GLuint query = mQueryPool.front();
+ mQueryPool.pop_front();
+ return query;
+}
+
+void LLReflectionMapManager::recycleQuery(GLuint query)
+{
+ mQueryPool.push_back(query);
+}
+
struct CompareProbeDepth
{
bool operator()(const LLReflectionMap* lhs, const LLReflectionMap* rhs)
@@ -713,6 +733,7 @@ void LLReflectionMapManager::deleteProbe(U32 i)
other->mNeighbors.erase(iter);
}
+ // Probes are distance sorted, order matters.
mProbes.erase(mProbes.begin() + i);
}
@@ -1549,10 +1570,23 @@ void LLReflectionMapManager::cleanup()
glDeleteBuffers(1, &mUBO);
mUBO = 0;
+ cleanupQueryPool();
+
// note: also called on teleport (not just shutdown), so make sure we're in a good "starting" state
initCubeFree();
}
+void LLReflectionMapManager::cleanupQueryPool()
+{
+ if (!mQueryPool.empty())
+ {
+ LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("cleanup query pool");
+ std::vector queries(mQueryPool.begin(), mQueryPool.end());
+ glDeleteQueries(static_cast(queries.size()), queries.data());
+ mQueryPool.clear();
+ }
+}
+
void LLReflectionMapManager::doOcclusion()
{
LLVector4a eye;
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index 5daed7d1cf1..1e1f9ad1a2a 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -106,6 +106,7 @@ class alignas(16) LLReflectionMapManager
// release any GL state
void cleanup();
+ void cleanupQueryPool();
// maintain reflection probes
void update();
@@ -164,6 +165,10 @@ class alignas(16) LLReflectionMapManager
U32 probeCount();
U32 probeMemory();
+ // glDeleteQueries is expensive, so we maintain a pool of queries
+ GLuint allocateQuery();
+ void recycleQuery(GLuint query);
+
private:
friend class LLPipeline;
friend class LLHeroProbeManager;
@@ -190,6 +195,8 @@ class alignas(16) LLReflectionMapManager
// bind UBO used for rendering
void setUniforms();
+ std::deque mQueryPool;
+
// render target for cube snapshots
// used to generate mipmaps without doing a copy-to-texture
LLRenderTarget mRenderTarget;
diff --git a/indra/newview/llremoteparcelrequest.cpp b/indra/newview/llremoteparcelrequest.cpp
index f89afd38ab6..c1b33f313bc 100644
--- a/indra/newview/llremoteparcelrequest.cpp
+++ b/indra/newview/llremoteparcelrequest.cpp
@@ -84,6 +84,7 @@ void LLRemoteParcelInfoProcessor::removeObserver(const LLUUID& parcel_id, LLRemo
//static
void LLRemoteParcelInfoProcessor::processParcelInfoReply(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLParcelData parcel_data;
msg->getUUID ("Data", "ParcelID", parcel_data.parcel_id);
diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp
index 415e6cfa72d..1b64eab3c0a 100644
--- a/indra/newview/llselectmgr.cpp
+++ b/indra/newview/llselectmgr.cpp
@@ -3054,7 +3054,6 @@ void LLSelectMgr::logNoOp(LLSelectNode* node, void *)
// static
void LLSelectMgr::logAttachmentRequest(LLSelectNode* node, void *)
{
- LLAttachmentsMgr::instance().onAttachmentRequested(node->mItemID);
}
// static
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 59d97943e3c..9bdfbaedc8c 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -29,6 +29,11 @@
#include "llappviewer.h"
#include "llstartup.h"
+#if LL_VELOPACK && LL_WINDOWS
+#include "llvelopack.h"
+#include
+#endif
+
#if LL_WINDOWS
# include // _spawnl()
#else
@@ -266,6 +271,7 @@ std::unique_ptr LLStartUp::sPhases(new LLViewerStats::P
void login_show();
void login_callback(S32 option, void* userdata);
+void uninstall_nsis_if_required();
void show_release_notes_if_required();
void show_first_run_dialog();
bool first_run_dialog_callback(const LLSD& notification, const LLSD& response);
@@ -921,6 +927,7 @@ bool idle_startup()
LL_DEBUGS("AppInit") << "PeekMessage processed" << LL_ENDL;
#endif
do_startup_frame();
+ uninstall_nsis_if_required();
timeout.reset();
return false;
}
@@ -2605,6 +2612,67 @@ void release_notes_coro(const std::string url)
LLWeb::loadURLInternal(url);
}
+/**
+* Check if this is a fresh velopack install and
+* if uninstallation of old viewer is needed.
+*/
+void uninstall_nsis_if_required()
+{
+#if LL_VELOPACK && LL_WINDOWS
+ bool checked_for_legacy_install = gSavedSettings.getBOOL("PreviousInstallChecked");
+ if (checked_for_legacy_install)
+ {
+ return;
+ }
+ gSavedSettings.setBOOL("PreviousInstallChecked", true);
+
+ LL_INFOS() << "Looking for previous NSIS installs" << LL_ENDL;
+
+ S32 found_major = 0;
+ S32 found_minor = 0;
+ S32 found_patch = 0;
+ U64 found_build = 0;
+
+ if (!get_nsis_version(found_major, found_minor, found_patch, found_build))
+ {
+ return;
+ }
+
+ LLVersionInfo* ver_inst = LLVersionInfo::getInstance();
+
+ if (found_major > ver_inst->getMajor())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ if (found_major == ver_inst->getMajor()
+ && found_minor > ver_inst->getMinor())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ if (found_major == ver_inst->getMajor()
+ && found_minor == ver_inst->getMinor()
+ && found_patch > ver_inst->getPatch())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ // Assume that nsis is going to be something like x.x.x, while velopack is x.x.(x+1),
+ // so there is no point to check build.
+ LL_INFOS() << "Found NSIS install " << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+
+ clear_nsis_links();
+
+ LLSD args;
+ args["VERSION"] = llformat("%d.%d.%d", found_major, found_minor, found_patch);
+ LLNotificationsUtil::add("FoundLegacyNsisInstallation", args);
+#endif
+}
+
void validate_release_notes_coro(const std::string url)
{
LLVersionInfo& versionInfo(LLVersionInfo::instance());
@@ -2638,15 +2706,24 @@ void show_release_notes_if_required()
// below. If viewer release notes stop working, might be because that
// LLEventMailDrop got moved out of LLVersionInfo and hasn't yet been
// instantiated.
- if (!release_notes_shown && (LLVersionInfo::instance().getChannelAndVersion() != gLastRunVersion)
- && LLVersionInfo::instance().getViewerMaturity() != LLVersionInfo::TEST_VIEWER // don't show Release Notes for the test builds
- && gSavedSettings.getBOOL("UpdaterShowReleaseNotes")
- && !gSavedSettings.getBOOL("FirstLoginThisInstall"))
+ if (release_notes_shown
+ || LLVersionInfo::instance().getChannelAndVersion() == gLastRunVersion
+ || gSavedSettings.getBOOL("FirstLoginThisInstall")) // New users don't need to see release notes
+ {
+ return;
+ }
+ S32 mode = gSavedSettings.getS32("UpdaterShowReleaseNotes");
+ if (mode == 0)
+ {
+ return;
+ }
+ if (mode == 2 // Show even for test builds
+ || LLVersionInfo::instance().getViewerMaturity() != LLVersionInfo::TEST_VIEWER) // don't show Release Notes for the test builds
+
{
#if LL_RELEASE_FOR_DOWNLOAD
- if (!gSavedSettings.getBOOL("CmdLineSkipUpdater")
- && !LLAppViewer::instance()->isUpdaterMissing())
+ if (!gSavedSettings.getBOOL("CmdLineSkipUpdater"))
{
// Instantiate a "relnotes" listener which assumes any arriving event
// is the release notes URL string. Since "relnotes" is an
@@ -3164,14 +3241,22 @@ void reset_login()
if ( gViewerWindow )
{ // Hide menus and normal buttons
gViewerWindow->setNormalControlsVisible( false );
- gLoginMenuBarView->setVisible( true );
- gLoginMenuBarView->setEnabled( true );
+
+ if (gLoginMenuBarView)
+ {
+ gLoginMenuBarView->setVisible(true);
+ gLoginMenuBarView->setEnabled(true);
+ }
+ else
+ {
+ LL_WARNS("AppInit") << "gLoginMenuBarView not initialized" << LL_ENDL;
+ }
}
// Hide any other stuff
LLFloaterReg::hideVisibleInstances();
- if (LLStartUp::getStartupState() > STATE_WORLD_INIT)
+ if (LLStartUp::getStartupState() > STATE_WORLD_INIT && gViewerWindow)
{
gViewerWindow->resetStatusBarContainer();
}
diff --git a/indra/newview/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp
index 52ec8c17c1d..001c4a0449d 100644
--- a/indra/newview/lltexturectrl.cpp
+++ b/indra/newview/lltexturectrl.cpp
@@ -285,7 +285,7 @@ void LLFloaterTexturePicker::setImageIDFromItem(const LLInventoryItem* itemp, bo
void LLFloaterTexturePicker::setActive( bool active )
{
- if (!active && getChild("Pipette")->getValue().asBoolean())
+ if (!active && mPipetteBtn->getValue().asBoolean())
{
stopUsingPipette();
}
@@ -658,7 +658,10 @@ bool LLFloaterTexturePicker::postBuild()
mSavedFolderState.setApply(false);
- LLToolPipette::getInstance()->setToolSelectCallback(boost::bind(&LLFloaterTexturePicker::onTextureSelect, this, _1));
+ mPipetteConnection = LLToolPipette::getInstance()->setToolSelectCallback([this](LLPointer object, S32 te_index)
+ {
+ onPipetteSelect(object, te_index);
+ });
getChild("l_bake_use_texture_combo_box")->setCommitCallback(onBakeTextureSelect, this);
@@ -1069,7 +1072,7 @@ void LLFloaterTexturePicker::onBtnSelect(void* userdata)
void LLFloaterTexturePicker::onBtnPipette()
{
- bool pipette_active = getChild("Pipette")->getValue().asBoolean();
+ bool pipette_active = mPipetteBtn->getValue().asBoolean();
pipette_active = !pipette_active;
if (pipette_active)
{
@@ -1419,8 +1422,7 @@ void LLFloaterTexturePicker::changeMode()
getChild("l_bake_use_texture_combo_box")->setVisible(index == PICKER_BAKE);
getChild("hide_base_mesh_region")->setVisible(false);// index == 2);
- bool pipette_visible = (index == PICKER_INVENTORY)
- && (mInventoryPickType != PICK_MATERIAL);
+ bool pipette_visible = (index == PICKER_INVENTORY);
mPipetteBtn->setVisible(pipette_visible);
if (index == PICKER_BAKE)
@@ -1560,15 +1562,8 @@ void LLFloaterTexturePicker::setInventoryPickType(EPickInventoryType type)
refreshLocalList();
refreshInventoryFilter();
- if (mInventoryPickType == PICK_MATERIAL)
- {
- getChild("Pipette")->setVisible(false);
- }
- else
- {
- S32 index = mModeSelector->getValue().asInteger();
- getChild("Pipette")->setVisible(index == 0);
- }
+ S32 index = mModeSelector->getValue().asInteger();
+ mPipetteBtn->setVisible(index == 0);
if (!mLabel.empty())
{
@@ -1640,39 +1635,88 @@ void LLFloaterTexturePicker::onPickerCallback(const std::vector& fi
}
}
-void LLFloaterTexturePicker::onTextureSelect( const LLTextureEntry& te )
+void LLFloaterTexturePicker::onPipetteSelect(LLPointer& object, S32 te_index)
{
- LLUUID inventory_item_id = findItemID(te.getID(), true);
- if (inventory_item_id.notNull())
+ if (mInventoryPickType == PICK_MATERIAL)
{
- LLToolPipette::getInstance()->setResult(true, "");
- if (mInventoryPickType == PICK_MATERIAL)
- {
- // tes have no data about material ids
- // Plus gltf materials are layered with overrides,
- // which mean that end result might have no id.
- LL_WARNS() << "tes have no data about material ids" << LL_ENDL;
- }
- else
+ // Note: does not copy overrides!
+ LLUUID mat_id = object->getRenderMaterialID(te_index);
+ if (mat_id == BLANK_MATERIAL_ASSET_ID)
{
- setImageID(te.getID());
+ // It's fine if blank material isn't in inventory, just set it
+ LLToolPipette::getInstance()->setResult(true, "");
+ setImageID(mat_id);
setTentative(false);
- }
- mNoCopyTextureSelected = false;
- LLInventoryItem* itemp = gInventory.getItem(inventory_item_id);
+ mNoCopyTextureSelected = false;
- if (itemp && !itemp->getPermissions().allowCopyBy(gAgent.getID()))
+ commitIfImmediateSet();
+ }
+ else if (mat_id.isNull())
{
- // no copy texture
- mNoCopyTextureSelected = true;
+ LLToolPipette::getInstance()->setResult(false, LLTrans::getString("InventoryNoMaterial"));
}
+ else
+ {
+ LLUUID inventory_item_id = findItemID(mat_id, true);
+ if (inventory_item_id.notNull())
+ {
+ LLToolPipette::getInstance()->setResult(true, "");
+ setImageID(mat_id);
+ setTentative(false);
+
+ mNoCopyTextureSelected = false;
+ LLInventoryItem* itemp = gInventory.getItem(inventory_item_id);
+
+ if (itemp && !itemp->getPermissions().allowCopyBy(gAgent.getID()))
+ {
+ // no copy texture
+ mNoCopyTextureSelected = true;
+ }
- commitIfImmediateSet();
+ commitIfImmediateSet();
+ }
+ else
+ {
+ // Not in inventory, can't apply
+ LLToolPipette::getInstance()->setResult(false, LLTrans::getString("InventoryNoMaterial"));
+ }
+ }
}
else
{
- LLToolPipette::getInstance()->setResult(false, LLTrans::getString("InventoryNoTexture"));
+ const LLTextureEntry* entry = object->getTE(te_index);
+ if (!entry)
+ {
+ // Whatever was selected is not a face/TE,
+ // no texture to check, so do nothing.
+ // Should not be reachable, if you hit this,
+ // check what happens in pipette tool.
+ llassert(false);
+ return;
+ }
+ LLUUID inventory_item_id = findItemID(entry->getID(), true);
+ if (inventory_item_id.notNull())
+ {
+ LLToolPipette::getInstance()->setResult(true, "");
+ setImageID(entry->getID());
+ setTentative(false);
+
+ mNoCopyTextureSelected = false;
+ LLInventoryItem* itemp = gInventory.getItem(inventory_item_id);
+
+ if (itemp && !itemp->getPermissions().allowCopyBy(gAgent.getID()))
+ {
+ // no copy texture
+ mNoCopyTextureSelected = true;
+ }
+
+ commitIfImmediateSet();
+ }
+ else
+ {
+ LLToolPipette::getInstance()->setResult(false, LLTrans::getString("InventoryNoTexture"));
+ }
}
}
diff --git a/indra/newview/lltexturectrl.h b/indra/newview/lltexturectrl.h
index e0060474cee..759711ebf7e 100644
--- a/indra/newview/lltexturectrl.h
+++ b/indra/newview/lltexturectrl.h
@@ -374,7 +374,7 @@ class LLFloaterTexturePicker : public LLFloater
static void onBtnNone(void* userdata);
void onSelectionChange(const std::deque &items, bool user_action);
static void onApplyImmediateCheck(LLUICtrl* ctrl, void* userdata);
- void onTextureSelect(const LLTextureEntry& te);
+ void onPipetteSelect(LLPointer& object, S32 te_index);
static void onModeSelect(LLUICtrl* ctrl, void *userdata);
static void onBtnAdd(void* userdata);
@@ -457,6 +457,8 @@ class LLFloaterTexturePicker : public LLFloater
set_image_asset_id_callback mSetImageAssetIDCallback;
set_on_update_image_stats_callback mOnUpdateImageStatsCallback;
+ boost::signals2::scoped_connection mPipetteConnection;
+
bool mBakeTextureEnabled;
bool mLocalTextureEnabled;
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/lltoastnotifypanel.cpp b/indra/newview/lltoastnotifypanel.cpp
index 6c0b3bfa13d..eb77343fd39 100644
--- a/indra/newview/lltoastnotifypanel.cpp
+++ b/indra/newview/lltoastnotifypanel.cpp
@@ -38,6 +38,7 @@
#include "llnotifications.h"
#include "lluiconstants.h"
#include "llrect.h"
+#include "llstring.h"
#include "lltrans.h"
#include "llnotificationsutil.h"
#include "llviewermessage.h"
@@ -90,8 +91,24 @@ LLButton* LLToastNotifyPanel::createButton(const LLSD& form_element, bool is_opt
std::string name = form_element["name"].asString();
std::string text = form_element["text"].asString();
bool make_small_btn = index == -1 || index == -2; // for block and ignore buttons in script dialog
+
+ // Use Emoji font as exception if text contains red heart emoji (10084 U+2764) to ensure proper rendering
+ std::string font_name = mIsScriptDialog ? sFontScript : sFontDefault;
+ if (mIsScriptDialog)
+ {
+ LLWString wtext = utf8str_to_wstring(text);
+ for (llwchar ch : wtext)
+ {
+ if (ch == 0x2764)
+ {
+ font_name = "Emoji";
+ break;
+ }
+ }
+ }
+
const LLFontGL* font = LLFontGL::getFont(LLFontDescriptor(
- mIsScriptDialog ? sFontScript : sFontDefault, make_small_btn ? "Small" : "Medium", 0));
+ font_name, make_small_btn ? "Small" : "Medium", 0));
p.name = name;
p.label = text;
p.tool_tip = text;
diff --git a/indra/newview/lltoolmgr.cpp b/indra/newview/lltoolmgr.cpp
index 07963a7bed7..c7e6f774dc3 100644
--- a/indra/newview/lltoolmgr.cpp
+++ b/indra/newview/lltoolmgr.cpp
@@ -57,6 +57,7 @@
#include "llviewerjoystick.h"
#include "llviewermenu.h"
#include "llviewerparcelmgr.h"
+#include "lleventapi.h"
// Used when app not active to avoid processing hover.
@@ -68,6 +69,67 @@ LLToolset* gCameraToolset = NULL;
LLToolset* gMouselookToolset = NULL;
LLToolset* gFaceEditToolset = NULL;
+/////////////////////////////////////////////////////
+// LLToolMgrListener
+
+class LLToolMgrListener: public LLEventAPI
+{
+public:
+ LLToolMgrListener():
+ LLEventAPI("LLToolMgr",
+ "LLToolMgr listener for tool management operations")
+ {
+ add("openBuildFloater",
+ "Open the build floater by toggling build mode",
+ &LLToolMgrListener::openFloater);
+ add("selectTool",
+ "Select the specified tool by [\"tool_name\"]. Valid tool names: Focus, Move, Edit, Create, Land",
+ &LLToolMgrListener::selectTool);
+ }
+
+private:
+ void openFloater(const LLSD& event) const
+ {
+ LLToolMgr::getInstance()->toggleBuildMode(LLSD("build"));
+ }
+
+ void selectTool(const LLSD& event) const
+ {
+ if (!event.has("tool_name"))
+ {
+ LL_WARNS() << "selectTool: called without tool_name" << LL_ENDL;
+ return;
+ }
+
+ static const std::unordered_map tool_indices = {
+ {"focus", 1},
+ {"move", 2},
+ {"edit", 3},
+ {"create", 4},
+ {"land", 5}
+ };
+
+ const std::string tool_name = utf8str_tolower(event["tool_name"].asString());
+ const auto it = tool_indices.find(tool_name);
+ if (it == tool_indices.end())
+ {
+ LL_WARNS() << "selectTool: unknown tool_name: " << std::quoted(tool_name) << LL_ENDL;
+ return;
+ }
+
+ LLToolset* current_toolset = LLToolMgr::getInstance()->getCurrentToolset();
+ if (!current_toolset)
+ {
+ LL_WARNS() << "selectTool: no current toolset available" << LL_ENDL;
+ return;
+ }
+
+ current_toolset->selectToolByIndex(it->second);
+ }
+};
+
+static LLToolMgrListener sToolMgrListener;
+
/////////////////////////////////////////////////////
// LLToolMgr
diff --git a/indra/newview/lltoolpipette.cpp b/indra/newview/lltoolpipette.cpp
index 9e3d4356887..6ec25e95122 100644
--- a/indra/newview/lltoolpipette.cpp
+++ b/indra/newview/lltoolpipette.cpp
@@ -47,8 +47,8 @@
//
LLToolPipette::LLToolPipette()
-: LLTool(std::string("Pipette")),
- mSuccess(true)
+: LLTool(std::string("Pipette"))
+, mSuccess(true)
{
}
@@ -103,15 +103,6 @@ bool LLToolPipette::handleToolTip(S32 x, S32 y, MASK mask)
return true;
}
-void LLToolPipette::setTextureEntry(const LLTextureEntry* entry)
-{
- if (entry)
- {
- mTextureEntry = *entry;
- mSignal(mTextureEntry);
- }
-}
-
void LLToolPipette::pickCallback(const LLPickInfo& pick_info)
{
LLViewerObject* hit_obj = pick_info.getObject();
@@ -120,12 +111,14 @@ void LLToolPipette::pickCallback(const LLPickInfo& pick_info)
// if we clicked on a face of a valid prim, save off texture entry data
if (hit_obj &&
hit_obj->getPCode() == LL_PCODE_VOLUME &&
- pick_info.mObjectFace != -1)
+ pick_info.mObjectFace != -1 &&
+ hit_obj->getNumTEs() > pick_info.mObjectFace)
{
//TODO: this should highlight the selected face only
LLSelectMgr::getInstance()->highlightObjectOnly(hit_obj);
- const LLTextureEntry* entry = hit_obj->getTE(pick_info.mObjectFace);
- LLToolPipette::getInstance()->setTextureEntry(entry);
+
+ LLPointer hit_obj_ptr = hit_obj;
+ LLToolPipette::getInstance()->mSignal(hit_obj_ptr, pick_info.mObjectFace);
}
}
diff --git a/indra/newview/lltoolpipette.h b/indra/newview/lltoolpipette.h
index 6c79674d765..7771f467e54 100644
--- a/indra/newview/lltoolpipette.h
+++ b/indra/newview/lltoolpipette.h
@@ -51,16 +51,13 @@ class LLToolPipette
virtual bool handleHover(S32 x, S32 y, MASK mask) override;
virtual bool handleToolTip(S32 x, S32 y, MASK mask) override;
- // Note: Don't return connection; use boost::bind + boost::signals2::trackable to disconnect slots
- typedef boost::signals2::signal signal_t;
- void setToolSelectCallback(const signal_t::slot_type& cb) { mSignal.connect(cb); }
+ typedef boost::signals2::signal obj, S32 te_index)> signal_t;
+ boost::signals2::connection setToolSelectCallback(const signal_t::slot_type& cb) { return mSignal.connect(cb); }
void setResult(bool success, const std::string& msg);
- void setTextureEntry(const LLTextureEntry* entry);
static void pickCallback(const LLPickInfo& pick_info);
protected:
- LLTextureEntry mTextureEntry;
signal_t mSignal;
bool mSuccess;
std::string mTooltipMsg;
diff --git a/indra/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp
index beae71e7bf9..d95560b3065 100644
--- a/indra/newview/lluilistener.cpp
+++ b/indra/newview/lluilistener.cpp
@@ -37,6 +37,7 @@
#include "llui.h" // getRootView(), resolvePath()
#include "lluictrl.h"
#include "llerror.h"
+#include "llcombobox.h"
LLUIListener::LLUIListener():
@@ -55,6 +56,12 @@ LLUIListener::LLUIListener():
"current value as [\"value\"] reply.",
&LLUIListener::getValue,
LLSDMap("path", LLSD())("reply", LLSD()));
+
+ add("setSelectedByValue",
+ "For the combobox identified by the path in [\"path\"] set selection by [\"value\"],\n"
+ "and return result as [\"reply\"]",
+ &LLUIListener::setSelectedByValue,
+ llsd::map("path", LLSD(), "value", LLSD(), "reply", LLSD()));
}
void LLUIListener::call(const LLSD& event) const
@@ -99,3 +106,20 @@ void LLUIListener::getValue(const LLSD&event) const
sendReply(reply, event);
}
+
+void LLUIListener::setSelectedByValue(const LLSD& event) const
+{
+ Response response(LLSD(), event);
+ std::string path(event["path"]);
+ LLComboBox* combo_ctrl = dynamic_cast(LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path));
+ if (combo_ctrl)
+ {
+ response.setResponse(combo_ctrl->setSelectedByValue(event["value"], true));
+ return;
+ }
+ else
+ {
+ LL_WARNS() << "Specified combobox doesn't exist: " << path << LL_ENDL;
+ }
+ response.setResponse(false);
+}
diff --git a/indra/newview/lluilistener.h b/indra/newview/lluilistener.h
index 70455c2c685..228e67ae266 100644
--- a/indra/newview/lluilistener.h
+++ b/indra/newview/lluilistener.h
@@ -42,6 +42,7 @@ class LLUIListener: public LLEventAPI
private:
void call(const LLSD& event) const;
void getValue(const LLSD&event) const;
+ void setSelectedByValue(const LLSD& event) const;
};
#endif /* ! defined(LL_LLUILISTENER_H) */
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
new file mode 100644
index 00000000000..d34d43cc481
--- /dev/null
+++ b/indra/newview/llvelopack.cpp
@@ -0,0 +1,1206 @@
+/**
+ * @file llvelopack.cpp
+ * @brief Velopack installer and update framework integration
+ *
+ * $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$
+ */
+
+#if LL_VELOPACK
+
+#include "llviewerprecompiledheaders.h"
+#include "llvelopack.h"
+#include "llstring.h"
+#include "llcorehttputil.h"
+#include "llversioninfo.h"
+
+#include
+#include
+#include
+#include "llnotificationsutil.h"
+#include "llviewercontrol.h"
+#include "llappviewer.h"
+#include "llcoros.h"
+
+#include "Velopack.h"
+
+#if LL_WINDOWS
+#include "llappviewerwin32.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "shlwapi.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "shell32.lib")
+#endif // LL_WINDOWS
+
+// Common state
+static std::string sUpdateUrl;
+static std::function sProgressCallback;
+static vpkc_update_manager_t* sUpdateManager = nullptr;
+static vpkc_update_info_t* sPendingUpdate = nullptr; // Downloaded, ready to apply
+static vpkc_update_info_t* sPendingCheckInfo = nullptr; // Checked, awaiting user response
+static vpkc_update_source_t* sUpdateSource = nullptr;
+static LLNotificationPtr sDownloadingNotification;
+static bool sRestartAfterUpdate = false;
+static bool sIsRequired = false; // Is the pending check a required update?
+static std::string sReleaseNotesUrl;
+static std::string sTargetVersion; // Velopack's actual target version
+
+// Forward declarations
+static void show_required_update_prompt();
+static void show_downloading_notification(const std::string& version);
+static void ensure_update_manager(bool allow_downgrade);
+static void velopack_download_pending_update();
+static std::unordered_map sAssetUrlMap; // basename -> original absolute URL
+
+//
+// Custom update source helpers
+//
+
+static std::string extract_basename(const std::string& url)
+{
+ // Strip query params / fragment
+ std::string path = url;
+ auto qpos = path.find('?');
+ if (qpos != std::string::npos) path = path.substr(0, qpos);
+ auto fpos = path.find('#');
+ if (fpos != std::string::npos) path = path.substr(0, fpos);
+
+ auto spos = path.rfind('/');
+ if (spos != std::string::npos && spos + 1 < path.size())
+ return path.substr(spos + 1);
+ return path;
+}
+
+static void rewrite_asset_urls(boost::json::value& jv)
+{
+ if (jv.is_object())
+ {
+ auto& obj = jv.as_object();
+ auto it = obj.find("FileName");
+ if (it != obj.end() && it->value().is_string())
+ {
+ std::string filename(it->value().as_string());
+ if (filename.find("://") != std::string::npos)
+ {
+ std::string basename = extract_basename(filename);
+ sAssetUrlMap[basename] = filename;
+ it->value() = basename;
+ LL_DEBUGS("Velopack") << "Rewrote FileName: " << basename << LL_ENDL;
+ }
+ }
+ for (auto& kv : obj)
+ {
+ rewrite_asset_urls(kv.value());
+ }
+ }
+ else if (jv.is_array())
+ {
+ for (auto& elem : jv.as_array())
+ {
+ rewrite_asset_urls(elem);
+ }
+ }
+}
+
+static std::string rewrite_release_feed(const std::string& json_str)
+{
+ boost::json::value jv = boost::json::parse(json_str);
+ rewrite_asset_urls(jv);
+ return boost::json::serialize(jv);
+}
+
+static std::string download_url_raw(const std::string& url)
+{
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ auto httpAdapter = std::make_shared("VelopackSource", httpPolicy);
+ auto httpRequest = std::make_shared();
+ auto httpOpts = std::make_shared();
+ httpOpts->setFollowRedirects(true);
+
+ LLSD result = httpAdapter->getRawAndSuspend(httpRequest, url, httpOpts);
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+ if (!status)
+ {
+ LL_WARNS("Velopack") << "HTTP request failed for " << url << ": " << status.toString() << LL_ENDL;
+ return {};
+ }
+
+ const LLSD::Binary& rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
+ return std::string(rawBody.begin(), rawBody.end());
+}
+
+static bool download_url_to_file(const std::string& url, const std::string& local_path)
+{
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ auto httpAdapter = std::make_shared("VelopackDownload", httpPolicy);
+ auto httpRequest = std::make_shared();
+ auto httpOpts = std::make_shared();
+ httpOpts->setFollowRedirects(true);
+ httpOpts->setTransferTimeout(1200);
+
+ LLSD result = httpAdapter->getRawAndSuspend(httpRequest, url, httpOpts);
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+ if (!status)
+ {
+ LL_WARNS("Velopack") << "Download failed for " << url << ": " << status.toString() << LL_ENDL;
+ return false;
+ }
+
+ const LLSD::Binary& rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
+ llofstream outFile(local_path, std::ios::binary | std::ios::trunc);
+ if (!outFile.is_open())
+ {
+ LL_WARNS("Velopack") << "Failed to open file for writing: " << local_path << LL_ENDL;
+ return false;
+ }
+ outFile.write(reinterpret_cast(rawBody.data()), rawBody.size());
+ outFile.close();
+ return true;
+}
+
+//
+// Custom source callbacks
+//
+
+static char* custom_get_release_feed(void* user_data, const char* releases_name)
+{
+ std::string base = sUpdateUrl;
+ if (!base.empty() && base.back() == '/')
+ base.pop_back();
+ std::string url = base + "/" + releases_name;
+ LL_INFOS("Velopack") << "Fetching release feed: " << url << LL_ENDL;
+
+ std::string json_str = download_url_raw(url);
+ if (json_str.empty())
+ {
+ return nullptr;
+ }
+
+ try
+ {
+ std::string rewritten = rewrite_release_feed(json_str);
+ char* result = static_cast(malloc(rewritten.size() + 1));
+ if (result)
+ {
+ memcpy(result, rewritten.c_str(), rewritten.size() + 1);
+ }
+ return result;
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("Velopack") << "Failed to parse/rewrite release feed: " << e.what() << LL_ENDL;
+ // Return original unmodified feed as fallback
+ char* result = static_cast(malloc(json_str.size() + 1));
+ if (result)
+ {
+ memcpy(result, json_str.c_str(), json_str.size() + 1);
+ }
+ return result;
+ }
+}
+
+static void custom_free_release_feed(void* user_data, char* feed)
+{
+ free(feed);
+}
+
+static std::string sPreDownloadedAssetPath;
+
+static bool custom_download_asset(void* user_data,
+ const vpkc_asset_t* asset,
+ const char* local_path,
+ size_t progress_callback_id)
+{
+ // 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.
+ if (sPreDownloadedAssetPath.empty())
+ {
+ LL_WARNS("Velopack") << "No pre-downloaded asset available" << LL_ENDL;
+ return false;
+ }
+
+ std::string filename = asset->FileName ? asset->FileName : "";
+ LL_INFOS("Velopack") << "Download asset callback: filename=" << filename
+ << " local_path=" << local_path
+ << " size=" << asset->Size << LL_ENDL;
+ vpkc_source_report_progress(progress_callback_id, 0);
+
+ std::ifstream src(sPreDownloadedAssetPath, std::ios::binary);
+ llofstream dst(local_path, std::ios::binary | std::ios::trunc);
+ if (!src.is_open() || !dst.is_open())
+ {
+ LL_WARNS("Velopack") << "Failed to open files for copy" << LL_ENDL;
+ return false;
+ }
+
+ dst << src.rdbuf();
+ dst.close();
+ src.close();
+
+ vpkc_source_report_progress(progress_callback_id, 100);
+ LL_INFOS("Velopack") << "Asset copy complete" << LL_ENDL;
+ return true;
+}
+
+//
+// Platform-specific helpers and hooks
+//
+
+#if LL_WINDOWS
+
+static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife";
+static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info";
+static std::wstring get_viewer_exe_name()
+{
+ return ll_convert(gDirUtilp->getExecutableFilename());
+}
+
+static std::wstring get_install_dir()
+{
+ wchar_t path[MAX_PATH];
+ GetModuleFileNameW(NULL, path, MAX_PATH);
+ PathRemoveFileSpecW(path);
+ return path;
+}
+
+static std::wstring get_app_name()
+{
+ // Match viewer_manifest.py app_name() logic: release channel uses "Viewer"
+ // suffix instead of "Release" for display purposes (shortcuts, uninstall, etc.)
+ std::wstring channel = LL_TO_WSTRING(LL_VIEWER_CHANNEL);
+ std::wstring release_suffix = L" Release";
+ if (channel.size() >= release_suffix.size() &&
+ channel.compare(channel.size() - release_suffix.size(), release_suffix.size(), release_suffix) == 0)
+ {
+ channel.replace(channel.size() - release_suffix.size(), release_suffix.size(), L" Viewer");
+ }
+ return channel;
+}
+
+static std::wstring get_app_name_oneword()
+{
+ std::wstring name = get_app_name();
+ name.erase(std::remove(name.begin(), name.end(), L' '), name.end());
+ return name;
+}
+
+static std::wstring get_start_menu_path()
+{
+ wchar_t path[MAX_PATH];
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PROGRAMS, NULL, 0, path)))
+ {
+ return path;
+ }
+ return L"";
+}
+
+static std::wstring get_desktop_path()
+{
+ wchar_t path[MAX_PATH];
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, path)))
+ {
+ return path;
+ }
+ return L"";
+}
+
+static bool create_shortcut(const std::wstring& shortcut_path,
+ const std::wstring& target_path,
+ const std::wstring& arguments,
+ const std::wstring& description,
+ const std::wstring& icon_path)
+{
+ HRESULT hr = CoInitialize(NULL);
+ if (FAILED(hr)) return false;
+
+ IShellLinkW* shell_link = nullptr;
+ hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, (void**)&shell_link);
+ if (SUCCEEDED(hr))
+ {
+ shell_link->SetPath(target_path.c_str());
+ shell_link->SetArguments(arguments.c_str());
+ shell_link->SetDescription(description.c_str());
+ shell_link->SetIconLocation(icon_path.c_str(), 0);
+
+ wchar_t work_dir[MAX_PATH];
+ wcscpy_s(work_dir, target_path.c_str());
+ PathRemoveFileSpecW(work_dir);
+ shell_link->SetWorkingDirectory(work_dir);
+
+ IPersistFile* persist_file = nullptr;
+ hr = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
+ if (SUCCEEDED(hr))
+ {
+ hr = persist_file->Save(shortcut_path.c_str(), TRUE);
+ persist_file->Release();
+ }
+ shell_link->Release();
+ }
+
+ CoUninitialize();
+ return SUCCEEDED(hr);
+}
+
+static void register_protocol_handler(const std::wstring& protocol,
+ const std::wstring& description,
+ const std::wstring& exe_path)
+{
+ std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol;
+ HKEY hkey;
+
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ RegSetValueExW(hkey, NULL, 0, REG_SZ,
+ (BYTE*)description.c_str(), (DWORD)((description.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"URL Protocol", 0, REG_SZ, (BYTE*)L"", sizeof(wchar_t));
+ RegCloseKey(hkey);
+ }
+
+ std::wstring icon_key_path = key_path + L"\\DefaultIcon";
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, icon_key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ std::wstring icon_value = L"\"" + exe_path + L"\"";
+ RegSetValueExW(hkey, NULL, 0, REG_SZ,
+ (BYTE*)icon_value.c_str(), (DWORD)((icon_value.size() + 1) * sizeof(wchar_t)));
+ RegCloseKey(hkey);
+ }
+
+ std::wstring cmd_key_path = key_path + L"\\shell\\open\\command";
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, cmd_key_path.c_str(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS)
+ {
+ std::wstring cmd_value = L"\"" + exe_path + L"\" -url \"%1\"";
+ RegSetValueExW(hkey, NULL, 0, REG_EXPAND_SZ,
+ (BYTE*)cmd_value.c_str(), (DWORD)((cmd_value.size() + 1) * sizeof(wchar_t)));
+ RegCloseKey(hkey);
+ }
+}
+
+void clear_nsis_links()
+{
+ wchar_t path[MAX_PATH];
+
+ // 1. The 'start' shortcuts set by nsis would be global, like app shortcut:
+ // C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Second Life Viewer\Second Life Viewer.lnk
+ // But it isn't just one link, it's a whole directory that needs to be removed.
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, path)))
+ {
+ std::wstring start_menu_path = path;
+ std::wstring folder_path = start_menu_path + L"\\" + get_app_name();
+
+ std::error_code ec;
+ std::filesystem::path dir(folder_path);
+ if (std::filesystem::exists(dir, ec))
+ {
+ std::filesystem::remove_all(dir, ec);
+ if (ec)
+ {
+ LL_WARNS("Velopack") << "Failed to remove NSIS start menu directory: "
+ << ll_convert_wide_to_string(folder_path) << LL_ENDL;
+ }
+ }
+ }
+
+ // 2. Desktop link, also a global one.
+ // C:\Users\Public\Desktop
+ if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_DESKTOPDIRECTORY, NULL, 0, path)))
+ {
+ std::wstring desktop_path = path;
+ std::wstring shortcut_path = desktop_path + L"\\" + get_app_name() + L".lnk";
+ if (!DeleteFileW(shortcut_path.c_str()))
+ {
+ DWORD error = GetLastError();
+ if (error != ERROR_FILE_NOT_FOUND)
+ {
+ LL_WARNS("Velopack") << "Failed to delete NSIS desktop shortcut: "
+ << ll_convert_wide_to_string(shortcut_path)
+ << " (error: " << error << ")" << LL_ENDL;
+ }
+ }
+ }
+}
+
+static void parse_version(const wchar_t* version_str, int& major, int& minor, int& patch, uint64_t& build)
+{
+ major = minor = patch = 0;
+ build = 0;
+ if (!version_str) return;
+ // Use swscanf for wide strings
+ swscanf(version_str, L"%d.%d.%d.%llu", &major, &minor, &patch, &build);
+}
+
+bool get_nsis_version(
+ int& nsis_major,
+ int& nsis_minor,
+ int& nsis_patch,
+ uint64_t& nsis_build)
+{
+ // Test for presence of NSIS viewer registration, then
+ // attempt to read uninstall info
+ std::wstring app_name_oneword = get_app_name_oneword();
+ std::wstring uninstall_key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword;
+ HKEY hkey;
+ LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, uninstall_key_path.c_str(), 0, KEY_READ, &hkey);
+ if (result != ERROR_SUCCESS)
+ {
+ return false;
+ }
+
+ // Read DisplayVersion
+ wchar_t version_buf[64] = { 0 };
+ DWORD version_buf_size = sizeof(version_buf);
+ DWORD type = 0;
+ LONG ver_rv = RegGetValueW(hkey, nullptr, L"DisplayVersion", RRF_RT_REG_SZ, &type, version_buf, &version_buf_size);
+
+ if (ver_rv != ERROR_SUCCESS)
+ {
+ RegCloseKey(hkey);
+ return false;
+ }
+
+ parse_version(version_buf, nsis_major, nsis_minor, nsis_patch, nsis_build);
+
+ // Make sure it actually exists and not a dead entry.
+ wchar_t path_buffer[MAX_PATH] = { 0 };
+ DWORD path_buf_size = sizeof(path_buffer);
+ LONG rv = RegGetValueW(hkey, nullptr, L"UninstallString", RRF_RT_REG_SZ, &type, path_buffer, &path_buf_size);
+ RegCloseKey(hkey);
+ if (rv != ERROR_SUCCESS)
+ {
+ return false;
+ }
+ size_t len = wcslen(path_buffer);
+ if (len > 0)
+ {
+ if (path_buffer[0] == L'\"')
+ {
+ // Likely to contain leading "
+ memmove(path_buffer, path_buffer + 1, len * sizeof(wchar_t));
+ }
+ wchar_t* pos = wcsstr(path_buffer, L"uninst.exe");
+ if (pos)
+ {
+ // Likely to contain trailing "
+ pos[wcslen(L"uninst.exe")] = L'\0';
+ }
+ }
+ std::error_code ec;
+ std::filesystem::path path(path_buffer);
+ if (!std::filesystem::exists(path, ec))
+ {
+ return false;
+ }
+
+ // Todo: check codesigning?
+
+ return true;
+}
+
+static void unregister_protocol_handler(const std::wstring& protocol)
+{
+ std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol;
+ 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)
+{
+ std::wstring app_name_oneword = get_app_name_oneword();
+ // Clears velopack's recently created 'uninstall' registry entry.
+ // We are going to use a custom one.
+ // Note that velopack doesn't know about our custom entry.
+ std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword;
+ RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str());
+ // Use a unique key name to avoid conflicts with any existing NSIS-based uninstall info,
+ // which can cause only one of the two entries to show up in the Add/Remove Programs list.
+ // The UI will show DisplayName, so the key name itself is not important to be user-friendly.
+ key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Vlpk" + app_name_oneword;
+ 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 exe_path = install_dir + L"\\" + get_viewer_exe_name();
+ // Update.exe lives one level above the current\ directory where the viewer exe runs
+ std::filesystem::path update_exe = std::filesystem::path(install_dir).parent_path() / L"Update.exe";
+ std::wstring uninstall_cmd = L"\"" + update_exe.wstring() + L"\" --uninstall";
+
+ RegSetValueExW(hkey, L"DisplayName", 0, REG_SZ,
+ (BYTE*)app_name.c_str(), (DWORD)((app_name.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"DisplayVersion", 0, REG_SZ,
+ (BYTE*)version.c_str(), (DWORD)((version.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"Publisher", 0, REG_SZ,
+ (BYTE*)L"Linden Research, Inc.", 44);
+ RegSetValueExW(hkey, L"UninstallString", 0, REG_SZ,
+ (BYTE*)uninstall_cmd.c_str(), (DWORD)((uninstall_cmd.size() + 1) * sizeof(wchar_t)));
+ RegSetValueExW(hkey, L"DisplayIcon", 0, REG_SZ,
+ (BYTE*)exe_path.c_str(), (DWORD)((exe_path.size() + 1) * sizeof(wchar_t)));
+
+ std::wstring link_url = L"https://support.secondlife.com/contact-support/";
+ RegSetValueExW(hkey, L"HelpLink", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ link_url = L"https://secondlife.com/whatis/";
+ RegSetValueExW(hkey, L"URLInfoAbout", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ link_url = L"https://secondlife.com/support/downloads/";
+ RegSetValueExW(hkey, L"URLUpdateInfo", 0, REG_SZ,
+ (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t)));
+
+ DWORD no_modify = 1;
+ RegSetValueExW(hkey, L"NoModify", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD));
+ RegSetValueExW(hkey, L"NoRepair", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD));
+
+ // Format YYYYMMDD
+ wchar_t dateStr[9];
+ time_t t = time(NULL);
+ struct tm tm;
+ localtime_s(&tm, &t);
+ wcsftime(dateStr, 9, L"%Y%m%d", &tm);
+ RegSetValueExW(hkey, L"InstallDate", 0, REG_SZ, (BYTE*)dateStr, (DWORD)((wcslen(dateStr) + 1) * sizeof(wchar_t))); // Let Windows fill in the install date
+
+ // 800 MB, inaccurate, but for a rough idea.
+ // We can check folder size here, but it would take time and
+ // information is of low importance.
+ DWORD estimated_size = 800000;
+ RegSetValueExW(hkey, L"EstimatedSize", 0, REG_DWORD, (BYTE*)&estimated_size, sizeof(DWORD));
+
+ RegCloseKey(hkey);
+ }
+}
+
+static void unregister_uninstall_info()
+{
+ std::wstring app_name_oneword = get_app_name_oneword();
+ std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Vlpk" + app_name_oneword;
+ RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str());
+}
+
+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;
+ std::wstring desktop_path = get_desktop_path();
+
+ CreateDirectoryW(start_menu_dir.c_str(), NULL);
+
+ create_shortcut(start_menu_dir + L"\\" + app_name + L".lnk",
+ exe_path, L"", app_name, exe_path);
+
+ create_shortcut(desktop_path + L"\\" + app_name + L".lnk",
+ exe_path, L"", app_name, exe_path);
+}
+
+static void remove_shortcuts(const std::wstring& app_name)
+{
+ std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name;
+ std::wstring desktop_path = get_desktop_path();
+
+ DeleteFileW((start_menu_dir + L"\\" + app_name + L".lnk").c_str());
+ RemoveDirectoryW(start_menu_dir.c_str());
+ DeleteFileW((desktop_path + L"\\" + app_name + L".lnk").c_str());
+}
+
+static void on_first_run(void* p_user_data, const char* app_version)
+{
+ // Velopack first executes 'after install' hook, then writes registry,
+ // then executes 'on first run' hook.
+ // As we need to clear velopack's 'uninstall' registry entry and use
+ // our own, clean it here instead of on_after_install.
+
+ std::wstring install_dir = get_install_dir();
+ std::wstring app_name = get_app_name();
+
+ int len = MultiByteToWideChar(CP_UTF8, 0, app_version, -1, NULL, 0);
+ std::wstring version(len, 0);
+ MultiByteToWideChar(CP_UTF8, 0, app_version, -1, &version[0], len);
+
+ register_uninstall_info(install_dir, app_name, version);
+}
+
+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();
+
+ register_protocol_handler(PROTOCOL_SECONDLIFE, L"URL:Second Life", exe_path);
+ register_protocol_handler(PROTOCOL_GRID_INFO, L"URL:Second Life", exe_path);
+ 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();
+
+ unregister_protocol_handler(PROTOCOL_SECONDLIFE);
+ unregister_protocol_handler(PROTOCOL_GRID_INFO);
+ 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)
+{
+ OutputDebugStringA("[Velopack] ");
+ OutputDebugStringA(level);
+ OutputDebugStringA(": ");
+ OutputDebugStringA(message);
+ OutputDebugStringA("\n");
+}
+
+#elif LL_DARWIN
+
+// macOS-specific hooks
+// TODO: Implement protocol handler registration via Launch Services
+// TODO: Implement app bundle management
+
+static void on_first_run(void* user_data, const char* app_version)
+{
+}
+
+static void on_after_install(void* user_data, const char* app_version)
+{
+ // macOS handles protocol registration via Info.plist CFBundleURLTypes
+ // No additional registration needed at runtime
+ LL_INFOS("Velopack") << "macOS post-install hook called for version: " << app_version << LL_ENDL;
+}
+
+static void on_before_uninstall(void* user_data, const char* app_version)
+{
+ LL_INFOS("Velopack") << "macOS pre-uninstall hook called for version: " << app_version << LL_ENDL;
+}
+
+static void on_log_message(void* user_data, const char* level, const char* message)
+{
+ LL_INFOS("Velopack") << "[" << level << "] " << message << LL_ENDL;
+}
+
+#endif // LL_WINDOWS / LL_DARWIN
+
+//
+// Common progress callback
+//
+
+static void on_progress(void* user_data, size_t progress)
+{
+ if (sProgressCallback)
+ {
+ sProgressCallback(static_cast(progress));
+ }
+}
+
+static void on_vpk_log(void* p_user_data,
+ const char* psz_level,
+ const char* psz_message)
+{
+ LL_DEBUGS("Velopack") << ll_safe_string(psz_message) << LL_ENDL;
+}
+
+//
+// Version comparison helper
+//
+
+// Compare running version against a VVM version string "major.minor.patch.build".
+// Returns -1 if running < vvm, 0 if equal, 1 if running > vvm.
+static int compare_running_version(const std::string& vvm_version)
+{
+ S32 major = 0, minor = 0, patch = 0;
+ U64 build = 0;
+ sscanf(vvm_version.c_str(), "%d.%d.%d.%llu", &major, &minor, &patch, &build);
+
+ const LLVersionInfo& vi = LLVersionInfo::instance();
+ S32 cur_major = vi.getMajor();
+ S32 cur_minor = vi.getMinor();
+ S32 cur_patch = vi.getPatch();
+ U64 cur_build = vi.getBuild();
+
+ if (cur_major != major) return cur_major < major ? -1 : 1;
+ if (cur_minor != minor) return cur_minor < minor ? -1 : 1;
+ if (cur_patch != patch) return cur_patch < patch ? -1 : 1;
+ if (cur_build != build) return cur_build < build ? -1 : 1;
+ return 0;
+}
+
+//
+// Update manager lifecycle
+//
+
+static void ensure_update_manager(bool allow_downgrade)
+{
+ if (sUpdateManager)
+ return;
+
+ vpkc_update_options_t options = {};
+ options.AllowVersionDowngrade = allow_downgrade;
+ options.ExplicitChannel = nullptr;
+
+ if (!sUpdateSource)
+ {
+ sUpdateSource = vpkc_new_source_custom_callback(
+ custom_get_release_feed,
+ custom_free_release_feed,
+ custom_download_asset,
+ nullptr);
+ }
+
+ vpkc_locator_config_t* locator_ptr = nullptr;
+
+#if LL_DARWIN
+ // Try auto-detection first (works when the app bundle was packaged by vpk
+ // and has UpdateMac + sq.version already present)
+ if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, nullptr, &sUpdateManager))
+ {
+ char err[512];
+ vpkc_get_last_error(err, sizeof(err));
+ LL_INFOS("Velopack") << "Auto-detect failed (" << ll_safe_string(err)
+ << "), falling back to explicit locator" << LL_ENDL;
+
+ // 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 = {};
+
+ // The executable lives at /Contents/MacOS/
+ // The app bundle root is two levels up from the executable directory.
+ std::string exe_dir = gDirUtilp->getExecutableDir();
+ std::string bundle_root = exe_dir + "/../..";
+ char resolved[PATH_MAX];
+ if (realpath(bundle_root.c_str(), resolved))
+ {
+ bundle_root = resolved;
+ }
+
+ // Construct a version string in Velopack SemVer format: major.minor.patch-build
+ const LLVersionInfo& vi = LLVersionInfo::instance();
+ std::string current_version = llformat("%d.%d.%d-%llu",
+ vi.getMajor(), vi.getMinor(), vi.getPatch(), vi.getBuild());
+
+ // Create a minimal sq.version manifest so Velopack knows our version.
+ // Proper vpk-packaged builds have this in the bundle already.
+ std::string manifest_path = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, "sq.version");
+ {
+ std::string app_name = LLVersionInfo::instance().getChannel();
+ std::string pack_id = app_name;
+ pack_id.erase(std::remove(pack_id.begin(), pack_id.end(), ' '), pack_id.end());
+
+ std::string nuspec = "\n"
+ "\n"
+ " \n"
+ " " + pack_id + "\n"
+ " " + current_version + "\n"
+ " " + app_name + "\n"
+ " \n"
+ "\n";
+
+ llofstream manifest_file(manifest_path, std::ios::trunc);
+ if (manifest_file.is_open())
+ {
+ manifest_file << nuspec;
+ manifest_file.close();
+ }
+ }
+
+ std::string packages_dir = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, "velopack-packages");
+ LLFile::mkdir(packages_dir);
+
+ locator.RootAppDir = const_cast(bundle_root.c_str());
+ locator.CurrentBinaryDir = const_cast(exe_dir.c_str());
+ locator.ManifestPath = const_cast(manifest_path.c_str());
+ locator.PackagesDir = const_cast(packages_dir.c_str());
+ locator.UpdateExePath = nullptr;
+ locator.IsPortable = false;
+
+ locator_ptr = &locator;
+
+ LL_INFOS("Velopack") << "Explicit locator: RootAppDir=" << bundle_root
+ << " CurrentBinaryDir=" << exe_dir
+ << " Version=" << current_version << LL_ENDL;
+
+ if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, locator_ptr, &sUpdateManager))
+ {
+ char err2[512];
+ vpkc_get_last_error(err2, sizeof(err2));
+ LL_WARNS("Velopack") << "Failed to create update manager: " << ll_safe_string(err2) << LL_ENDL;
+ }
+ }
+ return;
+#endif
+
+ // Windows: Velopack auto-detection works because the viewer is installed
+ // by Velopack's Setup.exe which creates the proper install structure.
+ if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, nullptr, &sUpdateManager))
+ {
+ char err[512];
+ vpkc_get_last_error(err, sizeof(err));
+ LL_WARNS("Velopack") << "Failed to create update manager: " << ll_safe_string(err) << LL_ENDL;
+ }
+}
+
+//
+// Public API - Cross-platform
+//
+
+bool velopack_initialize()
+{
+ vpkc_set_logger(on_log_message, nullptr);
+ vpkc_app_set_auto_apply_on_startup(false);
+
+#if LL_WINDOWS || LL_DARWIN
+ vpkc_app_set_hook_first_run(on_first_run);
+ vpkc_app_set_hook_after_install(on_after_install);
+ vpkc_app_set_hook_before_uninstall(on_before_uninstall);
+#endif
+
+ vpkc_app_run(nullptr);
+ return true;
+}
+
+// Downloads the update that was found during the check phase.
+// Operates on sPendingCheckInfo which was set by velopack_check_for_updates.
+static void velopack_download_pending_update()
+{
+ if (!sUpdateManager || !sPendingCheckInfo)
+ {
+ LL_WARNS("Velopack") << "No pending check info to download" << LL_ENDL;
+ return;
+ }
+
+ LL_DEBUGS("Velopack") << "Setting up detailed logging";
+ vpkc_set_logger(on_vpk_log, nullptr);
+ LL_CONT << LL_ENDL;
+ LL_INFOS("Velopack") << "Downloading update..." << LL_ENDL;
+
+ // Pre-download the nupkg at the coroutine level where getRawAndSuspend works.
+ // The download callback inside the Rust FFI cannot use coroutine HTTP.
+ std::string asset_filename = sPendingCheckInfo->TargetFullRelease->FileName
+ ? sPendingCheckInfo->TargetFullRelease->FileName : "";
+ std::string asset_url;
+ auto url_it = sAssetUrlMap.find(asset_filename);
+ if (url_it != sAssetUrlMap.end())
+ {
+ asset_url = url_it->second;
+ }
+ else
+ {
+ std::string base = sUpdateUrl;
+ if (!base.empty() && base.back() == '/')
+ base.pop_back();
+ asset_url = base + "/" + asset_filename;
+ }
+
+ sPreDownloadedAssetPath = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, asset_filename);
+ LL_INFOS("Velopack") << "Pre-downloading " << asset_url
+ << " to " << sPreDownloadedAssetPath << LL_ENDL;
+
+ if (!download_url_to_file(asset_url, sPreDownloadedAssetPath))
+ {
+ LL_WARNS("Velopack") << "Failed to pre-download update asset" << LL_ENDL;
+ sPreDownloadedAssetPath.clear();
+ return;
+ }
+
+ LL_INFOS("Velopack") << "Pre-download complete, handing to Velopack" << LL_ENDL;
+ if (vpkc_download_updates(sUpdateManager, sPendingCheckInfo, on_progress, nullptr))
+ {
+ if (sPendingUpdate)
+ {
+ vpkc_free_update_info(sPendingUpdate);
+ }
+ sPendingUpdate = sPendingCheckInfo;
+ sPendingCheckInfo = nullptr; // Ownership transferred
+ LL_INFOS("Velopack") << "Update downloaded and pending" << LL_ENDL;
+ }
+ else
+ {
+ char descr[512];
+ vpkc_get_last_error(descr, sizeof(descr));
+ LL_WARNS("Velopack") << "Failed to download update: " << ll_safe_string((const char*)descr) << LL_ENDL;
+ }
+}
+
+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
+ show_downloading_notification(sTargetVersion);
+ }
+}
+
+static void show_downloading_notification(const std::string& version)
+{
+ LLSD args;
+ args["VERSION"] = version;
+ sDownloadingNotification = LLNotificationsUtil::add("DownloadingUpdate", args, LLSD(), on_downloading_closed);
+}
+
+static void dismiss_downloading_notification()
+{
+ if (sDownloadingNotification)
+ {
+ LLNotificationsUtil::cancel(sDownloadingNotification);
+ sDownloadingNotification = nullptr;
+ }
+}
+
+static void on_required_update_response(const LLSD& notification, const LLSD& response)
+{
+ LL_INFOS("Velopack") << "Required update acknowledged, starting download" << LL_ENDL;
+ show_downloading_notification(sTargetVersion);
+ LLCoros::instance().launch("VelopackRequiredUpdate", []()
+ {
+ velopack_download_pending_update();
+ dismiss_downloading_notification();
+ if (velopack_is_update_pending())
+ {
+ LL_INFOS("Velopack") << "Required update downloaded, quitting to apply" << LL_ENDL;
+ velopack_request_restart_after_update();
+ LLAppViewer::instance()->requestQuit();
+ }
+ });
+}
+
+static void on_optional_update_response(const LLSD& notification, const LLSD& response)
+{
+ S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
+ if (option == 0) // "Install"
+ {
+ LL_INFOS("Velopack") << "User accepted optional update, starting download" << LL_ENDL;
+ show_downloading_notification(sTargetVersion);
+ LLCoros::instance().launch("VelopackOptionalUpdate", []()
+ {
+ velopack_download_pending_update();
+ dismiss_downloading_notification();
+ if (velopack_is_update_pending())
+ {
+ LL_INFOS("Velopack") << "Optional update downloaded, quitting to apply" << LL_ENDL;
+ velopack_request_restart_after_update();
+ LLAppViewer::instance()->requestQuit();
+ }
+ });
+ }
+ else
+ {
+ LL_INFOS("Velopack") << "User declined optional update (option=" << option << ")" << LL_ENDL;
+ // Free the check info since user declined
+ if (sPendingCheckInfo)
+ {
+ vpkc_free_update_info(sPendingCheckInfo);
+ sPendingCheckInfo = nullptr;
+ }
+ }
+}
+
+static void show_required_update_prompt()
+{
+ LLSD args;
+ args["VERSION"] = sTargetVersion;
+ args["URL"] = sReleaseNotesUrl;
+ LLNotificationsUtil::add("PauseForUpdate", args, LLSD(), on_required_update_response);
+}
+
+void velopack_check_for_updates(const std::string& required_version, const std::string& relnotes_url)
+{
+ if (sUpdateUrl.empty())
+ {
+ LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL;
+ return;
+ }
+
+ // Allow downgrades only for rollbacks: VVM requires a version that's
+ // 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
+ ensure_update_manager(allow_downgrade);
+ if (!sUpdateManager)
+ return;
+
+ // 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);
+
+ if (result != UPDATE_AVAILABLE || !update_info)
+ {
+ LL_INFOS("Velopack") << "No update available from feed (result=" << result << ")" << LL_ENDL;
+ return;
+ }
+
+ // Extract the actual target version from Velopack's feed
+ std::string target_version = update_info->TargetFullRelease->Version
+ ? update_info->TargetFullRelease->Version : "";
+ LL_INFOS("Velopack") << "Update available: " << target_version
+ << " (required_version=" << required_version << ")" << LL_ENDL;
+
+ // Store state for the prompt/download phase
+ sReleaseNotesUrl = relnotes_url;
+ sTargetVersion = target_version;
+ if (sPendingCheckInfo)
+ {
+ vpkc_free_update_info(sPendingCheckInfo);
+ }
+ 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
+ sIsRequired = is_required;
+
+ if (is_required)
+ {
+ LL_INFOS("Velopack") << "Required update (running below " << required_version
+ << "), prompting user for " << target_version << LL_ENDL;
+ show_required_update_prompt();
+ return;
+ }
+
+ // Optional update - check user preference
+ U32 updater_setting = gSavedSettings.getU32("UpdaterServiceSetting");
+
+ if (updater_setting == 3)
+ {
+ // "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();
+ return;
+ }
+
+ // Default / value 1: "Ask me when an optional update is ready to install"
+ LL_INFOS("Velopack") << "Optional update available (" << target_version << "), prompting user" << LL_ENDL;
+ LLSD args;
+ args["VERSION"] = target_version;
+ args["URL"] = relnotes_url;
+ LLNotificationsUtil::add("PromptOptionalUpdate", args, LLSD(), on_optional_update_response);
+}
+
+std::string velopack_get_current_version()
+{
+ if (!sUpdateManager)
+ {
+ return "";
+ }
+
+ char version[64];
+ size_t len = vpkc_get_current_version(sUpdateManager, version, sizeof(version));
+ if (len > 0)
+ {
+ return std::string(version, len);
+ }
+ return "";
+}
+
+bool velopack_is_update_pending()
+{
+ return sPendingUpdate != nullptr;
+}
+
+bool velopack_is_required_update_in_progress()
+{
+ return sIsRequired && sPendingCheckInfo != nullptr;
+}
+
+std::string velopack_get_required_update_version()
+{
+ return sTargetVersion;
+}
+
+bool velopack_should_restart_after_update()
+{
+ return sRestartAfterUpdate;
+}
+
+void velopack_request_restart_after_update()
+{
+ sRestartAfterUpdate = true;
+}
+
+void velopack_apply_pending_update(bool restart)
+{
+ if (!sUpdateManager || !sPendingUpdate || !sPendingUpdate->TargetFullRelease)
+ {
+ LL_WARNS("Velopack") << "Cannot apply update: no pending update or manager" << LL_ENDL;
+ return;
+ }
+
+ LL_INFOS("Velopack") << "Applying pending update (restart=" << restart << ")" << LL_ENDL;
+ vpkc_wait_exit_then_apply_updates(sUpdateManager,
+ sPendingUpdate->TargetFullRelease,
+ false,
+ restart,
+ nullptr, 0);
+}
+
+void velopack_cleanup()
+{
+ if (sUpdateManager)
+ {
+ vpkc_free_update_manager(sUpdateManager);
+ sUpdateManager = nullptr;
+ }
+ if (sUpdateSource)
+ {
+ vpkc_free_source(sUpdateSource);
+ sUpdateSource = nullptr;
+ }
+ if (sPendingUpdate)
+ {
+ vpkc_free_update_info(sPendingUpdate);
+ sPendingUpdate = nullptr;
+ }
+ if (sPendingCheckInfo)
+ {
+ vpkc_free_update_info(sPendingCheckInfo);
+ sPendingCheckInfo = nullptr;
+ }
+ sAssetUrlMap.clear();
+}
+
+void velopack_set_update_url(const std::string& url)
+{
+ sUpdateUrl = url;
+ LL_INFOS("Velopack") << "Update URL set to: " << url << LL_ENDL;
+}
+
+void velopack_set_progress_callback(std::function callback)
+{
+ sProgressCallback = callback;
+}
+
+#endif // LL_VELOPACK
diff --git a/indra/newview/llvelopack.h b/indra/newview/llvelopack.h
new file mode 100644
index 00000000000..d04d0db7dce
--- /dev/null
+++ b/indra/newview/llvelopack.h
@@ -0,0 +1,59 @@
+/**
+ * @file llvelopack.h
+ * @brief Velopack installer and update framework integration
+ *
+ * $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$
+ */
+
+#ifndef LL_LLVELOPACK_H
+#define LL_LLVELOPACK_H
+
+#if LL_VELOPACK
+
+#include
+#include
+
+bool velopack_initialize();
+void velopack_check_for_updates(const std::string& required_version, const std::string& relnotes_url);
+std::string velopack_get_current_version();
+bool velopack_is_update_pending();
+bool velopack_is_required_update_in_progress();
+std::string velopack_get_required_update_version();
+bool velopack_should_restart_after_update();
+void velopack_request_restart_after_update();
+void velopack_apply_pending_update(bool restart = true);
+void velopack_set_update_url(const std::string& url);
+void velopack_set_progress_callback(std::function callback);
+void velopack_cleanup();
+
+#if LL_WINDOWS
+void clear_nsis_links();
+bool get_nsis_version(
+ int& nsis_major,
+ int& nsis_minor,
+ int& nsis_patch,
+ uint64_t& nsis_build);
+#endif
+
+#endif // LL_VELOPACK
+#endif
+// EOF
diff --git a/indra/newview/llversioninfo.cpp b/indra/newview/llversioninfo.cpp
index 4e8320b72a4..178f10257d7 100644
--- a/indra/newview/llversioninfo.cpp
+++ b/indra/newview/llversioninfo.cpp
@@ -54,7 +54,7 @@ LLVersionInfo::LLVersionInfo():
mWorkingChannelName(LL_TO_STRING(LL_VIEWER_CHANNEL)),
build_configuration(LLBUILD_CONFIG), // set in indra/cmake/BuildVersion.cmake
// instantiate an LLEventMailDrop with canonical name to listen for news
- // from SLVersionChecker
+ // from the Viewer Version Manager
mPump{new LLEventMailDrop("relnotes")},
// immediately listen on mPump, store arriving URL into mReleaseNotes
mStore{new LLStoreListener(*mPump, mReleaseNotes)}
diff --git a/indra/newview/llversioninfo.h b/indra/newview/llversioninfo.h
index 237b37a084c..a2b93597e66 100644
--- a/indra/newview/llversioninfo.h
+++ b/indra/newview/llversioninfo.h
@@ -112,8 +112,8 @@ class LLVersionInfo: public LLSingleton
std::string mReleaseNotes;
// Store unique_ptrs to the next couple things so we don't have to explain
// to every consumer of this header file all the details of each.
- // mPump is the LLEventMailDrop on which we listen for SLVersionChecker to
- // post the release-notes URL from the Viewer Version Manager.
+ // mPump is the LLEventMailDrop on which we listen for the
+ // release-notes URL from the Viewer Version Manager.
std::unique_ptr mPump;
// mStore is an adapter that stores the release-notes URL in mReleaseNotes.
std::unique_ptr> mStore;
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 82fc4c6d87f..eef31c70487 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -85,7 +85,6 @@
#include "llfloatergroups.h"
#include "llfloaterhelpbrowser.h"
#include "llfloaterhoverheight.h"
-#include "llfloaterhowto.h"
#include "llfloaterhud.h"
#include "llfloaterimagepreview.h"
#include "llfloaterimsession.h"
@@ -503,7 +502,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/llviewergenericmessage.cpp b/indra/newview/llviewergenericmessage.cpp
index fd894a59976..f3a0f026b8f 100644
--- a/indra/newview/llviewergenericmessage.cpp
+++ b/indra/newview/llviewergenericmessage.cpp
@@ -73,6 +73,7 @@ void send_generic_message(const std::string& method,
void process_generic_message(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID agent_id;
msg->getUUID("AgentData", "AgentID", agent_id);
if (agent_id != gAgent.getID())
@@ -95,6 +96,7 @@ void process_generic_message(LLMessageSystem* msg, void**)
void process_generic_streaming_message(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLGenericStreamingMessage data;
data.unpack(msg);
switch (data.mMethod)
@@ -110,6 +112,7 @@ void process_generic_streaming_message(LLMessageSystem* msg, void**)
void process_large_generic_message(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID agent_id;
msg->getUUID("AgentData", "AgentID", agent_id);
if (agent_id != gAgent.getID())
diff --git a/indra/newview/llviewerjointattachment.cpp b/indra/newview/llviewerjointattachment.cpp
index 511fac9788f..cad96afd98c 100644
--- a/indra/newview/llviewerjointattachment.cpp
+++ b/indra/newview/llviewerjointattachment.cpp
@@ -168,7 +168,6 @@ void LLViewerJointAttachment::setupDrawable(LLViewerObject *object)
//-----------------------------------------------------------------------------
bool LLViewerJointAttachment::addObject(LLViewerObject* object)
{
- object->extractAttachmentItemID();
// Same object reattached
if (isObjectAttached(object))
@@ -179,6 +178,8 @@ bool LLViewerJointAttachment::addObject(LLViewerObject* object)
// re-connect object to the joint correctly
}
+ object->extractAttachmentItemID();
+
// Two instances of the same inventory item attached --
// Request detach, and kill the object in the meantime.
if (getAttachedObject(object->getAttachmentItemID()))
diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp
index a77b9f6103f..be4961e3c44 100644
--- a/indra/newview/llviewermedia.cpp
+++ b/indra/newview/llviewermedia.cpp
@@ -1758,6 +1758,7 @@ void LLViewerMediaImpl::createMediaSource()
//////////////////////////////////////////////////////////////////////////////////////////
void LLViewerMediaImpl::destroyMediaSource()
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA;
mNeedsNewTexture = true;
// Tell the viewer media texture it's no longer active
@@ -2628,19 +2629,30 @@ void LLViewerMediaImpl::navigateTo(const std::string& url, const std::string& mi
}
//////////////////////////////////////////////////////////////////////////////////////////
-void LLViewerMediaImpl::navigateInternal()
+void LLViewerMediaImpl::navigateInternal(bool should_log)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA;
// Helpful to have media urls in log file. Shouldn't be spammy.
{
// Do not log the query parts
LLURI u(mMediaURL);
std::string sanitized_url = (u.query().empty() ? mMediaURL : u.scheme() + "://" + u.authority() + u.path());
- LL_INFOS() << "media id= " << mTextureId << " url=" << sanitized_url << ", mime_type=" << mMimeType << LL_ENDL;
+ if (should_log)
+ {
+ LL_INFOS("Media") << "media id= " << mTextureId << " url=" << sanitized_url << ", mime_type=" << mMimeType << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("Media") << "media id= " << mTextureId << " url=" << sanitized_url << ", mime_type=" << mMimeType << LL_ENDL;
+ }
}
if(mNavigateSuspended)
{
- LL_WARNS() << "Deferring navigate." << LL_ENDL;
+ if (should_log || !mNavigateSuspendedDeferred)
+ {
+ LL_WARNS() << "Deferring navigate." << LL_ENDL;
+ }
mNavigateSuspendedDeferred = true;
return;
}
@@ -2648,7 +2660,13 @@ void LLViewerMediaImpl::navigateInternal()
if (!mMimeProbe.expired())
{
- LL_WARNS() << "MIME type probe already in progress -- bailing out." << LL_ENDL;
+ if (should_log)
+ {
+ // media periodically suspends and unsuspends (should_log == false),
+ // unsuspend calls this function, it's epxected that sometimes
+ // unsuspend will be attempted while a probe is in flight.
+ LL_WARNS() << "MIME type probe already in progress -- bailing out." << LL_ENDL;
+ }
return;
}
@@ -2709,10 +2727,14 @@ void LLViewerMediaImpl::navigateInternal()
{
loadURI();
}
- else
+ else if (should_log)
{
LL_WARNS("Media") << "Couldn't navigate to: " << mMediaURL << " as there is no media type for: " << mMimeType << LL_ENDL;
}
+ else
+ {
+ LL_DEBUGS("Media") << "Couldn't navigate to: " << mMediaURL << " as there is no media type for: " << mMimeType << LL_ENDL;
+ }
}
void LLViewerMediaImpl::mimeDiscoveryCoro(std::string url)
@@ -3978,7 +4000,7 @@ void LLViewerMediaImpl::setNavigateSuspended(bool suspend)
if(mNavigateSuspendedDeferred)
{
mNavigateSuspendedDeferred = false;
- navigateInternal();
+ navigateInternal(false /*suspend happens periodically, don't log*/);
}
}
}
diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h
index 1fc5bbc9e0f..c840fbb72c9 100644
--- a/indra/newview/llviewermedia.h
+++ b/indra/newview/llviewermedia.h
@@ -251,7 +251,7 @@ class LLViewerMediaImpl
void navigateHome();
void unload();
void navigateTo(const std::string& url, const std::string& mime_type = "", bool rediscover_type = false, bool server_request = false, bool clean_browser = false);
- void navigateInternal();
+ void navigateInternal(bool should_log = true);
void navigateStop();
bool handleKeyHere(KEY key, MASK mask);
bool handleKeyUpHere(KEY key, MASK mask);
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index dbcf4fbbf41..8d6b8ec7139 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -8504,15 +8504,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)
@@ -9964,10 +9955,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 5d8bd452186..812ba765511 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -3554,6 +3554,7 @@ extern U32Bits gObjectData;
void process_object_update(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
// Update the data counters
if (mesgsys->getReceiveCompressedSize())
{
@@ -3575,6 +3576,7 @@ void process_object_update(LLMessageSystem *mesgsys, void **user_data)
void process_compressed_object_update(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
// Update the data counters
if (mesgsys->getReceiveCompressedSize())
{
@@ -3596,6 +3598,7 @@ void process_compressed_object_update(LLMessageSystem *mesgsys, void **user_data
void process_cached_object_update(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
// Update the data counters
if (mesgsys->getReceiveCompressedSize())
{
@@ -3613,6 +3616,7 @@ void process_cached_object_update(LLMessageSystem *mesgsys, void **user_data)
void process_terse_object_update_improved(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
if (mesgsys->getReceiveCompressedSize())
{
gObjectData += (U32Bytes)mesgsys->getReceiveCompressedSize();
@@ -3972,6 +3976,7 @@ void process_sim_stats(LLMessageSystem *msg, void **user_data)
void process_avatar_animation(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID animation_id;
LLUUID uuid;
S32 anim_sequence_id;
@@ -4083,6 +4088,7 @@ void process_avatar_animation(LLMessageSystem *mesgsys, void **user_data)
void process_object_animation(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID animation_id;
LLUUID uuid;
S32 anim_sequence_id;
@@ -4148,6 +4154,7 @@ void process_object_animation(LLMessageSystem *mesgsys, void **user_data)
void process_avatar_appearance(LLMessageSystem *mesgsys, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID uuid;
mesgsys->getUUIDFast(_PREHASH_Sender, _PREHASH_ID, uuid);
@@ -5679,6 +5686,7 @@ void process_script_experience_details(const LLSD& experience_details, LLSD args
void process_script_question(LLMessageSystem *msg, void **user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
// *TODO: Translate owner name -> [FIRST] [LAST]
LLHost sender = msg->getSender();
diff --git a/indra/newview/llviewernetwork.cpp b/indra/newview/llviewernetwork.cpp
index 890580ddff2..6cb3aee20cc 100644
--- a/indra/newview/llviewernetwork.cpp
+++ b/indra/newview/llviewernetwork.cpp
@@ -575,6 +575,7 @@ std::string LLGridManager::getGridLoginID()
std::string LLGridManager::getUpdateServiceURL()
{
+ auto env_update_service = LLStringUtil::getoptenv("SL_UPDATE_SERVICE");
std::string update_url_base = gSavedSettings.getString("CmdLineUpdateService");;
if ( !update_url_base.empty() )
{
@@ -582,6 +583,13 @@ std::string LLGridManager::getUpdateServiceURL()
<< "Update URL base overridden from command line: " << update_url_base
<< LL_ENDL;
}
+ else if (env_update_service && env_update_service->find("http") != std::string::npos)
+ {
+ update_url_base = *env_update_service;
+ LL_INFOS("UpdaterService", "GridManager")
+ << "Update URL base overridden from SL_UPDATE_SERVICE environment variable: " << update_url_base
+ << LL_ENDL;
+ }
else if ( mGridList[mGrid].has(GRID_UPDATE_SERVICE_URL) )
{
update_url_base = mGridList[mGrid][GRID_UPDATE_SERVICE_URL].asString();
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index db1ef54ffaf..7c26cb3c9fa 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -1529,7 +1529,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys,
U16 param_type;
S32 param_size;
dp.unpackU16(param_type, "param_type");
- dp.unpackBinaryData(param_block, param_size, "param_data");
+ dp.unpackBinaryData(param_block, MAX_OBJECT_PARAMS_SIZE, param_size, "param_data");
//LL_INFOS() << "Param type: " << param_type << ", Size: " << param_size << LL_ENDL;
LLDataPackerBinaryBuffer dp2(param_block, param_size);
unpackParameterEntry(param_type, &dp2);
@@ -1786,7 +1786,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys,
dp->unpackU32(size, "ScratchPadSize");
delete [] mData;
mData = new U8[size];
- dp->unpackBinaryData((U8 *)mData, sp_size, "PartData");
+ dp->unpackBinaryData((U8 *)mData, size, sp_size, "PartData");
}
else
{
@@ -1859,7 +1859,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys,
U16 param_type;
S32 param_size;
dp->unpackU16(param_type, "param_type");
- dp->unpackBinaryData(param_block, param_size, "param_data");
+ dp->unpackBinaryData(param_block, MAX_OBJECT_PARAMS_SIZE, param_size, "param_data");
//LL_INFOS() << "Param type: " << param_type << ", Size: " << param_size << LL_ENDL;
LLDataPackerBinaryBuffer dp2(param_block, param_size);
unpackParameterEntry(param_type, &dp2);
@@ -3210,6 +3210,7 @@ S32 LLFilenameAndTask::sCount = 0;
// static
void LLViewerObject::processTaskInv(LLMessageSystem* msg, void** user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLUUID task_id;
msg->getUUIDFast(_PREHASH_InventoryData, _PREHASH_TaskID, task_id);
LLViewerObject* object = gObjectList.findObject(task_id);
diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp
index d39d4662053..5193514fe8a 100644
--- a/indra/newview/llviewerstats.cpp
+++ b/indra/newview/llviewerstats.cpp
@@ -783,7 +783,11 @@ void send_viewer_stats(bool include_preferences)
fail["failed_resends"] = (S32) gMessageSystem->mFailedResendPackets;
fail["off_circuit"] = (S32) gMessageSystem->mOffCircuitPackets;
fail["invalid"] = (S32) gMessageSystem->mInvalidOnCircuitPackets;
- fail["missing_updater"] = (S32) LLAppViewer::instance()->isUpdaterMissing();
+#if LL_VELOPACK
+ fail["missing_updater"] = false;
+#else
+ fail["missing_updater"] = true;
+#endif
LLSD &inventory = body["inventory"];
inventory["usable"] = gInventory.isInventoryUsable();
diff --git a/indra/newview/llviewertexteditor.cpp b/indra/newview/llviewertexteditor.cpp
index 210cd62d6f3..95e34b4df16 100644
--- a/indra/newview/llviewertexteditor.cpp
+++ b/indra/newview/llviewertexteditor.cpp
@@ -189,7 +189,12 @@ class LLEmbeddedItemSegment : public LLTextSegment
return new LLEmbeddedItemSegment(mStart, mImage, mItem, *editor);
}
- /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
+ /*virtual*/ bool getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height)
+ {
+ return getSegmentDimensionsF32(first_char, num_chars, width, height);
+ }
+
+ inline bool getSegmentDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const
{
if (num_chars == 0)
{
@@ -213,8 +218,9 @@ class LLEmbeddedItemSegment : public LLTextSegment
}
else
{
- S32 width, height;
- getDimensions(mStart, 1, width, height);
+ F32 width;
+ S32 height;
+ getSegmentDimensionsF32(mStart, 1, width, height);
if (width > num_pixels)
{
return 0;
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 96962bbeaeb..e4fd9478921 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -1096,7 +1096,17 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time)
while (!mCreateTextureList.empty())
{
- LLViewerFetchedTexture* imagep = mCreateTextureList.front();
+ // Hold a smart pointer to keep the texture alive throughout processing,
+ // even if side effects (e.g. pipeline rebuilds, GL operations) indirectly
+ // cause other references to be released. (see: #5426)
+ LLPointer imagep = mCreateTextureList.front();
+ mCreateTextureList.pop();
+
+ if (!imagep)
+ {
+ continue;
+ }
+
llassert(imagep->mCreatePending);
// desired discard may change while an image is being decoded. If the texture in VRAM is sufficient
@@ -1122,8 +1132,6 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time)
imagep->scaleDown();
}
- mCreateTextureList.pop();
-
if (create_timer.getElapsedTimeF32() > max_time)
{
break;
@@ -1192,8 +1200,18 @@ F32 LLViewerTextureList::updateImagesLoadingFastCache(F32 max_time)
LLTimer timer;
image_list_t::iterator enditer = mFastCacheList.begin();
{
- // prelock fast cache mutex to avoid waiting multiple times.
- LLMutexLock cache_lock(LLAppViewer::getTextureCache()->getFastCacheMutex());
+ // Prelock fast cache mutex to avoid waiting multiple times.
+ LLMutexTrylock fast_cache_lock(LLAppViewer::getTextureCache()->getFastCacheMutex());
+ if (!fast_cache_lock.isLocked())
+ {
+ // Cache is busy, skip this update cycle to avoid blocking the main thread.
+ //
+ // Generally fast cache operations are brief and rare in comparison to writing
+ // main texture body, but if disk is busy, it can get stuck for multiple
+ // seconds, waiting for that long is not practical.
+ // But some variant of a timed try lock for 0.1ms or less might be optimal.
+ return 0.0f;
+ }
for (image_list_t::iterator iter = mFastCacheList.begin();
iter != mFastCacheList.end();)
{
diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp
index efb09479e20..2f39a76156c 100644
--- a/indra/newview/llvoavatar.cpp
+++ b/indra/newview/llvoavatar.cpp
@@ -5984,6 +5984,7 @@ const LLUUID& LLVOAvatar::getStepSound() const
//-----------------------------------------------------------------------------
void LLVOAvatar::processAnimationStateChanges()
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
if ( isAnyAnimationSignaled(AGENT_WALK_ANIMS, NUM_AGENT_WALK_ANIMS) )
{
startMotion(ANIM_AGENT_WALK_ADJUST);
@@ -7566,7 +7567,8 @@ const LLViewerJointAttachment *LLVOAvatar::attachObject(LLViewerObject *viewer_o
updateAttachmentOverrides();
}
- updateVisualComplexity();
+ // Inform complexity logic to do partial update.
+ markAttachmentComplexityDirty(viewer_object->getID());
if (viewer_object->isSelected())
{
@@ -7870,7 +7872,7 @@ bool LLVOAvatar::detachObject(LLViewerObject *viewer_object)
if (attachment->isObjectAttached(viewer_object))
{
- updateVisualComplexity();
+ markAttachmentComplexityDirty(viewer_object->getID(), true);
bool is_animated_object = viewer_object->isAnimatedObject();
cleanupAttachedMesh(viewer_object);
@@ -8297,6 +8299,7 @@ void LLVOAvatar::updateRezzedStatusTimers(S32 rez_status)
selfStopPhase("wear_inventory_category", false);
selfStopPhase("process_initial_wearables_update", false);
+ // Start a complexity update.
updateVisualComplexity();
}
}
@@ -8588,7 +8591,7 @@ bool LLVOAvatar::processFullyLoadedChange(bool loading)
bool LLVOAvatar::isFullyLoaded() const
{
- return (mRenderUnloadedAvatar || mFullyLoaded);
+ return (mRenderUnloadedAvatar && !isSelf()) || mFullyLoaded;
}
bool LLVOAvatar::hasFirstFullAttachmentData() const
@@ -9230,6 +9233,7 @@ void LLVOAvatar::releaseComponentTextures()
{
// Regression case of messaging system. Expected 21 textures, received 20. last texture is not valid so set to default
setTETexture(TEX_HAIR_BAKED, IMG_DEFAULT_AVATAR);
+ markBodyPartsComplexityDirty();
}
}
@@ -9247,6 +9251,7 @@ void LLVOAvatar::releaseComponentTextures()
{
const U8 te = (ETextureIndex)bakedDicEntry->mLocalTextures[texture];
setTETexture(te, IMG_DEFAULT_AVATAR);
+ markBodyPartsComplexityDirty();
}
}
}
@@ -10132,6 +10137,7 @@ void LLVOAvatar::onBakedTextureMasksLoaded( bool success, LLViewerFetchedTexture
LL_INFOS() << "unexpected image id: " << id << LL_ENDL;
}
self->dirtyMesh();
+ self->markBodyPartsComplexityDirty();
}
else
{
@@ -10186,6 +10192,7 @@ void LLVOAvatar::onBakedTextureLoaded(bool success,
if (selfp && !success)
{
selfp->removeMissingBakedTextures();
+ selfp->markBodyPartsComplexityDirty();
}
if( final || !success )
@@ -10196,6 +10203,7 @@ void LLVOAvatar::onBakedTextureLoaded(bool success,
if( selfp && success && final )
{
selfp->useBakedTexture( id );
+ selfp->markBodyPartsComplexityDirty();
}
}
@@ -11146,10 +11154,384 @@ void LLVOAvatar::idleUpdateDebugInfo()
void LLVOAvatar::updateVisualComplexity()
{
LL_DEBUGS("AvatarRender") << "avatar " << getID() << " appearance changed" << LL_ENDL;
- // Set the cache time to in the past so it's updated ASAP
+ // Trigger cache recalculation on next idle update.
+ // Will recalculate stale, missing data and control avatar.
mVisualComplexityStale = true;
}
+void LLVOAvatar::calculateAttachmentComplexity(LLViewerObject* attached_object,
+ const F32 max_attachment_complexity,
+ ComplexityComponent& cache)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
+
+ cache.reset();
+
+ if (!attached_object
+ || attached_object->isDead())
+ {
+ return;
+ }
+
+ accountRenderComplexityForObject(
+ attached_object,
+ max_attachment_complexity,
+ cache.textures,
+ cache.render_cost,
+ cache.triangle_count,
+ cache.est_triangle_count,
+ cache.surface_area,
+ cache.hud_complexity,
+ cache.object_complexity
+ );
+
+ cache.last_update_time = LLFrameTimer::getTotalSeconds();
+ cache.needs_update = false;
+}
+
+void LLVOAvatar::calculateBodyPartsComplexity(ComplexityComponent& cache)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
+
+ cache.reset();
+
+ // Body parts have a fixed cost
+ // This represents the base avatar mesh (eyes, hair, shape, skin, etc.)
+ cache.render_cost = calculateBodyPartsComplexity();
+
+ // For more accurate body part complexity, could enumerate mesh LODs here
+ // For now, using a constant cost as in the original implementation
+
+ cache.last_update_time = LLFrameTimer::getTotalSeconds();
+ cache.needs_update = false;
+}
+
+bool LLVOAvatar::shouldUpdateComplexityComponent(const ComplexityComponent& component) const
+{
+ if (component.needs_update)
+ {
+ return true;
+ }
+
+ constexpr F32 CACHE_LIFETIME_SECONDS = 30.0; // Todo: should be indefinite, until something actually changes
+ F64 current_time = LLFrameTimer::getTotalSeconds();
+ return (current_time - component.last_update_time) > CACHE_LIFETIME_SECONDS;
+}
+
+bool LLVOAvatar::calculateControlAvatarComplexity(ComplexityComponent& cache, const F32 max_attachment_complexity)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
+
+ cache.reset();
+
+ if (!isControlAvatar())
+ {
+ return false;
+ }
+
+ LLControlAvatar* control_av = dynamic_cast(this);
+ if (!control_av)
+ {
+ return false;
+ }
+
+ LLVOVolume* volp = control_av->mRootVolp;
+ if (!volp || volp->isAttachment())
+ {
+ return false;
+ }
+
+ accountRenderComplexityForObject(
+ volp,
+ max_attachment_complexity,
+ cache.textures,
+ cache.render_cost,
+ cache.triangle_count,
+ cache.est_triangle_count,
+ cache.surface_area,
+ cache.hud_complexity,
+ cache.object_complexity
+ );
+
+ // todo: store 'expires' time instead or make it indefinite?
+ cache.last_update_time = LLFrameTimer::getTotalSeconds();
+ cache.needs_update = false;
+
+ return true;
+}
+
+void LLVOAvatar::accumulateComplexityComponent(const ComplexityComponent& component,
+ U32& total_cost,
+ hud_complexity_list_t& hud_list,
+ object_complexity_list_t& object_list)
+{
+ total_cost += component.render_cost;
+ mAttachmentSurfaceArea += component.surface_area;
+ mAttachmentVisibleTriangleCount += component.triangle_count;
+ mAttachmentEstTriangleCount += component.est_triangle_count;
+
+ // Add HUD/object complexity info if present
+ if (component.hud_complexity.objectId.notNull())
+ {
+ hud_list.push_back(component.hud_complexity);
+ }
+ if (component.object_complexity.objectId.notNull())
+ {
+ object_list.push_back(component.object_complexity);
+ }
+}
+
+void LLVOAvatar::markAttachmentComplexityDirty(const LLUUID& object_id, bool force_reset_attachment)
+{
+ // Mark the cache entry if it exists
+ complexity_cache_map_t::iterator it = mComplexityCache.find(object_id);
+ if (it != mComplexityCache.end())
+ {
+ if (force_reset_attachment)
+ {
+ // Object was detached.
+ // Force reset it in case it lingers in gObjectList for some reason (ex: dropped to world).
+ it->second.reset();
+ }
+ it->second.needs_update = true;
+ }
+
+ // Launch update process if not already scheduled
+ // It will add any missing attachments.
+ mVisualComplexityStale = true;
+}
+
+void LLVOAvatar::markBodyPartsComplexityDirty()
+{
+ mBodyPartsComplexity.needs_update = true;
+
+ // Launch update process if not already scheduled
+ mVisualComplexityStale = true;
+}
+
+void LLVOAvatar::performPartialComplexityUpdate(const F32 max_attachment_complexity)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
+
+ // Update any attachments marked as dirty
+ // Todo: might want to limit time or count here and defer the rest
+ // till next run. In such a case will need to make sure
+ // mVisualComplexityStale remains true.
+
+ for (attachment_map_t::iterator iter = mAttachmentPoints.begin();
+ iter != mAttachmentPoints.end(); ++iter)
+ {
+ LLViewerJointAttachment* attachment = iter->second;
+ if (!attachment || !attachment->getValid())
+ {
+ continue;
+ }
+
+ for (LLViewerJointAttachment::attachedobjs_vec_t::iterator attachment_iter = attachment->mAttachedObjects.begin();
+ attachment_iter != attachment->mAttachedObjects.end(); ++attachment_iter)
+ {
+ LLViewerObject* attached_object = attachment_iter->get();
+ if (attached_object && !attached_object->isDead())
+ {
+ LLUUID object_id = attached_object->getID();
+ ComplexityComponent& cache = mComplexityCache[object_id];
+
+ // Update if cache is stale or a new entry.
+ if (shouldUpdateComplexityComponent(cache))
+ {
+ calculateAttachmentComplexity(attached_object, max_attachment_complexity, cache);
+ }
+ }
+ }
+ }
+
+ // Update body parts if stale
+ if (shouldUpdateComplexityComponent(mBodyPartsComplexity))
+ {
+ calculateBodyPartsComplexity(mBodyPartsComplexity);
+ }
+}
+
+// Calculations for mVisualComplexity value
+// Call rate is flexible, can be once in 20, can be once in 200 frames,
+// depends on priority and known cost of an avatar in question.
+void LLVOAvatar::calculateUpdateRenderComplexity()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
+
+ // ****************************************************************
+ // This calculation should not be modified by third party viewers,
+ // since it is used to limit rendering and should be uniform for
+ // everyone. If you have suggested improvements, submit them to
+ // the official viewer for consideration.
+ // ****************************************************************
+
+ if (!mVisualComplexityStale)
+ {
+ return;
+ }
+
+ // Get the attachment complexity limit
+ static LLCachedControl max_complexity_setting(gSavedSettings, "MaxAttachmentComplexity");
+ F32 max_attachment_complexity = max_complexity_setting;
+ max_attachment_complexity = llmax(max_attachment_complexity, DEFAULT_MAX_ATTACHMENT_COMPLEXITY);
+
+ // Update complexity for any dirty attachments or body parts.
+ //
+ // Todo: Limit this by time or count and continue later as
+ // doing everything in one go can be very expensive (multiple ms)
+ // Note that calculateUpdateRenderComplexity() can be launched once
+ // per 200 frames. Limiting it by time or count runs the risk of
+ // already checked attachments getting stale on last_update_time,
+ // thus function will keep running indefinetely.
+ performPartialComplexityUpdate(max_attachment_complexity);
+
+ // Reset per-run counters
+ mAttachmentSurfaceArea = 0.f;
+ mAttachmentVisibleTriangleCount = 0;
+ mAttachmentEstTriangleCount = 0.f;
+
+ U32 total_cost = 0;
+ hud_complexity_list_t hud_complexity_list;
+ object_complexity_list_t object_complexity_list;
+
+ // Calculate and accumulate control avatar complexity if applicable
+ // For now this is on each run.
+ // Todo: See if mControlAvatarComplexity.needs_update is applicable here.
+ if (calculateControlAvatarComplexity(mControlAvatarComplexity, max_attachment_complexity))
+ {
+ accumulateComplexityComponent(
+ mControlAvatarComplexity,
+ total_cost,
+ hud_complexity_list,
+ object_complexity_list);
+ }
+
+ // Accumulate body parts complexity
+ accumulateComplexityComponent(mBodyPartsComplexity, total_cost, hud_complexity_list, object_complexity_list);
+
+ // Accumulate all attachment complexity from cache
+ // Clean up cache entries for attachments that no longer exist
+ std::vector to_remove;
+
+ for (complexity_cache_map_t::iterator cache_iter = mComplexityCache.begin();
+ cache_iter != mComplexityCache.end(); ++cache_iter)
+ {
+ const LLUUID& object_id = cache_iter->first;
+
+ // Verify object still exists
+ LLViewerObject* obj = gObjectList.findObject(object_id);
+ if (!obj
+ || obj->isDead()
+ || !obj->isAttachment())
+ {
+ to_remove.push_back(object_id);
+ continue;
+ }
+
+ // Accumulate this attachment's complexity
+ accumulateComplexityComponent(cache_iter->second, total_cost,
+ hud_complexity_list, object_complexity_list);
+ }
+
+ // Remove stale cache entries
+ for (std::vector::iterator it = to_remove.begin(); it != to_remove.end(); ++it)
+ {
+ mComplexityCache.erase(*it);
+ }
+
+ if (total_cost != mVisualComplexity)
+ {
+ LL_DEBUGS("AvatarRender") << "Avatar " << getID()
+ << " complexity updated was " << mVisualComplexity << " now " << total_cost
+ << " reported " << mReportedVisualComplexity
+ << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("AvatarRender") << "Avatar " << getID()
+ << " complexity updated no change " << mVisualComplexity
+ << " reported " << mReportedVisualComplexity
+ << LL_ENDL;
+ }
+
+ // Store results
+ mVisualComplexity = total_cost;
+
+ // Call the existing reporting function with the aggregated lists
+ processComplexityCostChange(hud_complexity_list, object_complexity_list);
+
+ // Stop processing until something changes
+ mVisualComplexityStale = false;
+}
+
+U32 LLVOAvatar::calculateBodyPartsComplexity()
+{
+ constexpr U32 COMPLEXITY_BODY_PART_COST = 200;
+ U32 cost = 0;
+ for (U8 baked_index = 0; baked_index < BAKED_NUM_INDICES; baked_index++)
+ {
+ const LLAvatarAppearanceDictionary::BakedEntry* baked_dict
+ = LLAvatarAppearance::getDictionary()->getBakedTexture((EBakedTextureIndex)baked_index);
+ ETextureIndex tex_index = baked_dict->mTextureIndex;
+ if ((tex_index != TEX_SKIRT_BAKED) || (isWearingWearableType(LLWearableType::WT_SKIRT)))
+ {
+ // Same as isTextureVisible(), but doesn't account for isSelf to ensure identical numbers for all avatars
+ if (isIndexLocalTexture(tex_index))
+ {
+ if (isTextureDefined(tex_index, 0))
+ {
+ cost += COMPLEXITY_BODY_PART_COST;
+ }
+ }
+ else
+ {
+ // baked textures can use TE images directly
+ if (isTextureDefined(tex_index)
+ && (getTEImage(tex_index)->getID() != IMG_INVISIBLE || LLDrawPoolAlpha::sShowDebugAlpha))
+ {
+ cost += COMPLEXITY_BODY_PART_COST;
+ }
+ }
+ }
+ }
+ LL_DEBUGS("ARCdetail") << "Avatar body parts complexity: " << cost << LL_ENDL;
+ return cost;
+}
+
+void LLVOAvatar::processComplexityCostChange(const hud_complexity_list_t &hud_complexity_list, const object_complexity_list_t &object_complexity_list)
+{
+ static LLCachedControl show_my_complexity_changes(gSavedSettings, "ShowMyComplexityChanges", 20);
+
+ if (isSelf() && show_my_complexity_changes)
+ {
+ // Avatar complexity
+ LLAvatarRenderNotifier::getInstance()->updateNotificationAgent(mVisualComplexity);
+ LLAvatarRenderNotifier::getInstance()->setObjectComplexityList(object_complexity_list);
+ // HUD complexity
+ LLHUDRenderNotifier::getInstance()->updateNotificationHUD(hud_complexity_list);
+ }
+
+ //schedule an update to ART next frame if needed
+ if (LLPerfStats::tunables.userAutoTuneEnabled &&
+ LLPerfStats::tunables.userFPSTuningStrategy != LLPerfStats::TUNE_SCENE_ONLY &&
+ !isVisuallyMuted())
+ {
+ const LLUUID id = getID(); // <== use id to make sure this avatar didn't get deleted between frames
+ LL::WorkQueue::getInstance("mainloop")->post([id]()
+ {
+ LLViewerObject* obj = gObjectList.findObject(id);
+ if (obj
+ && !obj->isDead()
+ && obj->isAvatar()
+ && obj->mDrawable)
+ {
+ LLVOAvatar* avatar = (LLVOAvatar*)obj;
+ gPipeline.profileAvatar(avatar);
+ }
+ });
+ }
+}
// Account for the complexity of a single top-level object associated
// with an avatar. This will be either an attached object or an animated
@@ -11159,15 +11541,18 @@ void LLVOAvatar::accountRenderComplexityForObject(
const F32 max_attachment_complexity,
LLVOVolume::texture_cost_t& textures,
U32& cost,
- hud_complexity_list_t& hud_complexity_list,
- object_complexity_list_t& object_complexity_list)
+ U32& visible_triangle_count,
+ F32& est_triangle_count,
+ F32& surface_area,
+ LLHUDComplexity& hud_object_complexity,
+ LLObjectComplexity& object_complexity)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
if (attached_object && !attached_object->isHUDAttachment())
{
- mAttachmentVisibleTriangleCount += attached_object->recursiveGetTriangleCount();
- mAttachmentEstTriangleCount += attached_object->recursiveGetEstTrianglesMax();
- mAttachmentSurfaceArea += attached_object->recursiveGetScaledSurfaceArea();
+ visible_triangle_count += attached_object->recursiveGetTriangleCount();
+ est_triangle_count += attached_object->recursiveGetEstTrianglesMax();
+ surface_area += attached_object->recursiveGetScaledSurfaceArea();
textures.clear();
const LLDrawable* drawable = attached_object->mDrawable;
@@ -11222,11 +11607,9 @@ void LLVOAvatar::accountRenderComplexityForObject(
if (isSelf())
{
- LLObjectComplexity object_complexity;
object_complexity.objectName = attached_object->getAttachmentItemName();
object_complexity.objectId = attached_object->getAttachmentItemID();
object_complexity.objectCost = (U32)attachment_total_cost;
- object_complexity_list.push_back(object_complexity);
}
}
}
@@ -11238,13 +11621,12 @@ void LLVOAvatar::accountRenderComplexityForObject(
&& attached_object->mDrawable)
{
textures.clear();
- mAttachmentSurfaceArea += attached_object->recursiveGetScaledSurfaceArea();
+ surface_area += attached_object->recursiveGetScaledSurfaceArea();
const LLVOVolume* volume = attached_object->mDrawable->getVOVolume();
if (volume)
{
bool is_rigged_mesh = volume->isRiggedMeshFast();
- LLHUDComplexity hud_object_complexity;
hud_object_complexity.objectName = attached_object->getAttachmentItemName();
hud_object_complexity.objectId = attached_object->getAttachmentItemID();
std::string joint_name;
@@ -11299,145 +11681,6 @@ void LLVOAvatar::accountRenderComplexityForObject(
}
}
}
- hud_complexity_list.push_back(hud_object_complexity);
- }
- }
-}
-
-// Calculations for mVisualComplexity value
-void LLVOAvatar::calculateUpdateRenderComplexity()
-{
- /*****************************************************************
- * This calculation should not be modified by third party viewers,
- * since it is used to limit rendering and should be uniform for
- * everyone. If you have suggested improvements, submit them to
- * the official viewer for consideration.
- *****************************************************************/
- if (mVisualComplexityStale)
- {
- LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR;
-
- static const U32 COMPLEXITY_BODY_PART_COST = 200;
- static LLCachedControl max_complexity_setting(gSavedSettings, "MaxAttachmentComplexity");
- F32 max_attachment_complexity = max_complexity_setting;
- max_attachment_complexity = llmax(max_attachment_complexity, DEFAULT_MAX_ATTACHMENT_COMPLEXITY);
-
- // Diagnostic list of all textures on our avatar
- static std::unordered_set all_textures;
-
- U32 cost = VISUAL_COMPLEXITY_UNKNOWN;
- LLVOVolume::texture_cost_t textures;
- hud_complexity_list_t hud_complexity_list;
- object_complexity_list_t object_complexity_list;
-
- for (U8 baked_index = 0; baked_index < BAKED_NUM_INDICES; baked_index++)
- {
- const LLAvatarAppearanceDictionary::BakedEntry *baked_dict
- = LLAvatarAppearance::getDictionary()->getBakedTexture((EBakedTextureIndex)baked_index);
- ETextureIndex tex_index = baked_dict->mTextureIndex;
- if ((tex_index != TEX_SKIRT_BAKED) || (isWearingWearableType(LLWearableType::WT_SKIRT)))
- {
- // Same as isTextureVisible(), but doesn't account for isSelf to ensure identical numbers for all avatars
- if (isIndexLocalTexture(tex_index))
- {
- if (isTextureDefined(tex_index, 0))
- {
- cost += COMPLEXITY_BODY_PART_COST;
- }
- }
- else
- {
- // baked textures can use TE images directly
- if (isTextureDefined(tex_index)
- && (getTEImage(tex_index)->getID() != IMG_INVISIBLE || LLDrawPoolAlpha::sShowDebugAlpha))
- {
- cost += COMPLEXITY_BODY_PART_COST;
- }
- }
- }
- }
- LL_DEBUGS("ARCdetail") << "Avatar body parts complexity: " << cost << LL_ENDL;
-
- mAttachmentVisibleTriangleCount = 0;
- mAttachmentEstTriangleCount = 0.f;
- mAttachmentSurfaceArea = 0.f;
-
- // A standalone animated object needs to be accounted for
- // using its associated volume. Attached animated objects
- // will be covered by the subsequent loop over attachments.
- LLControlAvatar *control_av = dynamic_cast(this);
- if (control_av)
- {
- LLVOVolume *volp = control_av->mRootVolp;
- if (volp && !volp->isAttachment())
- {
- accountRenderComplexityForObject(volp, max_attachment_complexity,
- textures, cost, hud_complexity_list, object_complexity_list);
- }
- }
-
- // Account for complexity of all attachments.
- for (attachment_map_t::const_iterator attachment_point = mAttachmentPoints.begin();
- attachment_point != mAttachmentPoints.end();
- ++attachment_point)
- {
- LLViewerJointAttachment* attachment = attachment_point->second;
- for (LLViewerJointAttachment::attachedobjs_vec_t::iterator attachment_iter = attachment->mAttachedObjects.begin();
- attachment_iter != attachment->mAttachedObjects.end();
- ++attachment_iter)
- {
- LLViewerObject* attached_object = attachment_iter->get();
- accountRenderComplexityForObject(attached_object, max_attachment_complexity,
- textures, cost, hud_complexity_list, object_complexity_list);
- }
- }
-
- if ( cost != mVisualComplexity )
- {
- LL_DEBUGS("AvatarRender") << "Avatar "<< getID()
- << " complexity updated was " << mVisualComplexity << " now " << cost
- << " reported " << mReportedVisualComplexity
- << LL_ENDL;
- }
- else
- {
- LL_DEBUGS("AvatarRender") << "Avatar "<< getID()
- << " complexity updated no change " << mVisualComplexity
- << " reported " << mReportedVisualComplexity
- << LL_ENDL;
- }
- mVisualComplexity = cost;
- mVisualComplexityStale = false;
-
- static LLCachedControl show_my_complexity_changes(gSavedSettings, "ShowMyComplexityChanges", 20);
-
- if (isSelf() && show_my_complexity_changes)
- {
- // Avatar complexity
- LLAvatarRenderNotifier::getInstance()->updateNotificationAgent(mVisualComplexity);
- LLAvatarRenderNotifier::getInstance()->setObjectComplexityList(object_complexity_list);
- // HUD complexity
- LLHUDRenderNotifier::getInstance()->updateNotificationHUD(hud_complexity_list);
- }
-
- //schedule an update to ART next frame if needed
- if (LLPerfStats::tunables.userAutoTuneEnabled &&
- LLPerfStats::tunables.userFPSTuningStrategy != LLPerfStats::TUNE_SCENE_ONLY &&
- !isVisuallyMuted())
- {
- const LLUUID id = getID(); // <== use id to make sure this avatar didn't get deleted between frames
- LL::WorkQueue::getInstance("mainloop")->post([id]()
- {
- LLViewerObject* obj = gObjectList.findObject(id);
- if (obj
- && !obj->isDead()
- && obj->isAvatar()
- && obj->mDrawable)
- {
- LLVOAvatar* avatar = (LLVOAvatar*)obj;
- gPipeline.profileAvatar(avatar);
- }
- });
}
}
}
diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h
index fc3a97a25d0..580d6ec911b 100644
--- a/indra/newview/llvoavatar.h
+++ b/indra/newview/llvoavatar.h
@@ -302,11 +302,17 @@ class LLVOAvatar :
const F32 max_attachment_complexity,
LLVOVolume::texture_cost_t& textures,
U32& cost,
- hud_complexity_list_t& hud_complexity_list,
- object_complexity_list_t& object_complexity_list);
+ U32& visible_triangle_count,
+ F32& est_triangle_count,
+ F32& surface_area,
+ LLHUDComplexity& hud_object_complexity,
+ LLObjectComplexity& object_complexity);
void calculateUpdateRenderComplexity();
static const U32 VISUAL_COMPLEXITY_UNKNOWN;
void updateVisualComplexity();
+ // Mark that an attachment needs complexity recalculation
+ void markAttachmentComplexityDirty(const LLUUID& object_id, bool force_reset_attachment = false);
+ void markBodyPartsComplexityDirty();
void placeProfileQuery();
void readProfileQuery(S32 retries);
@@ -581,12 +587,6 @@ class LLVOAvatar :
// CPU render time in ms
F32 mCPURenderTime = 0.f;
- // the isTooComplex method uses these mutable values to avoid recalculating too frequently
- // DEPRECATED -- obsolete avatar render cost values
- mutable U32 mVisualComplexity;
- mutable bool mVisualComplexityStale;
- U32 mReportedVisualComplexity; // from other viewers through the simulator
-
mutable bool mCachedInMuteList;
mutable F64 mCachedMuteListUpdateTime;
mutable bool mCachedInBuddyList = false;
@@ -594,6 +594,81 @@ class LLVOAvatar :
VisualMuteSettings mVisuallyMuteSetting; // Always or never visually mute this AV
+ //--------------------------------------------------------------------
+ // Complexity calculation and caching
+ //--------------------------------------------------------------------
+
+private:
+ // Structure to cache complexity metrics for individual attachments or components
+ struct ComplexityComponent
+ {
+ U32 render_cost;
+ U32 triangle_count;
+ F32 surface_area;
+ F32 est_triangle_count;
+ LLVOVolume::texture_cost_t textures;
+ F64 last_update_time;
+ bool needs_update;
+
+ // For tracking HUD and object lists
+ LLHUDComplexity hud_complexity;
+ LLObjectComplexity object_complexity;
+
+ ComplexityComponent()
+ : render_cost(0)
+ , triangle_count(0)
+ , surface_area(0.f)
+ , est_triangle_count(0.f)
+ , last_update_time(0.0)
+ , needs_update(true)
+ {
+ }
+
+ void reset()
+ {
+ render_cost = 0;
+ triangle_count = 0;
+ surface_area = 0.f;
+ est_triangle_count = 0.f;
+ textures.clear();
+ hud_complexity.reset();
+ object_complexity.reset();
+ needs_update = true;
+ }
+ };
+
+ void calculateAttachmentComplexity(LLViewerObject* attached_object,
+ const F32 max_attachment_complexity,
+ ComplexityComponent& cache);
+ void calculateBodyPartsComplexity(ComplexityComponent& cache);
+ U32 calculateBodyPartsComplexity();
+
+ // return true, if a valid control avatar.
+ bool calculateControlAvatarComplexity(ComplexityComponent& cache, const F32 max_attachment_complexity);
+
+ void accumulateComplexityComponent(const ComplexityComponent& component,
+ U32& total_cost,
+ hud_complexity_list_t& hud_list,
+ object_complexity_list_t& object_list);
+
+ bool shouldUpdateComplexityComponent(const ComplexityComponent& component) const;
+ void performPartialComplexityUpdate(const F32 max_attachment_complexity);
+
+ void processComplexityCostChange(const hud_complexity_list_t &hud_complexity_list, const object_complexity_list_t &object_complexity_list);
+
+ // Todo: probably safe to store by local instead of global id
+ // since they should be unique to this avatar, but local id might be not known.
+ typedef std::map complexity_cache_map_t;
+ complexity_cache_map_t mComplexityCache; // Cache per-attachment complexity
+ ComplexityComponent mBodyPartsComplexity; // Cache for body parts (mesh, eyes, hair, etc)
+ ComplexityComponent mControlAvatarComplexity; // Cache for animated object control avatar
+
+ // the isTooComplex method uses these mutable values to avoid recalculating too frequently
+ // DEPRECATED -- obsolete avatar render cost values
+ mutable U32 mVisualComplexity;
+ mutable bool mVisualComplexityStale;
+ U32 mReportedVisualComplexity; // from other viewers through the simulator
+
//--------------------------------------------------------------------
// animated object status
//--------------------------------------------------------------------
@@ -630,7 +705,6 @@ class LLVOAvatar :
// Shadowing
//--------------------------------------------------------------------
public:
- void updateShadowFaces();
LLDrawable* mShadow;
private:
LLFace* mShadow0Facep;
diff --git a/indra/newview/llvoicechannel.cpp b/indra/newview/llvoicechannel.cpp
index b941d356a10..0852258994a 100644
--- a/indra/newview/llvoicechannel.cpp
+++ b/indra/newview/llvoicechannel.cpp
@@ -472,10 +472,6 @@ void LLVoiceChannelGroup::activate()
}
}
}
-
- // Mic default state is OFF on initiating/joining Ad-Hoc/Group calls. It's on for P2P using the AdHoc infra.
-
- LLVoiceClient::getInstance()->setUserPTTState(mIsP2P);
}
}
@@ -534,6 +530,10 @@ void LLVoiceChannelGroup::handleStatusChange(EStatusType type)
case STATUS_JOINED:
mRetries = 3;
mIsRetrying = false;
+
+ // Mic default state is OFF on initiating/joining Ad-Hoc/Group calls. It's on for P2P using the AdHoc infra.
+ LLVoiceClient::getInstance()->setUserPTTState(mIsP2P);
+ break;
default:
break;
}
diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp
index 3a700423b30..333046adce1 100644
--- a/indra/newview/llvoicewebrtc.cpp
+++ b/indra/newview/llvoicewebrtc.cpp
@@ -343,6 +343,39 @@ const LLVoiceVersionInfo& LLWebRTCVoiceClient::getVersion()
return mVoiceVersion;
}
+// --------------------------------------------------
+
+void LLWebRTCVoiceClient::updateVersion()
+{
+ sessionStatePtr_t session = mNextSession.get() ? mNextSession : mSession;
+
+ if (session)
+ {
+ // A WebRTC session can be connected to multiple servers at once. To more easily disambiguate which server version is being printed, show the connection type. In most cases, this shouldn't matter and the Janus version should be the same for all connections. Janus versions are also logged for each connection.
+ mVoiceVersion.serverVersion = session->getVersion();
+ if (session->isCallbackPossible())
+ {
+ mVoiceVersion.mBuildVersion = "ad-hoc";
+ }
+ else if (session->isEstate())
+ {
+ mVoiceVersion.mBuildVersion = "estate";
+ }
+ else if (session->isSpatial())
+ {
+ mVoiceVersion.mBuildVersion = "parcel";
+ }
+ else
+ {
+ mVoiceVersion.mBuildVersion = mVoiceVersion.serverVersion;
+ }
+ }
+ else
+ {
+ mVoiceVersion.serverVersion = mVoiceVersion.mBuildVersion = "";
+ }
+}
+
//---------------------------------------------------
void LLWebRTCVoiceClient::updateSettings()
@@ -2054,6 +2087,22 @@ void LLWebRTCVoiceClient::sessionState::revive()
mShuttingDown = false;
}
+const std::string LLWebRTCVoiceClient::sessionState::getVersion() const
+{
+ // Prefer the version of a primary connection which has already received a version string over the data channel. If that does not make sense, fall back to any non-empty version string we can find.
+ bool primary = true;
+ do
+ {
+ for (auto& connection : mWebRTCConnections) {
+ if (connection->isPrimary() == primary && connection->getVersion().length()) {
+ return connection->getVersion();
+ }
+ }
+ primary = !primary;
+ } while (!primary);
+ return "";
+}
+
//=========================================================================
// the following are methods to support the coroutine implementation of the
// voice connection and processing. They should only be called in the context
@@ -2250,6 +2299,11 @@ void LLWebRTCVoiceClient::deleteSession(const sessionStatePtr_t &session)
{
mNextSession.reset();
}
+
+ if (!sShuttingDown)
+ {
+ updateVersion();
+ }
}
@@ -2625,6 +2679,10 @@ void LLVoiceWebRTCConnection::sendData(const std::string &data)
}
}
+const std::string& LLVoiceWebRTCConnection::getVersion() {
+ return mServerVersion;
+}
+
// Tell the simulator that we're shutting down a voice connection.
// The simulator will pass this on to the Secondlife WebRTC server.
void LLVoiceWebRTCConnection::breakVoiceConnectionCoro(connectionPtr_t connection)
@@ -3048,6 +3106,7 @@ bool LLVoiceWebRTCConnection::connectionStateMachine()
// An object where each key is an agent id. (in the future, we may allow
// integer indices into an agentid list, populated on join commands. For size.
// Each key will point to a json object with keys identifying what's updated.
+// 'V' - voice server version (string)
// 'p' - audio source power (level/volume) (int8 as int)
// 'j' - object of join data (currently only a boolean 'p' marking a primary participant)
// 'l' - boolean, always true if exists.
@@ -3108,6 +3167,16 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b
boost::json::object participant_obj = participant_elem.value().as_object();
+ if (participant_obj.contains("V") && participant_obj["V"].is_string() && agent_id == gAgentID)
+ {
+ // sendJoin was called on the connection. The voice server has responded with the new version string. Set it here.
+ mServerVersion = participant_obj["V"].as_string().c_str();
+ LLWebRTCVoiceClient::getInstance()->updateVersion();
+ LL_DEBUGS("Voice") << "Received version string \"" << participant_obj["V"].as_string().c_str()
+ << "\" for connection: primary=" << mPrimary << ", spatial=" << isSpatial()
+ << ", region=" << mRegionID << ", mChannelID=" << mChannelID << LL_ENDL;
+ }
+
LLWebRTCVoiceClient::participantStatePtr_t participant =
LLWebRTCVoiceClient::getInstance()->findParticipantByID(mChannelID, agent_id);
bool joined = false;
diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h
index 2ce575852ab..8efbd1778f4 100644
--- a/indra/newview/llvoicewebrtc.h
+++ b/indra/newview/llvoicewebrtc.h
@@ -80,6 +80,7 @@ class LLWebRTCVoiceClient : public LLSingleton,
static bool isShuttingDown() { return sShuttingDown; }
const LLVoiceVersionInfo& getVersion() override;
+ void updateVersion();
void updateSettings() override; // call after loading settings and whenever they change
@@ -285,6 +286,7 @@ class LLWebRTCVoiceClient : public LLSingleton,
void shutdownAllConnections();
void revive();
+ const std::string getVersion() const;
static void processSessionStates();
@@ -609,6 +611,7 @@ class LLVoiceWebRTCConnection :
void sendJoin();
void sendData(const std::string &data);
+ const std::string& getVersion();
void processIceUpdates();
@@ -623,6 +626,7 @@ class LLVoiceWebRTCConnection :
bool connectionStateMachine();
virtual bool isSpatial() { return false; }
+ bool isPrimary() const { return mPrimary; }
LLUUID getRegionID() { return mRegionID; }
@@ -694,6 +698,7 @@ class LLVoiceWebRTCConnection :
bool mPrimary;
LLUUID mViewerSession;
std::string mChannelID;
+ std::string mServerVersion;
std::string mChannelSDP;
std::string mRemoteChannelSDP;
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index b328d3a414c..3b41ccb6fc3 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -1253,7 +1253,35 @@ void LLVOVolume::updateSculptTexture()
void LLVOVolume::updateVisualComplexity()
{
- LLVOAvatar* avatar = getAvatarAncestor();
+ LLVOAvatar* avatar = nullptr;
+ LLViewerObject* pobj = (LLViewerObject*)getParent();
+ LLViewerObject* lobj = this;
+ while (pobj)
+ {
+ avatar = pobj->asAvatar();
+ if (avatar)
+ {
+ break;
+ }
+ lobj = pobj;
+ pobj = (LLViewerObject*)pobj->getParent();
+ }
+
+ if (avatar)
+ {
+ // mark parent as dirty, complexity will be updated recursively.
+ avatar->markAttachmentComplexityDirty(lobj->getID());
+ }
+ LLVOAvatar* rigged_avatar = getAvatar();
+ if (rigged_avatar && (rigged_avatar != avatar))
+ {
+ // This might be wrong. Control avatars update each run,
+ // due to lack of dirty mechanics, and this might be
+ // where we should implement and call
+ // markControlAvatarComplexityDirty() if !isAttachment().
+ rigged_avatar->markAttachmentComplexityDirty(lobj->getID());
+ }
+ /*LLVOAvatar* avatar = getAvatarAncestor();
if (avatar)
{
avatar->updateVisualComplexity();
@@ -1262,7 +1290,7 @@ void LLVOVolume::updateVisualComplexity()
if(rigged_avatar && (rigged_avatar != avatar))
{
rigged_avatar->updateVisualComplexity();
- }
+ }*/
}
void LLVOVolume::notifyMeshLoaded()
@@ -5983,7 +6011,7 @@ void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group)
bool should_render = true;
if (gltf_mat->mAlphaMode == LLGLTFMaterial::ALPHA_MODE_BLEND)
{
- if (gltf_mat->mBaseColor.mV[3] == 0.0f)
+ if (gltf_mat->mBaseColor.mV[3] == 0.0f && !LLDrawPoolAlpha::sShowDebugAlpha)
{
should_render = false;
}
diff --git a/indra/newview/llvvmquery.cpp b/indra/newview/llvvmquery.cpp
new file mode 100644
index 00000000000..12dcc1d04dd
--- /dev/null
+++ b/indra/newview/llvvmquery.cpp
@@ -0,0 +1,189 @@
+/**
+ * @file llvvmquery.cpp
+ * @brief Query the Viewer Version Manager (VVM) for update information
+ *
+ * $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$
+ */
+
+#include "llviewerprecompiledheaders.h"
+#include "llvvmquery.h"
+
+#include "llcorehttputil.h"
+#include "llcoros.h"
+#include "llevents.h"
+#include "llviewernetwork.h"
+#include "llversioninfo.h"
+#include "llviewercontrol.h"
+#include "llhasheduniqueid.h"
+#include "lluri.h"
+#include "llsys.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
+namespace
+{
+ std::string get_platform_string()
+ {
+#if LL_WINDOWS
+ return "win64";
+#elif LL_DARWIN
+ return "mac64";
+#elif LL_LINUX
+ return "lnx64";
+#else
+ return "unknown";
+#endif
+ }
+
+ std::string get_platform_version()
+ {
+ return LLOSInfo::instance().getOSVersionString();
+ }
+
+ std::string get_machine_id()
+ {
+ unsigned char id[MD5HEX_STR_SIZE];
+ if (llHashedUniqueID(id))
+ {
+ return std::string(reinterpret_cast(id));
+ }
+ return "unknown";
+ }
+
+ void query_vvm_coro()
+ {
+ // Get base URL from grid manager
+ std::string base_url = LLGridManager::getInstance()->getUpdateServiceURL();
+
+ // We use this for dev testing when working with VVM and working on the updater. Not advisable to uncomment it.
+ //std::string base_url = "https://update.qa.secondlife.io/update";
+
+ if (base_url.empty())
+ {
+ LL_WARNS("VVM") << "No update service URL configured" << LL_ENDL;
+ return;
+ }
+
+ // Gather parameters for VVM query
+ std::string channel = LLVersionInfo::instance().getChannel();
+
+ // We use this for dev testing when working with VVM and working on the updater. Not advisable to uncomment it.
+ // std::string channel = "QA Target for Velopack";
+
+ std::string version = LLVersionInfo::instance().getVersion();
+ std::string platform = get_platform_string();
+ std::string platform_version = get_platform_version();
+ std::string test_ok = gSavedSettings.getBOOL("UpdaterWillingToTest") ? "testok" : "testno";
+ std::string machine_id = get_machine_id();
+
+ // Build URL: {base}/v1.2/{channel}/{version}/{platform}/{platform_version}/{testok}/{uuid}
+ std::string url = base_url + "/v1.2/" +
+ LLURI::escape(channel) + "/" +
+ LLURI::escape(version) + "/" +
+ platform + "/" +
+ LLURI::escape(platform_version) + "/" +
+ test_ok + "/" +
+ machine_id;
+
+ LL_INFOS("VVM") << "Querying VVM: " << url << LL_ENDL;
+
+ // Make HTTP GET request
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t adapter =
+ std::make_shared("VVMQuery", httpPolicy);
+ LLCore::HttpRequest::ptr_t request = std::make_shared();
+
+ LLSD result = adapter->getAndSuspend(request, url);
+
+ // Check HTTP status
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ if (!status)
+ {
+ if (status.getType() == 404)
+ {
+ LL_INFOS("VVM") << "Unmanaged channel, no updates available" << LL_ENDL;
+ return;
+ }
+ LL_WARNS("VVM") << "VVM query failed: " << status.toString() << LL_ENDL;
+ return;
+ }
+
+ // Read whether this update is required or optional
+ bool update_required = result["required"].asBoolean();
+ std::string relnotes = result["more_info"].asString();
+
+ // Extract update URL for current platform
+ LLSD platforms = result["platforms"];
+ if (platforms.has(platform))
+ {
+ std::string update_url = platforms[platform]["url"].asString();
+#if LL_VELOPACK
+ std::string velopack_url = platforms[platform]["velopack_url"].asString();
+ U32 updater_service = gSavedSettings.getU32("UpdaterServiceSetting");
+ std::string required_version = update_required ? result["version"].asString() : "";
+ // Skip network check if no required version AND user only wants mandatory updates
+ if (!velopack_url.empty() && (update_required || updater_service != 0))
+ {
+ LL_INFOS("VVM") << "Velopack feed URL: " << velopack_url
+ << " required_version: " << required_version << LL_ENDL;
+ velopack_set_update_url(velopack_url);
+
+ LLCoros::instance().launch("VelopackUpdateCheck",
+ [required_version, relnotes]()
+ {
+ velopack_check_for_updates(required_version, relnotes);
+ });
+ }
+ else if (!velopack_url.empty())
+ {
+ LL_INFOS("VVM") << "Optional update skipped (UpdaterServiceSetting=0)" << LL_ENDL;
+ }
+ else
+#endif
+ if (!update_url.empty())
+ {
+ LL_INFOS("VVM") << "Update available at: " << update_url << LL_ENDL;
+ }
+ }
+ else
+ {
+ LL_INFOS("VVM") << "No update available for platform: " << platform << LL_ENDL;
+ }
+
+ // Post release notes URL to the relnotes event pump
+ if (!relnotes.empty())
+ {
+ LL_INFOS("VVM") << "Release notes URL: " << relnotes << LL_ENDL;
+ LLEventPumps::instance().obtain("relnotes").post(relnotes);
+ }
+ }
+}
+
+void initVVMUpdateCheck()
+{
+ LL_INFOS("VVM") << "Initializing VVM update check" << LL_ENDL;
+ LLCoros::instance().launch("VVMUpdateCheck", &query_vvm_coro);
+}
diff --git a/indra/newview/llfloaterhowto.h b/indra/newview/llvvmquery.h
similarity index 52%
rename from indra/newview/llfloaterhowto.h
rename to indra/newview/llvvmquery.h
index 9d7793817a6..977d82af643 100644
--- a/indra/newview/llfloaterhowto.h
+++ b/indra/newview/llvvmquery.h
@@ -1,10 +1,10 @@
/**
- * @file llfloaterhowto.h
- * @brief A variant of web floater meant to open guidebook
+ * @file llvvmquery.h
+ * @brief Query the Viewer Version Manager (VVM) for update information
*
- * $LicenseInfo:firstyear=2021&license=viewerlgpl$
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
* Second Life Viewer Source Code
- * Copyright (C) 2021, Linden Research, Inc.
+ * 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
@@ -24,35 +24,19 @@
* $/LicenseInfo$
*/
-#ifndef LL_LLFLOATERHOWTO_H
-#define LL_LLFLOATERHOWTO_H
+#ifndef LL_LLVVMQUERY_H
+#define LL_LLVVMQUERY_H
-#include "llfloaterwebcontent.h"
-
-class LLMediaCtrl;
-
-
-class LLFloaterHowTo :
- public LLFloaterWebContent
-{
-public:
- LOG_CLASS(LLFloaterHowTo);
-
- typedef LLFloaterWebContent::Params Params;
-
- LLFloaterHowTo(const Params& key);
-
- 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*/ };
-
-private:
- bool postBuild() override;
-};
-
-#endif // LL_LLFLOATERHOWTO_H
+/**
+ * Initialize the VVM update check.
+ *
+ * This launches a coroutine that queries the Viewer Version Manager (VVM)
+ * to check for available updates. If an update is available, it configures
+ * Velopack with the update URL and initiates the update check/download.
+ *
+ * The release notes URL from the VVM response is posted to the "relnotes"
+ * event pump for display.
+ */
+void initVVMUpdateCheck();
+#endif // LL_LLVVMQUERY_H
diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp
index 47e1815bc2b..d02694de7dc 100644
--- a/indra/newview/llworld.cpp
+++ b/indra/newview/llworld.cpp
@@ -1217,6 +1217,7 @@ void process_disable_simulator(LLMessageSystem *mesgsys, void **user_data)
void process_region_handshake(LLMessageSystem* msg, void** user_data)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
LLHost host = msg->getSender();
LLViewerRegion* regionp = LLWorld::getInstance()->getRegion(host);
if (!regionp)
diff --git a/indra/newview/llworldmap.cpp b/indra/newview/llworldmap.cpp
index 1db36649fae..c7c8404eb91 100644
--- a/indra/newview/llworldmap.cpp
+++ b/indra/newview/llworldmap.cpp
@@ -603,25 +603,103 @@ void LLWorldMap::dropImagePriorities()
// Load all regions in a given rectangle (in region grid coordinates, i.e. world / 256 meters)
void LLWorldMap::updateRegions(S32 x0, S32 y0, S32 x1, S32 y1)
{
- // Convert those boundaries to the corresponding (MAP_BLOCK_SIZE x MAP_BLOCK_SIZE) block coordinates
- x0 = x0 / MAP_BLOCK_SIZE;
- x1 = x1 / MAP_BLOCK_SIZE;
- y0 = y0 / MAP_BLOCK_SIZE;
- y1 = y1 / MAP_BLOCK_SIZE;
-
- // Load the region info those blocks
- for (S32 block_x = llmax(x0, 0); block_x <= llmin(x1, MAP_BLOCK_RES-1); ++block_x)
+ constexpr S32 MAX_REQUEST_REGIONS = 64; // Server side enforced limit.
+ constexpr S32 REGIONS_PER_BLOCK = MAP_BLOCK_SIZE * MAP_BLOCK_SIZE; // 4 x 4 = 16 regions per block
+ constexpr S32 MAX_TOTAL_BLOCKS = MAX_REQUEST_REGIONS / REGIONS_PER_BLOCK; // 64 / 16 = 4 blocks total
+ constexpr S32 MAX_BLOCKS_PER_SIDE = MAX_TOTAL_BLOCKS; // Can have up to 4 blocks in one dimension (e.g., 4x1, 1x4, 2x2)
+
+ // Convert region coordinates to block coordinates
+ // We use fixed sized blocks for ease of storage and lookup,
+ // but the requests can be of variable size of up to
+ // MAX_REQUEST_REGIONS.
+ S32 block_x0 = x0 / MAP_BLOCK_SIZE;
+ S32 block_x1 = x1 / MAP_BLOCK_SIZE;
+ S32 block_y0 = y0 / MAP_BLOCK_SIZE;
+ S32 block_y1 = y1 / MAP_BLOCK_SIZE;
+
+ // Clamp to valid range
+ block_x0 = llmax(block_x0, 0);
+ block_x1 = llmin(block_x1, MAP_BLOCK_RES - 1);
+ block_y0 = llmax(block_y0, 0);
+ block_y1 = llmin(block_y1, MAP_BLOCK_RES - 1);
+
+ // Process blocks, grouping unloaded blocks into larger requests up to MAX_TOTAL_BLOCKS
+ for (S32 block_y = block_y0; block_y <= block_y1; )
{
- for (S32 block_y = llmax(y0, 0); block_y <= llmin(y1, MAP_BLOCK_RES-1); ++block_y)
+ for (S32 block_x = block_x0; block_x <= block_x1; )
{
S32 offset = block_x | (block_y * MAP_BLOCK_RES);
if (!mMapBlockLoaded[offset])
{
- //LL_INFOS("WorldMap") << "Loading Block (" << block_x << "," << block_y << ")" << LL_ENDL;
- LLWorldMapMessage::getInstance()->sendMapBlockRequest(block_x * MAP_BLOCK_SIZE, block_y * MAP_BLOCK_SIZE, (block_x * MAP_BLOCK_SIZE) + MAP_BLOCK_SIZE - 1, (block_y * MAP_BLOCK_SIZE) + MAP_BLOCK_SIZE - 1);
- mMapBlockLoaded[offset] = true;
+ // Find the maximum contiguous unloaded rectangle starting at this block
+ S32 request_width = 1;
+ S32 request_height = 1;
+
+ // Expand width (check horizontal contiguous unloaded blocks)
+ while (request_width < MAX_BLOCKS_PER_SIDE &&
+ (block_x + request_width) <= block_x1)
+ {
+ // request_height is 1, can add blocks one by one.
+ S32 check_offset = (block_x + request_width) | (block_y * MAP_BLOCK_RES);
+ if (mMapBlockLoaded[check_offset])
+ {
+ break;
+ }
+ ++request_width;
+ }
+
+ // Expand height (check vertical contiguous unloaded blocks)
+ while (request_height < MAX_BLOCKS_PER_SIDE &&
+ (block_y + request_height) <= block_y1 &&
+ (request_width * (request_height + 1) <= MAX_TOTAL_BLOCKS)) // Don't exceed 64 total regions
+ {
+ bool can_expand = true;
+ // Width can be >1, loop over blocks in the line
+ for (S32 x = 0; x < request_width; ++x)
+ {
+ S32 check_offset = (block_x + x) | ((block_y + request_height) * MAP_BLOCK_RES);
+ if (mMapBlockLoaded[check_offset])
+ {
+ can_expand = false;
+ break;
+ }
+ }
+ if (!can_expand) break;
+ ++request_height;
+ }
+
+ // Send request for the contiguous rectangle
+ S32 min_x = block_x * MAP_BLOCK_SIZE;
+ S32 min_y = block_y * MAP_BLOCK_SIZE;
+ S32 max_x = (block_x + request_width) * MAP_BLOCK_SIZE - 1;
+ S32 max_y = (block_y + request_height) * MAP_BLOCK_SIZE - 1;
+
+ LL_DEBUGS("WorldMap") << "Loading Block rectangle (" << block_x << "," << block_y
+ << ") to (" << (block_x + request_width - 1) << "," << (block_y + request_height - 1)
+ << ") [" << (request_width * request_height * MAP_BLOCK_SIZE * MAP_BLOCK_SIZE) << " regions]" << LL_ENDL;
+
+ LLWorldMapMessage::getInstance()->sendMapBlockRequest(min_x, min_y, max_x, max_y);
+
+ // Mark all blocks in the requested rectangle as loaded
+ for (S32 y = 0; y < request_height; ++y)
+ {
+ for (S32 x = 0; x < request_width; ++x)
+ {
+ S32 mark_offset = (block_x + x) | ((block_y + y) * MAP_BLOCK_RES);
+ mMapBlockLoaded[mark_offset] = true;
+ }
+ }
+
+ // Skip over the width of blocks we just requested
+ block_x += request_width;
+ }
+ else
+ {
+ // This block is already loaded, move to next
+ ++block_x;
}
}
+ ++block_y;
}
}
diff --git a/indra/newview/llworldmapmessage.cpp b/indra/newview/llworldmapmessage.cpp
index e81e6b45966..c039f9de3fd 100644
--- a/indra/newview/llworldmapmessage.cpp
+++ b/indra/newview/llworldmapmessage.cpp
@@ -33,7 +33,7 @@
#include "llagent.h"
#include "llfloaterworldmap.h"
-const U32 LAYER_FLAG = 2;
+constexpr U32 LAYER_FLAG = 2;
//---------------------------------------------------------------------------
// World Map Message Handling
@@ -135,7 +135,11 @@ void LLWorldMapMessage::sendMapBlockRequest(U16 min_x, U16 min_y, U16 max_x, U16
msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
U32 flags = LAYER_FLAG;
- flags |= (return_nonexistent ? 0x10000 : 0);
+ if (return_nonexistent)
+ {
+ // overwrite LAYER_FLAG, otherwise server won't respond to missing regions
+ flags = MAP_SIM_RETURN_NULL_SIMS;
+ }
msg->addU32Fast(_PREHASH_Flags, flags);
msg->addU32Fast(_PREHASH_EstateID, 0); // Filled in on sim
msg->addBOOLFast(_PREHASH_Godlike, false); // Filled in on sim
@@ -150,6 +154,7 @@ void LLWorldMapMessage::sendMapBlockRequest(U16 min_x, U16 min_y, U16 max_x, U16
// public static
void LLWorldMapMessage::processMapBlockReply(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
if (gNonInteractive)
{
return;
@@ -157,15 +162,17 @@ void LLWorldMapMessage::processMapBlockReply(LLMessageSystem* msg, void**)
U32 agent_flags;
msg->getU32Fast(_PREHASH_AgentData, _PREHASH_Flags, agent_flags);
- // There's only one flag that we ever use here
- if (agent_flags != LAYER_FLAG)
+ S32 num_blocks = msg->getNumberOfBlocksFast(_PREHASH_Data);
+
+ // There's only one flag that we ever use here, unless we also want an existence check.
+ if (agent_flags != LAYER_FLAG
+ && num_blocks != 1) // we check existence for a single region
{
LL_WARNS() << "Invalid map image type returned! layer = " << agent_flags << LL_ENDL;
return;
}
- S32 num_blocks = msg->getNumberOfBlocksFast(_PREHASH_Data);
- //LL_INFOS("WorldMap") << "num_blocks = " << num_blocks << LL_ENDL;
+ LL_DEBUGS("WorldMap") << "num_blocks = " << num_blocks << LL_ENDL;
bool found_null_sim = false;
@@ -191,26 +198,31 @@ void LLWorldMapMessage::processMapBlockReply(LLMessageSystem* msg, void**)
U32 x_world = (U32)(x_regions) * REGION_WIDTH_UNITS;
U32 y_world = (U32)(y_regions) * REGION_WIDTH_UNITS;
- // name shouldn't be empty, see EXT-4568
- llassert(!name.empty());
-
- // Insert that region in the world map, if failure, flag it as a "null_sim"
- if (!(LLWorldMap::getInstance()->insertRegion(x_world, y_world, name, image_id, (U32)accesscode, region_flags)))
+ // Name shouldn't be empty unless region doesn't exist
+ if (!name.empty())
{
- found_null_sim = true;
- }
+ // Insert that region in the world map, if failure, flag it as a "null_sim"
+ if (!(LLWorldMap::getInstance()->insertRegion(x_world, y_world, name, image_id, (U32)accesscode, region_flags)))
+ {
+ found_null_sim = true;
+ }
- // If we hit a valid tracking location, do what needs to be done app level wise
- if (LLWorldMap::getInstance()->isTrackingValidLocation())
- {
- LLVector3d pos_global = LLWorldMap::getInstance()->getTrackedPositionGlobal();
- if (LLWorldMap::getInstance()->isTrackingDoubleClick())
+ // If we hit a valid tracking location, do what needs to be done app level wise
+ if (LLWorldMap::getInstance()->isTrackingValidLocation())
{
- // Teleport if the user double clicked
- gAgent.teleportViaLocation(pos_global);
+ LLVector3d pos_global = LLWorldMap::getInstance()->getTrackedPositionGlobal();
+ if (LLWorldMap::getInstance()->isTrackingDoubleClick())
+ {
+ // Teleport if the user double clicked
+ gAgent.teleportViaLocation(pos_global);
+ }
+ // Update the "real" tracker information
+ gFloaterWorldMap->trackLocation(pos_global);
}
- // Update the "real" tracker information
- gFloaterWorldMap->trackLocation(pos_global);
+ }
+ else
+ {
+ found_null_sim = true;
}
// Handle the SLURL callback if any
@@ -237,6 +249,7 @@ void LLWorldMapMessage::processMapBlockReply(LLMessageSystem* msg, void**)
// public static
void LLWorldMapMessage::processMapItemReply(LLMessageSystem* msg, void**)
{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
//LL_INFOS("WorldMap") << LL_ENDL;
U32 type;
msg->getU32Fast(_PREHASH_RequestData, _PREHASH_ItemType, type);
diff --git a/indra/newview/llworldmapview.cpp b/indra/newview/llworldmapview.cpp
index 1be6a6cfff1..8e7b86eb009 100755
--- a/indra/newview/llworldmapview.cpp
+++ b/indra/newview/llworldmapview.cpp
@@ -1737,8 +1737,11 @@ void LLWorldMapView::updateVisibleBlocks()
const F32 half_height = F32(height) / 2.0f;
// Compute center into sim grid coordinates
- S32 world_center_x = S32((-mPanX / mMapScale) + (camera_global.mdV[0] / REGION_WIDTH_METERS));
- S32 world_center_y = S32((-mPanY / mMapScale) + (camera_global.mdV[1] / REGION_WIDTH_METERS));
+ // mPanX and mPanY values can be obsolete as they are used for
+ // animation and there is no point loading regions we will see
+ // so briefly, they won't have time to load. Use target directly.
+ S32 world_center_x = S32((-mTargetPanX / mMapScale) + (camera_global.mdV[0] / REGION_WIDTH_METERS));
+ S32 world_center_y = S32((-mTargetPanY / mMapScale) + (camera_global.mdV[1] / REGION_WIDTH_METERS));
// Compute the boundaries into sim grid coordinates
S32 world_left = world_center_x - S32(half_width / mMapScale) - 1;
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index c9d53bbcbc0..4ef88f5deb3 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -609,7 +609,9 @@ void LLPipeline::init()
{
cntrl_ptr->getCommitSignal()->connect([](LLControlVariable* control, const LLSD& value, const LLSD& previous)
{
- LLFontVertexBuffer::enableBufferCollection(control->getValue().asBoolean());
+ bool enable_buffers = control->getValue().asBoolean();
+ LLFontVertexBuffer::enableBufferCollection(enable_buffers);
+ LLFontWidthBuffer::enableBufferCollection(enable_buffers);
});
}
}
@@ -1146,7 +1148,9 @@ void LLPipeline::refreshCachedSettings()
LLVOAvatar::updateImpostorRendering(LLVOAvatar::sMaxNonImpostors);
}
- LLFontVertexBuffer::enableBufferCollection(gSavedSettings.getBOOL("CollectFontVertexBuffers"));
+ bool enable_buffers = gSavedSettings.getBOOL("CollectFontVertexBuffers");
+ LLFontVertexBuffer::enableBufferCollection(enable_buffers);
+ LLFontWidthBuffer::enableBufferCollection(enable_buffers);
}
void LLPipeline::releaseGLBuffers()
diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml
index ff5737ab49a..faf2bd9434d 100644
--- a/indra/newview/skins/default/textures/textures.xml
+++ b/indra/newview/skins/default/textures/textures.xml
@@ -141,7 +141,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/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/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/strings.xml b/indra/newview/skins/default/xui/de/strings.xml
index d7b2ee57a8e..bd26672ccfe 100644
--- a/indra/newview/skins/default/xui/de/strings.xml
+++ b/indra/newview/skins/default/xui/de/strings.xml
@@ -5537,9 +5537,6 @@ Setzen Sie den Editorpfad in Anführungszeichen
Grid-Status
-
- Infos
-
Inventar
@@ -5636,9 +5633,6 @@ Setzen Sie den Editorpfad in Anführungszeichen
Aktuellen Grid-Status anzeigen
-
- Wie führe ich gängige Aufgaben aus?
-
Ihr Eigentum anzeigen und benutzen
diff --git a/indra/newview/skins/default/xui/en/floater_create_landmark.xml b/indra/newview/skins/default/xui/en/floater_create_landmark.xml
index abe8344097c..6d3da72962d 100644
--- a/indra/newview/skins/default/xui/en/floater_create_landmark.xml
+++ b/indra/newview/skins/default/xui/en/floater_create_landmark.xml
@@ -87,7 +87,7 @@
name="notes_editor"
spellcheck="true"
text_readonly_color="white"
- prevalidator="ascii_with_newline"
+ prevalidator="ascii"
commit_on_focus_lost="true"
top_pad="5"
width="290"
diff --git a/indra/newview/skins/default/xui/en/floater_how_to.xml b/indra/newview/skins/default/xui/en/floater_how_to.xml
deleted file mode 100644
index 5b00d23faaf..00000000000
--- a/indra/newview/skins/default/xui/en/floater_how_to.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/en/menu_login.xml b/indra/newview/skins/default/xui/en/menu_login.xml
index 5fff9b7bc0f..de967d25e35 100644
--- a/indra/newview/skins/default/xui/en/menu_login.xml
+++ b/indra/newview/skins/default/xui/en/menu_login.xml
@@ -56,15 +56,6 @@
label="Help"
tear_off="true"
name="Help">
-
-
-
-
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index ffe4bcebd53..f7541f52ad0 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -1804,14 +1804,6 @@ function="World.EnvPreset"
label="Help"
name="Help"
tear_off="true">
-
-
-
-
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 93ee27b1969..221bac329d9 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -200,6 +200,16 @@ No tutorial is currently available.
yestext="OK"/>
+
+ [APP_NAME] found an installation of an older version [VERSION]. To uninstall the older version, please follow [https://community.secondlife.com/knowledgebase/english/how-to-uninstall-and-reinstall-second-life-r524 this manual].
+
+
+
+
+Downloading update [VERSION]...
+The viewer will restart once the download is complete.
+
+
This land has damage enabled.
-You can be hurt here. If you die, you will be teleported to your home location.
+You can be hurt here. If you die, you might be teleported to your home location, region telehub or parcel landing point.
+
+
+
+
+
+ Members are not loaded
+
Folder is empty.
No matches.
You do not have a copy of this texture in your inventory
+ You do not have a copy of this material in your inventory
Your Marketplace purchases will appear here. You may then drag them into your inventory to use them.
https://marketplace.[MARKETPLACE_DOMAIN_NAME]/
http://community.secondlife.com/t5/English-Knowledge-Base/Selling-in-the-Marketplace/ta-p/700193#Section_.3
@@ -4201,7 +4202,6 @@ name="Command_360_Capture_Label">360 snapshot
My Environments
Gestures
Grid status
- Guidebook
Inventory
Map
Marketplace
@@ -4234,7 +4234,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/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/strings.xml b/indra/newview/skins/default/xui/es/strings.xml
index 86e454b83ec..1bfab01108e 100644
--- a/indra/newview/skins/default/xui/es/strings.xml
+++ b/indra/newview/skins/default/xui/es/strings.xml
@@ -5439,9 +5439,6 @@ Inténtalo incluyendo la ruta de acceso al editor entre comillas
Estado del Grid
-
- Cómo
-
Inventario
@@ -5538,9 +5535,6 @@ Inténtalo incluyendo la ruta de acceso al editor entre comillas
Mostrar el estado actual del Grid
-
- Cómo hacer las tareas habituales
-
Ver y usar tus pertenencias
diff --git a/indra/newview/skins/default/xui/fr/floater_how_to.xml b/indra/newview/skins/default/xui/fr/floater_how_to.xml
deleted file mode 100644
index a414212ba06..00000000000
--- a/indra/newview/skins/default/xui/fr/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/fr/notifications.xml b/indra/newview/skins/default/xui/fr/notifications.xml
index 587c88faad2..9c79f288c19 100644
--- a/indra/newview/skins/default/xui/fr/notifications.xml
+++ b/indra/newview/skins/default/xui/fr/notifications.xml
@@ -1109,7 +1109,6 @@ Vérifiez votre connexion Internet et réessayez dans quelques minutes, cliquez
Votre personnage va apparaître dans un moment.
Pour marcher, utilisez les flèches de direction.
-Appuyez sur F1 pour obtenir de l'aide ou en savoir plus sur [SECOND_LIFE].
Choisissez un avatar homme ou femme.
Vous pourrez revenir sur votre décision plus tard.
diff --git a/indra/newview/skins/default/xui/fr/strings.xml b/indra/newview/skins/default/xui/fr/strings.xml
index 4fb5167d67b..4a7a337d4c2 100644
--- a/indra/newview/skins/default/xui/fr/strings.xml
+++ b/indra/newview/skins/default/xui/fr/strings.xml
@@ -5538,9 +5538,6 @@ Essayez avec le chemin d'accès à l'éditeur entre guillemets doubles
État de la grille
-
- Aide rapide
-
Inventaire
@@ -5637,9 +5634,6 @@ Essayez avec le chemin d'accès à l'éditeur entre guillemets doubles
Afficher l’état actuel de la grille
-
- Comment effectuer les opérations courantes
-
Afficher et utiliser vos possessions
diff --git a/indra/newview/skins/default/xui/it/floater_how_to.xml b/indra/newview/skins/default/xui/it/floater_how_to.xml
deleted file mode 100644
index 8f0e2105712..00000000000
--- a/indra/newview/skins/default/xui/it/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/it/notifications.xml b/indra/newview/skins/default/xui/it/notifications.xml
index f79cc1515b3..284f947b18c 100644
--- a/indra/newview/skins/default/xui/it/notifications.xml
+++ b/indra/newview/skins/default/xui/it/notifications.xml
@@ -1115,7 +1115,6 @@ Controlla la tua connessione Internet e riprova fra qualche minuto, oppure clicc
Il tuo avatar apparirà fra un attimo.
Usa le frecce per muoverti.
-Premi F1 in qualunque momento per la guida o per apprendere altre cose di [SECOND_LIFE].
Scegli un avatar maschile o femminile. Puoi sempre cambiare idea più tardi.
diff --git a/indra/newview/skins/default/xui/it/strings.xml b/indra/newview/skins/default/xui/it/strings.xml
index 2ddb7d77d14..b4d6b2a3151 100644
--- a/indra/newview/skins/default/xui/it/strings.xml
+++ b/indra/newview/skins/default/xui/it/strings.xml
@@ -5453,9 +5453,6 @@ Prova a racchiudere il percorso dell'editor in doppie virgolette.
Stato della griglia
-
- Istruzioni
-
Inventario
@@ -5552,9 +5549,6 @@ Prova a racchiudere il percorso dell'editor in doppie virgolette.
Mostra stato griglia corrente
-
- Come eseguire le attività più comuni
-
Visualizza e usa le tue cose
diff --git a/indra/newview/skins/default/xui/ja/floater_how_to.xml b/indra/newview/skins/default/xui/ja/floater_how_to.xml
deleted file mode 100644
index 480fb4649cf..00000000000
--- a/indra/newview/skins/default/xui/ja/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/indra/newview/skins/default/xui/ja/notifications.xml b/indra/newview/skins/default/xui/ja/notifications.xml
index 9ec7a0de986..07d6c412138 100644
--- a/indra/newview/skins/default/xui/ja/notifications.xml
+++ b/indra/newview/skins/default/xui/ja/notifications.xml
@@ -2162,7 +2162,6 @@ https://wiki.secondlife.com/wiki/Adding_Spelling_Dictionaries
まもなく、あなたのアバターが表示されます。矢印キーを使用して歩きます。
-ヘルプが必要なときや、[SECOND_LIFE]について知りたいときは、F1キーを押してください。
男性あるいは女性のアバターを選択してください。この設定は後で変更できます。
confirm
diff --git a/indra/newview/skins/default/xui/ja/strings.xml b/indra/newview/skins/default/xui/ja/strings.xml
index 7d618bc3377..a67e58a42ae 100644
--- a/indra/newview/skins/default/xui/ja/strings.xml
+++ b/indra/newview/skins/default/xui/ja/strings.xml
@@ -7143,9 +7143,6 @@ www.secondlife.com から最新バージョンをダウンロードしてくだ
グリッド状況
-
- ハウツー
-
インベントリ
@@ -7233,9 +7230,6 @@ www.secondlife.com から最新バージョンをダウンロードしてくだ
現在のグリッドステータスを表示します。
-
- 一般的タスクの実行方法を表示します。
-
インベントリの内容を表示したり使用したりします。
diff --git a/indra/newview/skins/default/xui/pl/floater_how_to.xml b/indra/newview/skins/default/xui/pl/floater_how_to.xml
deleted file mode 100644
index 2c412de30ab..00000000000
--- a/indra/newview/skins/default/xui/pl/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/pl/notifications.xml b/indra/newview/skins/default/xui/pl/notifications.xml
index ad9d2ecf1d4..a8ea3c7c1a8 100644
--- a/indra/newview/skins/default/xui/pl/notifications.xml
+++ b/indra/newview/skins/default/xui/pl/notifications.xml
@@ -1397,7 +1397,6 @@ Możesz sprawdzić swoje połączenie z Internetem i spróbować ponownie za kil
Twoja postać pojawi się za moment.
Używaj strzałek żeby się poruszać.
-Naciśnij F1 w dowolnej chwili po pomoc albo żeby dowiedzieć się więcej o [SECOND_LIFE].
Wybierz awatara właściwej płci. Ten wybór będzie można później zmienić.
diff --git a/indra/newview/skins/default/xui/pl/strings.xml b/indra/newview/skins/default/xui/pl/strings.xml
index 7a618786185..dc7ca53aaa6 100644
--- a/indra/newview/skins/default/xui/pl/strings.xml
+++ b/indra/newview/skins/default/xui/pl/strings.xml
@@ -4992,9 +4992,6 @@ Spróbuj załączyć ścieżkę do edytora w cytowaniu.
Status świata
-
- Samouczek
-
Szafa
@@ -5076,9 +5073,6 @@ Spróbuj załączyć ścieżkę do edytora w cytowaniu.
Pokaż obecny status świata
-
- Jak wykonywać zwyczajne rzeczy
-
Przeglądaj i używaj rzeczy, jakie należą do Ciebie
diff --git a/indra/newview/skins/default/xui/pt/floater_how_to.xml b/indra/newview/skins/default/xui/pt/floater_how_to.xml
deleted file mode 100644
index 15c4946cb03..00000000000
--- a/indra/newview/skins/default/xui/pt/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/pt/notifications.xml b/indra/newview/skins/default/xui/pt/notifications.xml
index a3220bca54a..153109fc1bb 100644
--- a/indra/newview/skins/default/xui/pt/notifications.xml
+++ b/indra/newview/skins/default/xui/pt/notifications.xml
@@ -1108,7 +1108,6 @@ Cheque sua conexão e tente em alguns minutos, clique na Ajuda para acessar o [S
Seu personagem irá aparecer num momento.
Use as teclas de seta para andar.
-Pressione a tecla F1 para ajuda ou aprender mais sobre [SECOND_LIFE].
Por favor, escolha se o seu avatar é feminino ou masculino. Você pode mudar de idéia depois.
diff --git a/indra/newview/skins/default/xui/pt/strings.xml b/indra/newview/skins/default/xui/pt/strings.xml
index 543fd45573f..3d03a71e814 100644
--- a/indra/newview/skins/default/xui/pt/strings.xml
+++ b/indra/newview/skins/default/xui/pt/strings.xml
@@ -5406,9 +5406,6 @@ Tente colocar o caminho do editor entre aspas.
Status da grade
-
- Como
-
Inventário
@@ -5505,9 +5502,6 @@ Tente colocar o caminho do editor entre aspas.
Mostrar status da grade atual
-
- Como executar tarefas comuns
-
Exibir e usar seus pertences
diff --git a/indra/newview/skins/default/xui/ru/floater_how_to.xml b/indra/newview/skins/default/xui/ru/floater_how_to.xml
deleted file mode 100644
index 52525e5d333..00000000000
--- a/indra/newview/skins/default/xui/ru/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/ru/notifications.xml b/indra/newview/skins/default/xui/ru/notifications.xml
index e75fd1fd822..96eb917e390 100644
--- a/indra/newview/skins/default/xui/ru/notifications.xml
+++ b/indra/newview/skins/default/xui/ru/notifications.xml
@@ -1459,7 +1459,6 @@
Ваш персонаж появится через мгновение.
Для ходьбы нажимайте клавиши со стрелками.
-В любой момент можно нажать клавишу F1 для получения справки или информации о [SECOND_LIFE].
Выберите мужской или женский аватар. Этот выбор затем можно будет изменить.
diff --git a/indra/newview/skins/default/xui/ru/strings.xml b/indra/newview/skins/default/xui/ru/strings.xml
index 8433f893b5a..86973c6ae73 100644
--- a/indra/newview/skins/default/xui/ru/strings.xml
+++ b/indra/newview/skins/default/xui/ru/strings.xml
@@ -5544,9 +5544,6 @@ support@secondlife.com.
Состояние сетки
-
- Инструкции
-
Инвентарь
@@ -5643,9 +5640,6 @@ support@secondlife.com.
Показать текущее состояние сетки
-
- Выполнение типичных задач
-
Просмотр и использование вашего имущества
diff --git a/indra/newview/skins/default/xui/tr/floater_how_to.xml b/indra/newview/skins/default/xui/tr/floater_how_to.xml
deleted file mode 100644
index a42fe0b1221..00000000000
--- a/indra/newview/skins/default/xui/tr/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/tr/notifications.xml b/indra/newview/skins/default/xui/tr/notifications.xml
index 17d2969d196..de429f3292b 100644
--- a/indra/newview/skins/default/xui/tr/notifications.xml
+++ b/indra/newview/skins/default/xui/tr/notifications.xml
@@ -1460,7 +1460,6 @@ Yeni bir ana konum ayarlamak isteyebilirsiniz.
Karakteriniz birazdan görünecek.
Yürümek için ok tuşlarını kullanın.
-Yardım almak ya da [SECOND_LIFE] hakkında daha fazla bilgi edinmek için istediğiniz zaman F1 tuşuna basın.
Lütfen bir erkek ya da kadın avatar seçin. Fikrinizi daha sonra değiştirebilirsiniz.
diff --git a/indra/newview/skins/default/xui/tr/strings.xml b/indra/newview/skins/default/xui/tr/strings.xml
index e68d0001797..3eaa6a64925 100644
--- a/indra/newview/skins/default/xui/tr/strings.xml
+++ b/indra/newview/skins/default/xui/tr/strings.xml
@@ -5539,9 +5539,6 @@ Düzenleyici yolunu çift tırnakla çevrelemeyi deneyin.
Ağ durumu
-
- Nasıl yapılır
-
Envanter
@@ -5638,9 +5635,6 @@ Düzenleyici yolunu çift tırnakla çevrelemeyi deneyin.
Ağın mevcut durumunu göster
-
- Genel görevleri nasıl yapacağınız
-
Eşyalarınızı görüntüleyin ve kullanın
diff --git a/indra/newview/skins/default/xui/zh/floater_how_to.xml b/indra/newview/skins/default/xui/zh/floater_how_to.xml
deleted file mode 100644
index e033327165d..00000000000
--- a/indra/newview/skins/default/xui/zh/floater_how_to.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/indra/newview/skins/default/xui/zh/notifications.xml b/indra/newview/skins/default/xui/zh/notifications.xml
index 4d0f1cb85b6..fd0b613dd62 100644
--- a/indra/newview/skins/default/xui/zh/notifications.xml
+++ b/indra/newview/skins/default/xui/zh/notifications.xml
@@ -1446,7 +1446,6 @@
你的人物很快將會出現。
用方向鍵行走。
-任何時候你都可按 F1 鍵察看幫助,進一步瞭解 [SECOND_LIFE]。
請選擇男性或女性化身。 以後你仍可改變這個選擇。
diff --git a/indra/newview/skins/default/xui/zh/strings.xml b/indra/newview/skins/default/xui/zh/strings.xml
index cf6fa1d85f0..1a094ea258f 100644
--- a/indra/newview/skins/default/xui/zh/strings.xml
+++ b/indra/newview/skins/default/xui/zh/strings.xml
@@ -5531,9 +5531,6 @@ http://secondlife.com/support 求助解決問題。
網格狀態
-
- 簡易教學
-
收納區
@@ -5630,9 +5627,6 @@ http://secondlife.com/support 求助解決問題。
顯示當前網格狀態
-
- 如何完成常用的動作
-
察看並使用你擁有的物件
diff --git a/indra/newview/tests/lllogininstance_test.cpp b/indra/newview/tests/lllogininstance_test.cpp
index b82a58163c0..b00b89b9fd1 100644
--- a/indra/newview/tests/lllogininstance_test.cpp
+++ b/indra/newview/tests/lllogininstance_test.cpp
@@ -223,8 +223,6 @@ bool llHashedUniqueID(unsigned char* id)
//-----------------------------------------------------------------------------
#include "../llappviewer.h"
void LLAppViewer::forceQuit(void) {}
-bool LLAppViewer::isUpdaterMissing() { return true; }
-bool LLAppViewer::waitForUpdater() { return false; }
LLAppViewer * LLAppViewer::sInstance = 0;
//-----------------------------------------------------------------------------
diff --git a/indra/newview/tests/llworldmap_test.cpp b/indra/newview/tests/llworldmap_test.cpp
index 60172b39602..692dd5638c3 100644
--- a/indra/newview/tests/llworldmap_test.cpp
+++ b/indra/newview/tests/llworldmap_test.cpp
@@ -54,10 +54,22 @@ LLViewerFetchedTexture* LLViewerTextureManager::getFetchedTexture(const LLUUID&,
LLGLint, LLGLenum, LLHost ) { return NULL; }
// Stub related map calls
+
+// Records each sendMapBlockRequest call for inspection in tests
+struct MapBlockRequest
+{
+ U16 min_x, min_y, max_x, max_y;
+};
+static std::vector gMapBlockRequests;
+
LLWorldMapMessage::LLWorldMapMessage() { }
LLWorldMapMessage::~LLWorldMapMessage() { }
void LLWorldMapMessage::sendItemRequest(U32 type, U64 handle) { }
-void LLWorldMapMessage::sendMapBlockRequest(U16 min_x, U16 min_y, U16 max_x, U16 max_y, bool return_nonexistent) { }
+void LLWorldMapMessage::sendMapBlockRequest(U16 min_x, U16 min_y, U16 max_x, U16 max_y, bool return_nonexistent)
+{
+ MapBlockRequest req = { min_x, min_y, max_x, max_y };
+ gMapBlockRequests.push_back(req);
+}
LLWorldMipmap::LLWorldMipmap() { }
LLWorldMipmap::~LLWorldMipmap() { }
@@ -518,4 +530,63 @@ namespace tut
mWorld->cancelTracking();
ensure("LLWorldMap::cancelTracking() at end test failed", mWorld->isTracking() == false);
}
+ // Test updateRegions() request grouping, size limits, and bounds
+ template<> template<>
+ void worldmap_object_t::test<4>()
+ {
+ // Test 27 : 1x1 block (4x4 = 16 regions) - single small request within bounds
+ mWorld->reset();
+ gMapBlockRequests.clear();
+ mWorld->updateRegions(0, 0, 3, 3);
+ ensure("updateRegions 1x1 block: expected 1 request", gMapBlockRequests.size() == 1);
+ {
+ const MapBlockRequest& req = gMapBlockRequests[0];
+ ensure("updateRegions 1x1 block: min_x", req.min_x == 0);
+ ensure("updateRegions 1x1 block: min_y", req.min_y == 0);
+ ensure("updateRegions 1x1 block: max_x", req.max_x == 3);
+ ensure("updateRegions 1x1 block: max_y", req.max_y == 3);
+ S32 regions = (req.max_x - req.min_x + 1) * (req.max_y - req.min_y + 1);
+ ensure("updateRegions 1x1 block: must not exceed 64 regions", regions <= 64);
+ }
+
+ // Test 28 : 2x2 blocks (8x8 = 64 regions) - single request at the 64-region limit
+ mWorld->reset();
+ gMapBlockRequests.clear();
+ mWorld->updateRegions(0, 0, 7, 7);
+ ensure("updateRegions 2x2 blocks: expected 1 request", gMapBlockRequests.size() == 1);
+ {
+ const MapBlockRequest& req = gMapBlockRequests[0];
+ ensure("updateRegions 2x2 blocks: min_x", req.min_x == 0);
+ ensure("updateRegions 2x2 blocks: min_y", req.min_y == 0);
+ ensure("updateRegions 2x2 blocks: max_x", req.max_x == 7);
+ ensure("updateRegions 2x2 blocks: max_y", req.max_y == 7);
+ S32 regions = (req.max_x - req.min_x + 1) * (req.max_y - req.min_y + 1);
+ ensure("updateRegions 2x2 blocks: must not exceed 64 regions", regions <= 64);
+ }
+
+ // Test 29 : 4x4 blocks (16x16 = 256 total regions) - must split into multiple requests
+ // each spanning a 4x1 strip (64 regions), none exceeding the server limit
+ mWorld->reset();
+ gMapBlockRequests.clear();
+ mWorld->updateRegions(0, 0, 15, 15);
+ ensure("updateRegions 4x4 blocks: expected 4 requests", gMapBlockRequests.size() == 4);
+ for (size_t i = 0; i < gMapBlockRequests.size(); ++i)
+ {
+ const MapBlockRequest& req = gMapBlockRequests[i];
+ S32 regions = (req.max_x - req.min_x + 1) * (req.max_y - req.min_y + 1);
+ ensure("updateRegions 4x4 blocks: each request must not exceed 64 regions", regions <= 64);
+ // Requests must stay within the requested area (0..15)
+ ensure("updateRegions 4x4 blocks: max_x in bounds", req.max_x <= 15);
+ ensure("updateRegions 4x4 blocks: max_y in bounds", req.max_y <= 15);
+ }
+ // Each request covers one horizontal strip of 4 blocks (rows 0..3, 4..7, 8..11, 12..15)
+ ensure("updateRegions 4x4 blocks: request[0] min_y", gMapBlockRequests[0].min_y == 0);
+ ensure("updateRegions 4x4 blocks: request[0] max_y", gMapBlockRequests[0].max_y == 3);
+ ensure("updateRegions 4x4 blocks: request[1] min_y", gMapBlockRequests[1].min_y == 4);
+ ensure("updateRegions 4x4 blocks: request[1] max_y", gMapBlockRequests[1].max_y == 7);
+ ensure("updateRegions 4x4 blocks: request[2] min_y", gMapBlockRequests[2].min_y == 8);
+ ensure("updateRegions 4x4 blocks: request[2] max_y", gMapBlockRequests[2].max_y == 11);
+ ensure("updateRegions 4x4 blocks: request[3] min_y", gMapBlockRequests[3].min_y == 12);
+ ensure("updateRegions 4x4 blocks: request[3] max_y", gMapBlockRequests[3].max_y == 15);
+ }
}
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 109f00c9ae6..c33f4f140ae 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -540,18 +540,6 @@ def construct(self):
'*.bat',
'*.tar.xz')))
- with self.prefix(src=os.path.join(pkgdir, "VMP")):
- # include the compiled launcher scripts so that it gets included in the file_list
- self.path('SLVersionChecker.exe')
-
- with self.prefix(dst="vmp_icons"):
- with self.prefix(src=self.icon_path()):
- self.path("secondlife.ico")
- #VMP Tkinter icons
- with self.prefix(src="vmp_icons"):
- self.path("*.png")
- self.path("*.gif")
-
# Plugin host application
self.path2basename(os.path.join(os.pardir,
'llplugin', 'slplugin', self.args['configuration']),
@@ -765,6 +753,126 @@ def INSTDIR(path):
return '\n'.join(result)
def package_finish(self):
+ # Check if we should use Velopack instead of NSIS
+ # Note: as of 2026.01's release, we will be building with Velopack's one click install.
+ # We maintain the legacy NSIS packaging mainly for TPVs at this point.
+ if self.args.get('velopack', 'OFF') == 'ON':
+ self.velopack_package_finish()
+ return
+
+ # NSIS packaging (legacy)
+ self.nsis_package_finish()
+
+ def velopack_package_finish(self):
+ # packId determines install folder: %LocalAppData%\{packId}
+ # Uses same naming as NSIS INSTNAME for channel separation
+ pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc.
+ # Velopack requires SemVer2. Use major.minor.patch-buildnumber so that
+ # Velopack can distinguish builds and order them correctly.
+ pack_version = '.'.join(self.args['version'][:3])
+ if len(self.args['version']) > 3 and self.args['version'][3]:
+ pack_version += '-' + self.args['version'][3]
+ pack_title = self.app_name() # Display name with spaces
+ pack_dir = self.get_dst_prefix()
+ main_exe = self.final_exe()
+ installer_base = self.installer_base_name()
+ exclude_pattern = r'.*\.pdb|.*\.map|.*\.bat|.*\.exp|.*\.lib|.*\.nsi|.*\.tar\.xz|secondlife-bin\..*|.*_Setup\.exe|.*-Setup\.exe'
+
+ # Channel-specific icon for the Velopack installer.
+ # CMake copies icons/{channel}/secondlife.ico to res/ll_icon.ico at configure time.
+ # Try the CMake-generated copy first, fall back to the source icon.
+ icon_path = os.path.join(self.get_src_prefix(), 'res', 'll_icon.ico')
+ if not os.path.exists(icon_path):
+ icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.ico')
+
+ # In CI, defer Velopack packaging to the sign step where Azure credentials
+ # are available. Emit metadata as GitHub outputs so the sign step can run
+ # vpk pack with --signTemplate, producing a package with signed executables.
+ if os.getenv('GITHUB_ACTIONS'):
+ # Copy the icon into pack_dir so it's included in the Windows-app artifact
+ icon_filename = ''
+ if os.path.exists(icon_path):
+ icon_filename = os.path.basename(icon_path)
+ icon_dest = os.path.join(pack_dir, icon_filename)
+ shutil.copy2(icon_path, icon_dest)
+ print("Copied icon %s to %s" % (icon_path, icon_dest))
+ else:
+ print("WARNING: Icon not found at %s" % icon_path)
+
+ # Emit metadata for the sign step
+ self.set_github_output('velopack_pack_id', pack_id)
+ self.set_github_output('velopack_pack_version', pack_version)
+ self.set_github_output('velopack_pack_title', pack_title)
+ self.set_github_output('velopack_main_exe', main_exe)
+ self.set_github_output('velopack_icon', icon_filename)
+ self.set_github_output('velopack_installer_base', installer_base)
+ self.set_github_output('velopack_exclude', exclude_pattern)
+ # Set package_file so llmanifest's touched.bat logic doesn't crash
+ self.package_file = installer_base + '_Setup.exe'
+ print("CI mode: Velopack packaging deferred to sign step")
+ return
+
+ # Local builds: run vpk pack directly (unsigned)
+ vpk_args = [
+ 'vpk', 'pack',
+ '--packId', pack_id,
+ '--packVersion', pack_version,
+ '--packDir', pack_dir,
+ '--mainExe', main_exe,
+ '--packTitle', pack_title,
+ '--exclude', exclude_pattern,
+ # Suppress Velopack's built-in shortcut creation; we create our own
+ # shortcuts in llvelopack.cpp on_after_install hook instead.
+ '--shortcuts', '',
+ ]
+
+ # Add icon — CMake copies the channel-appropriate secondlife.ico to res/ll_icon.ico
+ if os.path.exists(icon_path):
+ print("Using icon: %s" % icon_path)
+ vpk_args.extend(['--icon', icon_path])
+ else:
+ print("WARNING: Icon not found at %s — Setup.exe will have no icon" % icon_path)
+
+ print("Running Velopack packaging: %s" % ' '.join(vpk_args))
+
+ # Run vpk command
+ import subprocess
+ result = subprocess.run(vpk_args, cwd=os.path.dirname(pack_dir), capture_output=True, text=True)
+ if result.stdout:
+ print("vpk stdout: %s" % result.stdout)
+ if result.stderr:
+ print("vpk stderr: %s" % result.stderr)
+ if result.returncode != 0:
+ raise ManifestError("Velopack packaging failed with code %d" % result.returncode)
+
+ # Velopack outputs to a Releases directory
+ releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases')
+
+ # Move the setup exe INTO pack_dir so it's included in the Windows-app artifact
+ # IMPORTANT: Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern
+ # in viewer_app output (line ~538). The underscore pattern excludes NSIS installers
+ # which are rebuilt during signing, but Velopack installers are created here.
+ # Velopack creates: {packId}-win-Setup.exe
+ velopack_setup = os.path.join(releases_dir, '%s-win-Setup.exe' % pack_id)
+ self.package_file = installer_base + '_Setup.exe'
+ our_setup = os.path.join(pack_dir, self.package_file)
+ if os.path.exists(velopack_setup):
+ shutil.move(velopack_setup, our_setup)
+ print("Moved %s to %s" % (velopack_setup, our_setup))
+
+ # Rename the portable zip to include the version number
+ # Velopack creates: {packId}-win-Portable.zip
+ velopack_portable = os.path.join(releases_dir, '%s-win-Portable.zip' % pack_id)
+ if os.path.exists(velopack_portable):
+ our_portable = os.path.join(releases_dir, installer_base + '_Portable.zip')
+ shutil.move(velopack_portable, our_portable)
+ print("Moved %s to %s" % (velopack_portable, our_portable))
+
+ # Output the Releases directory path for artifact upload (contains nupkg, RELEASES for updates)
+ self.set_github_output('velopack_releases', releases_dir)
+
+ def nsis_package_finish(self):
+ """Package the viewer using NSIS installer (legacy)"""
# a standard map of strings for replacing in the templates
substitution_strings = {
'version' : '.'.join(self.args['version']),
@@ -781,7 +889,7 @@ def package_finish(self):
substitution_strings['installer_file'] = installer_file
version_vars = """
- !define INSTEXE "SLVersionChecker.exe"
+ !define INSTEXE "%(final_exe)s"
!define VERSION "%(version_short)s"
!define VERSION_LONG "%(version)s"
!define VERSION_DASHES "%(version_dashes)s"
@@ -967,15 +1075,6 @@ def construct(self):
with self.prefix(src=self.icon_path(), dst="") :
self.path("secondlife.icns")
- # Copy in the updater script and helper modules
- self.path(src=os.path.join(pkgdir, 'VMP'), dst="updater")
-
- with self.prefix(src="", dst=os.path.join("updater", "icons")):
- self.path2basename(self.icon_path(), "secondlife.ico")
- with self.prefix(src="vmp_icons", dst=""):
- self.path("*.png")
- self.path("*.gif")
-
with self.prefix(src_dst="cursors_mac"):
self.path("*.tif")
@@ -1127,6 +1226,123 @@ def package_finish(self):
arcname=self.app_name() + ".app")
self.set_github_output_path('viewer_app', tarpath)
+ # Generate Velopack update packages if enabled
+ # This creates the nupkg and RELEASES files needed for auto-updates
+ # Distribution is still via DMG, but updates use Velopack
+ if self.args.get('velopack', 'OFF') == 'ON':
+ self.velopack_package_finish()
+
+ def velopack_package_finish(self):
+ """Generate Velopack update packages for macOS.
+
+ This creates the nupkg and releases.json files needed for auto-updates.
+ Distribution is still via DMG - Velopack only handles the update infrastructure.
+ """
+ # packId determines install identification - same as Windows for consistency
+ pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc.
+ # Velopack requires SemVer2. Use major.minor.patch-buildnumber so that
+ # Velopack can distinguish builds and order them correctly.
+ pack_version = '.'.join(self.args['version'][:3])
+ if len(self.args['version']) > 3 and self.args['version'][3]:
+ pack_version += '-' + self.args['version'][3]
+ pack_title = self.app_name() # Display name with spaces
+
+ # The .app bundle path (e.g., "/path/to/Second Life Release.app")
+ app_bundle = self.get_dst_prefix()
+ # Bundle ID from args (e.g., "com.secondlife.viewer")
+ bundle_id = self.args.get('bundleid', 'com.secondlife.indra.viewer')
+
+ # Icon path for macOS
+ icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.icns')
+
+ # The main executable inside Contents/MacOS/ is named after the channel
+ main_exe = self.channel()
+
+ # In CI, defer Velopack packaging to the sign step where code signing
+ # credentials are available. Emit metadata as GitHub outputs so the
+ # sign step can run vpk pack after signing the app bundle.
+ if os.getenv('GITHUB_ACTIONS'):
+ self.set_github_output('velopack_mac_pack_id', pack_id)
+ self.set_github_output('velopack_mac_pack_version', pack_version)
+ self.set_github_output('velopack_mac_pack_title', pack_title)
+ self.set_github_output('velopack_mac_main_exe', main_exe)
+ self.set_github_output('velopack_mac_bundle_id', bundle_id)
+ print("CI mode: macOS Velopack packaging deferred to sign step")
+ return
+
+ # Local builds: run vpk pack directly (unsigned)
+
+ # Parent directory containing the .app bundle - this is where we run vpk from
+ # and where the Releases directory will be created
+ work_dir = os.path.dirname(app_bundle)
+
+ # Output directory for releases - clean it first to avoid version conflicts
+ releases_dir = os.path.join(work_dir, 'Releases')
+ if os.path.exists(releases_dir):
+ print("Cleaning existing Releases directory: %s" % releases_dir)
+ shutil.rmtree(releases_dir)
+
+ # Build vpk command for macOS
+ # See: https://docs.velopack.io/reference/cli/content/vpk-osx
+ vpk_args = [
+ 'vpk', 'pack',
+ '--packId', pack_id,
+ '--packVersion', pack_version,
+ '--packDir', app_bundle,
+ '--packTitle', pack_title,
+ '--mainExe', main_exe, # Executable name inside Contents/MacOS/
+ '--bundleId', bundle_id,
+ '--outputDir', releases_dir,
+ '--noInst', # Don't generate .pkg installer - we use DMG for distribution
+ '--verbose', # Show detailed output
+ ]
+
+ # Add icon if exists
+ if os.path.exists(icon_path):
+ vpk_args.extend(['--icon', icon_path])
+
+ print("Running Velopack packaging for macOS:")
+ print(" Command: %s" % ' '.join(vpk_args))
+ print(" Working directory: %s" % work_dir)
+ print(" App bundle: %s" % app_bundle)
+ print(" Main executable: %s" % main_exe)
+
+ # Run vpk command
+ result = subprocess.run(vpk_args, cwd=work_dir, capture_output=True, text=True)
+
+ # Always print output for debugging
+ if result.stdout:
+ print("vpk stdout:\n%s" % result.stdout)
+ if result.stderr:
+ print("vpk stderr:\n%s" % result.stderr)
+
+ if result.returncode != 0:
+ raise ManifestError("Velopack packaging failed with code %d" % result.returncode)
+
+ # Verify the Releases directory was created and contains expected files
+ if not os.path.exists(releases_dir):
+ raise ManifestError("Velopack releases directory not found: %s" % releases_dir)
+
+ # List what was created
+ releases_contents = os.listdir(releases_dir)
+ print("Velopack releases directory contents: %s" % releases_contents)
+
+ # Verify we have the expected files (nupkg and releases JSON)
+ nupkg_files = [f for f in releases_contents if f.endswith('.nupkg')]
+ json_files = [f for f in releases_contents if f.endswith('.json')]
+
+ if not nupkg_files:
+ raise ManifestError("No .nupkg files found in releases directory")
+ if not json_files:
+ raise ManifestError("No releases JSON files found in releases directory")
+
+ print("Generated %d nupkg file(s): %s" % (len(nupkg_files), nupkg_files))
+ print("Generated %d JSON file(s): %s" % (len(json_files), json_files))
+
+ # Output the Releases directory path for artifact upload
+ self.set_github_output('velopack_releases', releases_dir)
+ print("Velopack releases directory: %s" % releases_dir)
+
class LinuxManifest(ViewerManifest):
build_data_json_platform = 'lnx'
@@ -1324,6 +1540,7 @@ def construct(self):
dict(name='discord', description="""Indication discord social sdk libraries are needed""", default='OFF'),
dict(name='openal', description="""Indication openal libraries are needed""", default='OFF'),
dict(name='tracy', description="""Indication tracy profiler is enabled""", default='OFF'),
+ dict(name='velopack', description="""Use Velopack installer instead of NSIS""", default='OFF'),
]
try:
main(extra=extra_arguments)
diff --git a/indra/test/lldatapacker_tut.cpp b/indra/test/lldatapacker_tut.cpp
index 2e975ee6370..5aa52338069 100644
--- a/indra/test/lldatapacker_tut.cpp
+++ b/indra/test/lldatapacker_tut.cpp
@@ -127,7 +127,7 @@ namespace tut
LLDataPackerBinaryBuffer lldp1(packbuf, cur_size);
lldp1.unpackString(unpkstr , "linden_lab_str");
- lldp1.unpackBinaryData((U8*)unpkstrBinary, unpksizeBinary, "linden_lab_bd");
+ lldp1.unpackBinaryData((U8*)unpkstrBinary, 256, unpksizeBinary, "linden_lab_bd");
lldp1.unpackBinaryDataFixed((U8*)unpkstrBinaryFixed, sizeBinaryFixed, "linden_lab_bdf");
lldp1.unpackU8(unpkvalU8,"linden_lab_u8");
lldp1.unpackU16(unpkvalU16,"linden_lab_u16");
@@ -286,7 +286,7 @@ namespace tut
LLDataPackerAsciiBuffer lldp1(packbuf, cur_size);
lldp1.unpackString(unpkstr , "linden_lab_str");
- lldp1.unpackBinaryData((U8*)unpkstrBinary, unpksizeBinary, "linden_lab_bd");
+ lldp1.unpackBinaryData((U8*)unpkstrBinary, 256, unpksizeBinary, "linden_lab_bd");
lldp1.unpackBinaryDataFixed((U8*)unpkstrBinaryFixed, sizeBinaryFixed, "linden_lab_bdf");
lldp1.unpackU8(unpkvalU8,"linden_lab_u8");
lldp1.unpackU16(unpkvalU16,"linden_lab_u16");
@@ -431,7 +431,7 @@ namespace tut
LLDataPackerAsciiFile lldp1(fp,2);
lldp1.unpackString(unpkstr , "linden_lab_str");
- lldp1.unpackBinaryData((U8*)unpkstrBinary, unpksizeBinary, "linden_lab_bd");
+ lldp1.unpackBinaryData((U8*)unpkstrBinary, 256, unpksizeBinary, "linden_lab_bd");
lldp1.unpackBinaryDataFixed((U8*)unpkstrBinaryFixed, sizeBinaryFixed, "linden_lab_bdf");
lldp1.unpackU8(unpkvalU8,"linden_lab_u8");
lldp1.unpackU16(unpkvalU16,"linden_lab_u16");
@@ -538,7 +538,7 @@ namespace tut
LLDataPackerAsciiFile lldp1(istr,2);
lldp1.unpackString(unpkstr , "linden_lab_str");
- lldp1.unpackBinaryData((U8*)unpkstrBinary, unpksizeBinary, "linden_lab_bd");
+ lldp1.unpackBinaryData((U8*)unpkstrBinary, 256, unpksizeBinary, "linden_lab_bd");
lldp1.unpackBinaryDataFixed((U8*)unpkstrBinaryFixed, sizeBinaryFixed, "linden_lab_bdf");
lldp1.unpackU8(unpkvalU8,"linden_lab_u8");
lldp1.unpackU16(unpkvalU16,"linden_lab_u16");
diff --git a/indra/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp
index 37b70964c3a..144f8078526 100644
--- a/indra/viewer_components/login/lllogin.cpp
+++ b/indra/viewer_components/login/lllogin.cpp
@@ -34,7 +34,6 @@
#include "llcoros.h"
#include "llevents.h"
-#include "lleventfilter.h"
#include "lleventcoro.h"
#include "llexception.h"
#include "stringize.h"
@@ -133,16 +132,6 @@ void LLLogin::Impl::connect(const std::string& uri, const LLSD& login_params)
LL_DEBUGS("LLLogin") << " connected with uri '" << uri << "', login_params " << login_params << LL_ENDL;
}
-namespace
-{
-// Instantiate this rendezvous point at namespace scope so it's already
-// present no matter how early the updater might post to it.
-// Use an LLEventMailDrop, which has future-like semantics: regardless of the
-// relative order in which post() or listen() are called, it delivers each
-// post() event to its listener(s) until one of them consumes that event.
-static LLEventMailDrop sSyncPoint("LoginSync");
-}
-
void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
{
LLSD printable_params = hidePasswd(login_params);
@@ -225,58 +214,7 @@ void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
}
else
{
- // Synchronize here with the updater. We synchronize here rather
- // than in the fail.login handler, which actually examines the
- // response from login.cgi, because here we are definitely in a
- // coroutine and can definitely use suspendUntilBlah(). Whoever's
- // listening for fail.login might not be.
-
- // If the reason for login failure is that we must install a
- // required update, we definitely want to pass control to the
- // updater to manage that for us. We'll handle any other login
- // failure ourselves, as usual. We figure that no matter where you
- // are in the world, or what kind of network you're on, we can
- // reasonably expect the Viewer Version Manager to respond more or
- // less as quickly as login.cgi. This synchronization is only
- // intended to smooth out minor races between the two services.
- // But what if the updater crashes? Use a timeout so that
- // eventually we'll tire of waiting for it and carry on as usual.
- // Given the above, it can be a fairly short timeout, at least
- // from a human point of view.
-
- // Since sSyncPoint is an LLEventMailDrop, we DEFINITELY want to
- // consume the posted event.
- LLCoros::OverrideConsuming oc(true);
LLSD responses(mAuthResponse["responses"]);
- LLSD updater;
-
- if (printable_params["wait_for_updater"].asBoolean())
- {
- std::string reason_response = responses["data"]["reason"].asString();
- // Timeout should produce the isUndefined() object passed here.
- if (reason_response == "update")
- {
- LL_INFOS("LLLogin") << "Login failure, waiting for sync from updater" << LL_ENDL;
- updater = llcoro::suspendUntilEventOnWithTimeout(sSyncPoint, 10, LLSD());
- }
- else
- {
- LL_DEBUGS("LLLogin") << "Login failure, waiting for sync from updater" << LL_ENDL;
- updater = llcoro::suspendUntilEventOnWithTimeout(sSyncPoint, 3, LLSD());
- }
- if (updater.isUndefined())
- {
- LL_WARNS("LLLogin") << "Failed to hear from updater, proceeding with fail.login"
- << LL_ENDL;
- }
- else
- {
- LL_DEBUGS("LLLogin") << "Got responses from updater and login.cgi" << LL_ENDL;
- }
- }
-
- // Let the fail.login handler deal with empty updater response.
- responses["updater"] = updater;
sendProgressEvent("offline", "fail.login", responses);
}
return; // Done!