From c94a118af80367c3f73cfe93559185c8f2cf3750 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Wed, 25 Feb 2026 03:00:09 +0200 Subject: #5084 Fix mac's watchdog not catching test case This happens because on mac watchdog quits at the end of frame. And events get handled outside the frame. The might be other cases that need similar handling. --- indra/llwindow/llwindowmacosx-objc.h | 2 ++ indra/newview/llappdelegate-objc.mm | 2 ++ indra/newview/llappviewermacosx.cpp | 10 ++++++++++ 3 files changed, 14 insertions(+) (limited to 'indra') diff --git a/indra/llwindow/llwindowmacosx-objc.h b/indra/llwindow/llwindowmacosx-objc.h index b302a705da..ec9afb1844 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/newview/llappdelegate-objc.mm b/indra/newview/llappdelegate-objc.mm index 0b3d0355a2..d3aac5a43e 100644 --- a/indra/newview/llappdelegate-objc.mm +++ b/indra/newview/llappdelegate-objc.mm @@ -395,11 +395,13 @@ struct AttachmentInfo - (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/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp index b074c40c17..2bd9c8661a 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()) -- cgit v1.3 From 88c4cf7da060fdd20d562519f0c067942cd5d61d Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Tue, 3 Mar 2026 18:10:42 +0200 Subject: #5426 Fix crash in LLPointer::notNull --- indra/newview/llviewertexturelist.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'indra') diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 96962bbeae..bfa662e5ef 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; -- cgit v1.3 From 52ed305d78bbad19a08a6d0c28d7a5468fdf8dca Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:49:38 +0200 Subject: #5084 Convert watchdog to a simpleton --- indra/llcommon/llwatchdog.cpp | 2 +- indra/llcommon/llwatchdog.h | 6 +++--- indra/newview/llappviewer.cpp | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 1bc1283d0b..d3242a6c96 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 fded881bb8..2100a90879 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -83,12 +83,12 @@ private: }; 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/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index bae9749772..20dba648fd 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -1240,6 +1240,7 @@ bool LLAppViewer::init() // TODO: consider moving proxy initialization here or LLCopocedureManager after proxy initialization, may be implement // some other protection to make sure we don't use network before initializng proxy + LLWatchdog::createInstance(); /*----------------------------------------------------------------------*/ // nat 2016-06-29 moved the following here from the former mainLoop(). mMainloopTimeout = new LLWatchdogTimeout("mainloop"); -- cgit v1.3 From ce139f26e0542e3ab4c2334c10044a2af9085049 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:39:47 +0200 Subject: #5486 Fix potential unpackBinaryData buffer issues --- indra/llmessage/lldatapacker.cpp | 67 +++++++++++++++++++++++++++++-------- indra/llmessage/lldatapacker.h | 8 ++--- indra/llprimitive/llprimitive.cpp | 2 +- indra/llprimitive/lltextureanim.cpp | 2 +- indra/newview/llviewerobject.cpp | 6 ++-- indra/test/lldatapacker_tut.cpp | 8 ++--- 6 files changed, 66 insertions(+), 27 deletions(-) (limited to 'indra') diff --git a/indra/llmessage/lldatapacker.cpp b/indra/llmessage/lldatapacker.cpp index e911150787..ecd0b4ee8d 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 167c102b43..5ac45356cf 100644 --- a/indra/llmessage/lldatapacker.h +++ b/indra/llmessage/lldatapacker.h @@ -49,7 +49,7 @@ public: 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 @@ public: /*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 @@ public: /*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 @@ public: /*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/llprimitive/llprimitive.cpp b/indra/llprimitive/llprimitive.cpp index c5d6076b98..7a638dd625 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 579538075a..963b7456b8 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/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index db1ef54ffa..521c6731e0 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); diff --git a/indra/test/lldatapacker_tut.cpp b/indra/test/lldatapacker_tut.cpp index 2e975ee637..5aa5233806 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"); -- cgit v1.3 From ba92b8d503d3976124b7583e878566d0107123ed Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:32:23 +0200 Subject: #5084 Fix watchdog's simpleton init It's needed for initWindow() --- indra/newview/llappviewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 20dba648fd..4bca656108 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -961,6 +961,7 @@ bool LLAppViewer::init() // Initialize event recorder LLViewerEventRecorder::createInstance(); + LLWatchdog::createInstance(); // // Initialize the window @@ -1240,7 +1241,6 @@ bool LLAppViewer::init() // TODO: consider moving proxy initialization here or LLCopocedureManager after proxy initialization, may be implement // some other protection to make sure we don't use network before initializng proxy - LLWatchdog::createInstance(); /*----------------------------------------------------------------------*/ // nat 2016-06-29 moved the following here from the former mainLoop(). mMainloopTimeout = new LLWatchdogTimeout("mainloop"); -- cgit v1.3 From bb69f7e5ac190b6783ed1a4a306ea34172b69601 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Thu, 5 Mar 2026 19:21:20 +0200 Subject: #5497 Handle a case of missing files in conversation view According to logs multiple files are missing/failed to parse, inform user about the need to reinstall --- indra/newview/llconversationview.cpp | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'indra') diff --git a/indra/newview/llconversationview.cpp b/indra/newview/llconversationview.cpp index 2297fddf0c..117bfbf64c 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"); -- cgit v1.3 From 9d985d7d3a64033077dec02f8ec35ce1518de5f4 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:14:24 +0200 Subject: #5506 Periodical stalls on media update --- indra/llplugin/llpluginclassmedia.cpp | 1 + indra/newview/llviewermedia.cpp | 34 ++++++++++++++++++++++++++++------ indra/newview/llviewermedia.h | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) (limited to 'indra') diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 77a4b08af5..88d428c233 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/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index a77b9f6103..be4961e3c4 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 1fc5bbc9e0..c840fbb72c 100644 --- a/indra/newview/llviewermedia.h +++ b/indra/newview/llviewermedia.h @@ -251,7 +251,7 @@ public: 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); -- cgit v1.3 From 85d4e8ed95f7b2f32204209d241de76045928694 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:49:20 +0200 Subject: #3612 Fix empty 'copy' message --- indra/newview/llfavoritesbar.cpp | 5 +++++ indra/newview/llinventorybridge.cpp | 5 +++++ indra/newview/llpanelplaces.cpp | 6 ++++++ indra/newview/llpanelplacestab.cpp | 6 ++++++ indra/newview/llpanelteleporthistory.cpp | 5 +++++ indra/newview/llworldmapmessage.cpp | 1 - 6 files changed, 27 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llfavoritesbar.cpp b/indra/newview/llfavoritesbar.cpp index 98b3ca820b..b3408f7861 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/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index b4cce02bda..c8ea14a11e 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/llpanelplaces.cpp b/indra/newview/llpanelplaces.cpp index 5435a79e16..5c866905d6 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 3916e34e31..e870ab7a2c 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 bfdfa68e01..0fac70dbd2 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/llworldmapmessage.cpp b/indra/newview/llworldmapmessage.cpp index a5433133ab..3264f8ae8b 100644 --- a/indra/newview/llworldmapmessage.cpp +++ b/indra/newview/llworldmapmessage.cpp @@ -34,7 +34,6 @@ #include "llfloaterworldmap.h" constexpr U32 LAYER_FLAG = 2; -constexpr S32 MAP_SIM_RETURN_NULL_SIMS = 0x00010000; //--------------------------------------------------------------------------- // World Map Message Handling -- cgit v1.3 From 7f5963be9f136e577be09062e146daa4a071226b Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:19:46 +0200 Subject: #5512 Crash on reset_login --- indra/newview/llstartup.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'indra') diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 59d97943e3..018c23963c 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -3164,14 +3164,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(); } -- cgit v1.3 From 6194e8829f274a8f6b12c6b80baa105b350bd975 Mon Sep 17 00:00:00 2001 From: trish-sl Date: Thu, 12 Mar 2026 00:56:19 -0400 Subject: Merge pull request #5411 from trish-sl/fix-highlighttransparency-alphablend0 FIX - PBR materials with alpha mode BLEND and alpha value of 0 are not highlighted --- indra/newview/llvovolume.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index b328d3a414..c12f65babf 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -5983,7 +5983,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; } -- cgit v1.3 From 53af39b68e2dbc2f89096bbd987f1d0b8a12ace5 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:56:46 +0200 Subject: #1625 Blinding white screen when changing environments --- indra/llinventory/llsettingssky.cpp | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) (limited to 'indra') diff --git a/indra/llinventory/llsettingssky.cpp b/indra/llinventory/llsettingssky.cpp index 4957cf3c02..6f7510fef2 100644 --- a/indra/llinventory/llsettingssky.cpp +++ b/indra/llinventory/llsettingssky.cpp @@ -653,14 +653,39 @@ void LLSettingsSky::blend(LLSettingsBase::ptr_t &end, F64 blendf) } } - mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_float(mHazeDensity, mLegacyHazeDensity, other->mHazeDensity, other->mLegacyHazeDensity, 0.7f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_float(mDistanceMultiplier, mLegacyDistanceMultiplier, other->mDistanceMultiplier, other->mLegacyDistanceMultiplier, 0.8f, (F32)blendf); - mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); mHasLegacyHaze |= lerp_legacy_color(mAmbientColor, mLegacyAmbientColor, other->mAmbientColor, other->mLegacyAmbientColor, LLColor3(0.25f, 0.25f, 0.25f), (F32)blendf); - mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); mHasLegacyHaze |= lerp_legacy_color(mBlueDensity, mLegacyBlueDensity, other->mBlueDensity, other->mLegacyBlueDensity, LLColor3(0.2447f, 0.4487f, 0.7599f), (F32)blendf); + if (mLegacyHazeHorizon == mLegacyDensityMultiplier == mLegacyBlueHorizon) + { + // mHazeHorizon coupled with mDensityMultiplier, mDistanceMultiplier and + // drastic blue horizon changes can result in a very bright sky during + // the transition. Purpose of this code is to calculate a 'fake level' + // and use it to even out brightness change. + // + // Example values that make lerp-based individual transition painfully bright: + // Start: 3 Haze Horiz, 0.1 Density, 6.54 Distance, white ambient, white blue horizon + // End: 0.03 Haze Horiz, 0.775 Density, 90.95 Distance, black ambient, black blue horizon + F32 strt_level = mHazeHorizon * mDensityMultiplier * mBlueHorizon.length(); + F32 end_level = other->mHazeHorizon * other->mDensityMultiplier * other->mBlueHorizon.length(); + mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); + mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); + mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); + + // lerp the fake level instead of density multiplier to avoid brightening the sky too much. + // This makes density multiplier non linear. + F32 new_level = lerp(strt_level, end_level, (F32)blendf); + mDensityMultiplier = new_level / (mHazeHorizon * mBlueHorizon.length()); + } + else + { + // default values are used, so we should lerp settings independently + mHasLegacyHaze |= lerp_legacy_float(mHazeHorizon, mLegacyHazeHorizon, other->mHazeHorizon, other->mLegacyHazeHorizon, 0.19f, (F32)blendf); + mHasLegacyHaze |= lerp_legacy_float(mDensityMultiplier, mLegacyDensityMultiplier, other->mDensityMultiplier, other->mLegacyDensityMultiplier, 0.0001f, (F32)blendf); + mHasLegacyHaze |= lerp_legacy_color(mBlueHorizon, mLegacyBlueHorizon, other->mBlueHorizon, other->mLegacyBlueHorizon, LLColor3(0.4954f, 0.4954f, 0.6399f), (F32)blendf); + } parammapping_t defaults = other->getParameterMap(); stringset_t skip = getSkipInterpolateKeys(); stringset_t slerps = getSlerpKeys(); -- cgit v1.3 From d44da708b5b5bb5d761520fc07816f0b86085bf9 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 29 Aug 2024 13:54:11 +0200 Subject: Clean up unused code --- indra/newview/llappearancemgr.cpp | 30 ------------------------------ 1 file changed, 30 deletions(-) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index f65aaccddc..961341d09d 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -708,7 +708,6 @@ public: bool pollMissingWearables(); bool isMissingCompleted(); void recoverMissingWearable(LLWearableType::EType type); - void clearCOFLinksForMissingWearables(); void onWearableAssetFetch(LLViewerWearable *wearable); void onAllComplete(); @@ -1120,20 +1119,6 @@ bool LLWearableHoldingPattern::isMissingCompleted() return mTypesToLink.size()==0 && mTypesToRecover.size()==0; } -void LLWearableHoldingPattern::clearCOFLinksForMissingWearables() -{ - for (found_list_t::iterator it = getFoundList().begin(); it != getFoundList().end(); ++it) - { - 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); - } - } -} - bool LLWearableHoldingPattern::pollMissingWearables() { if (!isMostRecent()) @@ -1164,14 +1149,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 +1195,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)); -- cgit v1.3 From 8b0d042ed9fc58b8e346eb813374549ec71db704 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 29 Aug 2024 14:49:59 +0200 Subject: LLViewerObject::getAttachmentItemID() sometimes returns the NULL UUID for the avatar's own attachments --- indra/newview/llviewerjointattachment.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llviewerjointattachment.cpp b/indra/newview/llviewerjointattachment.cpp index 511fac9788..cad96afd98 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())) -- cgit v1.3 From ac0c734aaf7390118b68a42496b357d074586c01 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 29 Aug 2024 14:54:56 +0200 Subject: LLAppearanceMgr::updateAppearanceFromCOF() doesn't properly filter items collected from folder links --- indra/newview/llappearancemgr.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index 961341d09d..ef72b1ab6a 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -2606,6 +2606,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); dumpItemArray(wear_items,"asset_dump: wear_item"); dumpItemArray(obj_items,"asset_dump: obj_item"); -- cgit v1.3 From a7512cfc9c7fdf7b3a24f09bc1b84878aa188ffb Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 29 Aug 2024 20:42:02 +0200 Subject: Resolve race condition when removing/add attachments inside of a LLWearableHoldingPattern (see PR for details) --- indra/newview/llappearancemgr.cpp | 103 ++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 53 deletions(-) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index ef72b1ab6a..e71325e9cf 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -716,7 +716,6 @@ public: 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(); @@ -726,7 +725,6 @@ public: 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; @@ -803,11 +801,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; @@ -913,55 +906,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()) @@ -2622,6 +2570,56 @@ 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()) + { + 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. + 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"); @@ -2636,7 +2634,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 -- cgit v1.3 From b2a22e1a9bfe93d72503492f4d6096567e164937 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 29 Aug 2024 20:51:00 +0200 Subject: Resolve issues tracking worn attachments that aren't in COF yet (see PR for more details) --- indra/newview/llappearancemgr.cpp | 24 ++++++- indra/newview/llappearancemgr.h | 1 + indra/newview/llattachmentsmgr.cpp | 143 +++++++++++++++---------------------- indra/newview/llattachmentsmgr.h | 12 ++-- indra/newview/llvoavatar.cpp | 2 +- 5 files changed, 89 insertions(+), 93 deletions(-) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index e71325e9cf..4bc9c55019 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -2574,6 +2574,23 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions, // 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; @@ -2600,7 +2617,11 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions, } // Take off the attachments that will no longer be in the outfit. - LLAgentWearables::userRemoveMultipleAttachments(objects_to_remove); + // (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. @@ -4173,6 +4194,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 131b6817ed..46028abb3e 100644 --- a/indra/newview/llappearancemgr.h +++ b/indra/newview/llappearancemgr.h @@ -151,6 +151,7 @@ public: // 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. diff --git a/indra/newview/llattachmentsmgr.cpp b/indra/newview/llattachmentsmgr.cpp index 8b5db2c0fa..e27d6fb1f5 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) { } @@ -113,8 +122,6 @@ void LLAttachmentsMgr::onIdle() expireOldDetachRequests(); - checkInvalidCOFLinks(); - spamStatusInfo(); } @@ -222,6 +229,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 +280,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 +451,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 +474,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 +482,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 2428acfb38..4ccd4d3224 100644 --- a/indra/newview/llattachmentsmgr.h +++ b/indra/newview/llattachmentsmgr.h @@ -85,8 +85,14 @@ public: 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 @@ private: 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 @@ private: // 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/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index efb09479e2..530fb71f6b 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -8588,7 +8588,7 @@ bool LLVOAvatar::processFullyLoadedChange(bool loading) bool LLVOAvatar::isFullyLoaded() const { - return (mRenderUnloadedAvatar || mFullyLoaded); + return (mRenderUnloadedAvatar && !isSelf()) || mFullyLoaded; } bool LLVOAvatar::hasFirstFullAttachmentData() const -- cgit v1.3 From 6c6d618f6db3941e95223a49f1daf8f6a05346a2 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Fri, 30 Aug 2024 01:03:23 +0200 Subject: If multiple LLWearableHoldingPattern instances are waiting for completion, only the most recently created one should progress --- indra/newview/llappearancemgr.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index 4bc9c55019..5845477554 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -703,6 +703,7 @@ public: void onFetchCompletion(); bool isFetchCompleted(); bool isTimedOut(); + bool pollStopped(); void checkMissingWearables(); bool pollMissingWearables(); @@ -947,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(); @@ -1017,6 +1022,9 @@ void recovered_item_cb(const LLUUID& item_id, LLWearableType::EType type, LLView { // 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; @@ -1067,12 +1075,27 @@ bool LLWearableHoldingPattern::isMissingCompleted() return mTypesToLink.size()==0 && mTypesToRecover.size()==0; } +bool LLWearableHoldingPattern::pollStopped() +{ + // We have to keep on polling until we're sure that all callbacks have completed or they'll cause a crash + if (isFetchCompleted() && isMissingCompleted()) + { + delete this; + return true; + } + return false; +} + bool LLWearableHoldingPattern::pollMissingWearables() { if (!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 + doOnIdleRepeating(boost::bind(&LLWearableHoldingPattern::pollStopped, this)); + return true; } bool timed_out = isTimedOut(); -- cgit v1.3 From f6d0b361d5b79ef023506f95e4ed6f8f2ebdc1dd Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 30 Jan 2025 00:10:33 +0100 Subject: Accurately reflect the worn status of inventory items (the reasons for why this was needed have been resolved in previous commits) --- indra/newview/llinventoryfunctions.cpp | 6 ------ 1 file changed, 6 deletions(-) (limited to 'indra') diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp index 7522ea4907..a6fda946ef 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: -- cgit v1.3 From 5bc81c9decd000d0218d80319f1fa307c0acb7f4 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Thu, 30 Jan 2025 00:13:12 +0100 Subject: Attaching a rezzed object incorrectly delays attachment link creation by 5 seconds --- indra/newview/llattachmentsmgr.cpp | 3 +++ indra/newview/llselectmgr.cpp | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llattachmentsmgr.cpp b/indra/newview/llattachmentsmgr.cpp index e27d6fb1f5..f73d4241c9 100644 --- a/indra/newview/llattachmentsmgr.cpp +++ b/indra/newview/llattachmentsmgr.cpp @@ -89,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; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index 415e6cfa72..1b64eab3c0 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 -- cgit v1.3 From 4034714980dd36a454639b1324cfac6a6bd8bf59 Mon Sep 17 00:00:00 2001 From: Kitty Barnett Date: Sat, 28 Feb 2026 20:18:47 +0100 Subject: [FIXED] Wearing >56 clothing items will incorrectly filter since body parts are passed in as well --- indra/newview/llappearancemgr.cpp | 18 +++++++++++++----- indra/newview/llappearancemgr.h | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index 5845477554..8aa28e1b09 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -2084,15 +2084,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; } @@ -2581,7 +2589,7 @@ void LLAppearanceMgr::updateAppearanceFromCOF(bool enforce_item_restrictions, removeDuplicateItems(wear_items); removeDuplicateItems(obj_items); removeDuplicateItems(gest_items); - filterWearableItems(wear_items, 0, LLAgentWearables::MAX_CLOTHING_LAYERS); + filterWearableItems(wear_items, 0, LLAgentWearables::MAX_CLOTHING_LAYERS, true); dumpItemArray(wear_items,"asset_dump: wear_item"); dumpItemArray(obj_items,"asset_dump: obj_item"); diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h index 46028abb3e..9d90be0004 100644 --- a/indra/newview/llappearancemgr.h +++ b/indra/newview/llappearancemgr.h @@ -250,7 +250,7 @@ private: 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, -- cgit v1.3 From 75a98ba9aa932d76227d50e739ae4beb722a3cc6 Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Mon, 16 Mar 2026 17:45:55 +0200 Subject: #5541 Add safety checks for inventory save during shutdown --- indra/newview/llinventorymodel.cpp | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index c2f9c483c0..187b2248de 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); -- cgit v1.3 From a5a6cfb995e9baed20980a67dfb89cacc6fe053c Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:59:56 +0200 Subject: viewer-private#540 Fix freeze when opening large profiles #1 --- indra/llui/lltextbase.cpp | 41 +++++++++++++++++++++++++++++++++++------ indra/llui/lltextbase.h | 1 + 2 files changed, 36 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 24ae5c09e9..b53b5bd7a4 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()); @@ -1092,7 +1096,10 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s insert_len = getLength() - old_len; } - onValueChange(pos, pos + insert_len); + if (mTrackValueChange) + { + onValueChange(pos, pos + insert_len); + } needsReflow(pos); return insert_len; @@ -1108,7 +1115,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 +1169,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 +1180,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 +1191,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; @@ -2330,6 +2349,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 +2373,7 @@ void LLTextBase::setText(const LLStringExplicit &utf8str, const LLStyle::Params& startOfDoc(); } + mTrackValueChange = true; onValueChange(0, getLength()); } @@ -2553,6 +2577,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 +2595,7 @@ void LLTextBase::copyContents(const LLTextBase* source) getViewModel()->setDisplay(source->getViewModel()->getDisplay()); + mTrackValueChange = true; onValueChange(0, getLength()); needsReflow(); } diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index 3ab5e905e3..21134fd7c9 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -761,6 +761,7 @@ protected: 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 -- cgit v1.3 From dd4f5cf13dbf9ec463bee2ce25e652786e5fb370 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:53:13 +0200 Subject: viewer-private#540 Speed up url search #2 --- indra/llui/llurlregistry.cpp | 77 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp index cb101d325d..8a21222b3c 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 Date: Tue, 17 Mar 2026 01:15:29 +0200 Subject: viewer-private#540 Avoid micro truncations #3 --- indra/llui/lltextbase.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'indra') diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index b53b5bd7a4..c084b400f6 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -1091,13 +1091,12 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s getViewModel()->getEditableDisplay().insert(pos, wstr); - if ( truncate() ) - { - insert_len = getLength() - old_len; - } - if (mTrackValueChange) { + if (truncate()) + { + insert_len = getLength() - old_len; + } onValueChange(pos, pos + insert_len); } needsReflow(pos); @@ -2373,6 +2372,7 @@ 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()); } @@ -2404,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). { @@ -2595,6 +2599,7 @@ 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(); -- cgit v1.3 From 0aa33a144d392c16eaa419ffefa7c98af2e37834 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Thu, 19 Mar 2026 18:51:14 +0200 Subject: #5454 fix not showing Friend requests during start up notification --- indra/newview/llchannelmanager.cpp | 1 + indra/newview/llchannelmanager.h | 3 +++ indra/newview/llimprocessing.cpp | 1 + indra/newview/llnotificationofferhandler.cpp | 7 +++++++ 4 files changed, 12 insertions(+) (limited to 'indra') diff --git a/indra/newview/llchannelmanager.cpp b/indra/newview/llchannelmanager.cpp index 454991ab83..27e5263967 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 4db7f32b10..75b3060e66 100644 --- a/indra/newview/llchannelmanager.h +++ b/indra/newview/llchannelmanager.h @@ -104,12 +104,15 @@ public: */ 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/llimprocessing.cpp b/indra/newview/llimprocessing.cpp index 779ed725ac..b1e42e11fb 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); } } diff --git a/indra/newview/llnotificationofferhandler.cpp b/indra/newview/llnotificationofferhandler.cpp index b65da28bda..27ef28f32a 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); + } } -- cgit v1.3 From 166fa85705bd7501a1af81d4b529fa4b83038be7 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:59:22 +0200 Subject: #5543 PBR support for pipette --- indra/newview/llfloatercolorpicker.cpp | 16 +++-- indra/newview/llfloatercolorpicker.h | 3 +- indra/newview/lltexturectrl.cpp | 96 ++++++++++++++++++++------ indra/newview/lltexturectrl.h | 4 +- indra/newview/lltoolpipette.cpp | 21 ++---- indra/newview/lltoolpipette.h | 7 +- indra/newview/skins/default/xui/en/strings.xml | 1 + 7 files changed, 101 insertions(+), 47 deletions(-) (limited to 'indra') diff --git a/indra/newview/llfloatercolorpicker.cpp b/indra/newview/llfloatercolorpicker.cpp index cd45093856..56e086502a 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 5c27fffd08..af199d92a3 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/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp index 52ec8c17c1..f0655901f7 100644 --- a/indra/newview/lltexturectrl.cpp +++ b/indra/newview/lltexturectrl.cpp @@ -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); @@ -1640,39 +1643,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 e0060474ce..759711ebf7 100644 --- a/indra/newview/lltexturectrl.h +++ b/indra/newview/lltexturectrl.h @@ -374,7 +374,7 @@ public: 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 @@ private: 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/lltoolpipette.cpp b/indra/newview/lltoolpipette.cpp index 9e3d435688..6ec25e9512 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 6c79674d76..7771f467e5 100644 --- a/indra/newview/lltoolpipette.h +++ b/indra/newview/lltoolpipette.h @@ -51,16 +51,13 @@ public: 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/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 1860d38b0e..e022f29b76 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -2333,6 +2333,7 @@ For AI Character: Get the closest navigable point to the point provided. 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 -- cgit v1.3 From 69cbb4869e2804d0cd063eaa1d352824e13604fa Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:30:15 +0200 Subject: #5543 Clear pipette button's visibility logic for materials Removed the limitation as pipette now works for pbr materials. Logic wasn't working correctly as pipette stayed visible regardless of state. --- indra/newview/lltexturectrl.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) (limited to 'indra') diff --git a/indra/newview/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp index f0655901f7..001c4a0449 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(); } @@ -1072,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) { @@ -1422,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) @@ -1563,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()) { -- cgit v1.3 From e9b39bf4cde6e8db7ee4475995074a5c06d01007 Mon Sep 17 00:00:00 2001 From: Hecklezz Date: Thu, 26 Feb 2026 17:45:22 +1000 Subject: Improve detection of marketplace urls for the marketplace floater --- indra/newview/llfloatermarketplace.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llfloatermarketplace.cpp b/indra/newview/llfloatermarketplace.cpp index bb2378d864..a80546595c 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); } -- cgit v1.3 From 689123f10a6ddf1b8de5f88c40d23b3a57c3f3f9 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:53:21 +0200 Subject: #5558 Adjust map request size dynamically --- indra/newview/llworldmap.cpp | 102 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 12 deletions(-) (limited to 'indra') diff --git a/indra/newview/llworldmap.cpp b/indra/newview/llworldmap.cpp index 1db36649fa..798462748d 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_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 < 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; } } -- cgit v1.3 From 0084a93bc4019be2e1ba9aae715a2e927981900b Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:09:17 +0200 Subject: #5558 Don't spam server with requests we don't need Animation takes less than a second, regions take over a second to load. Don't request transitionarry regions, got for target. --- indra/newview/llworldmap.cpp | 4 ++-- indra/newview/llworldmapview.cpp | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/newview/llworldmap.cpp b/indra/newview/llworldmap.cpp index 798462748d..c7c8404eb9 100644 --- a/indra/newview/llworldmap.cpp +++ b/indra/newview/llworldmap.cpp @@ -623,7 +623,7 @@ void LLWorldMap::updateRegions(S32 x0, S32 y0, S32 x1, S32 y1) 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_BLOCKS + // 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_x = block_x0; block_x <= block_x1; ) @@ -651,7 +651,7 @@ void LLWorldMap::updateRegions(S32 x0, S32 y0, S32 x1, S32 y1) // Expand height (check vertical contiguous unloaded blocks) while (request_height < MAX_BLOCKS_PER_SIDE && (block_y + request_height) <= block_y1 && - (request_width * request_height < MAX_TOTAL_BLOCKS)) // Don't exceed 64 total regions + (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 diff --git a/indra/newview/llworldmapview.cpp b/indra/newview/llworldmapview.cpp index 1be6a6cfff..8e7b86eb00 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; -- cgit v1.3 From b7fc5b2a9ec79f10a97fdb7b80f6eeb9cd65e0e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:46:58 +0000 Subject: Extend sendMapBlockRequest stub and add test<4> for request-size assertions Co-authored-by: akleshchev <117672381+akleshchev@users.noreply.github.com> Agent-Logs-Url: https://github.com/secondlife/viewer/sessions/fb89a385-185d-4179-a3a4-39a45a713f4f --- indra/newview/tests/llworldmap_test.cpp | 73 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/tests/llworldmap_test.cpp b/indra/newview/tests/llworldmap_test.cpp index 60172b3960..692dd5638c 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); + } } -- cgit v1.3 From 5a7a5272a07935aae544767abc700e8fabf3cbbd Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:46:38 +0200 Subject: #4298 Handle various out of memory cases --- indra/llrender/llfontfreetype.cpp | 43 +++++++++++++++++++++++--------------- indra/llrender/llshadermgr.cpp | 19 +++++++++++++++-- indra/newview/lldebugview.cpp | 4 ---- indra/newview/lldebugview.h | 1 - indra/newview/llinventorymodel.cpp | 6 ++++++ 5 files changed, 49 insertions(+), 24 deletions(-) (limited to 'indra') diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index d37b16ce0c..d9857a7ad5 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/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 2c35a6acae..b8545b3ed9 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/newview/lldebugview.cpp b/indra/newview/lldebugview.cpp index 53da9826ed..3941b82e75 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 8fa2acc3c9..ca274d6f91 100644 --- a/indra/newview/lldebugview.h +++ b/indra/newview/lldebugview.h @@ -59,7 +59,6 @@ public: void setStatsVisible(bool visible); - LLFastTimerView* mFastTimerView; LLConsole* mDebugConsolep; LLView* mFloaterSnapRegion; }; diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 187b2248de..8b9aad4641 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -3589,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(""); -- cgit v1.3 From 83c3a3c15d55d932b56a06de7e50ec2de2442fae Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Mon, 30 Mar 2026 19:42:27 +0300 Subject: #5585 fix the issue with ptt state when joining p2p call while in voice-disabled parcel --- indra/newview/llvoicechannel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoicechannel.cpp b/indra/newview/llvoicechannel.cpp index b941d356a1..0852258994 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; } -- cgit v1.3 From 9846a1c2cb11bf4252ba262f88067f22c57137b3 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:40:32 +0300 Subject: #3420 CreateLandmarkForPosition floater fails to save descriptions Description field isn't supposed to hold newline --- indra/newview/skins/default/xui/en/floater_create_landmark.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') 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 abe8344097..6d3da72962 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" -- cgit v1.3 From 136eb7afcf22ea20344f22b1d6c46a6a207a360b Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:59:45 +0200 Subject: #5579 Separate parts of calculateUpdateRenderComplexity Cleanup of the function, since these parts should remain unchanged. --- indra/newview/llvoavatar.cpp | 128 +++++++++++++++++++++++-------------------- indra/newview/llvoavatar.h | 4 +- 2 files changed, 73 insertions(+), 59 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 530fb71f6b..74fcae9849 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -11151,6 +11151,74 @@ void LLVOAvatar::updateVisualComplexity() } +U32 LLVOAvatar::calculateBodyPartsComplexity() +{ + static const 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(hud_complexity_list_t hud_complexity_list, 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 // object. @@ -11317,7 +11385,6 @@ void LLVOAvatar::calculateUpdateRenderComplexity() { 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); @@ -11330,33 +11397,7 @@ void LLVOAvatar::calculateUpdateRenderComplexity() 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; + cost += calculateBodyPartsComplexity(); mAttachmentVisibleTriangleCount = 0; mAttachmentEstTriangleCount = 0.f; @@ -11409,36 +11450,7 @@ void LLVOAvatar::calculateUpdateRenderComplexity() 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); - } - }); - } + processComplexityCostChange(hud_complexity_list, object_complexity_list); } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index fc3a97a25d..0e719bbd6f 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -594,6 +594,9 @@ private: VisualMuteSettings mVisuallyMuteSetting; // Always or never visually mute this AV + U32 calculateBodyPartsComplexity(); + + void processComplexityCostChange(hud_complexity_list_t hud_complexity_list, object_complexity_list_t object_complexity_list); //-------------------------------------------------------------------- // animated object status //-------------------------------------------------------------------- @@ -630,7 +633,6 @@ private: // Shadowing //-------------------------------------------------------------------- public: - void updateShadowFaces(); LLDrawable* mShadow; private: LLFace* mShadow0Facep; -- cgit v1.3 From c7576cddf3e548a7a6e63776da5674b7cb31297f Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:01:18 +0200 Subject: #5579 Implement partial complexity calculations --- indra/newview/llvoavatar.cpp | 473 +++++++++++++++++++++++++++++++++++++++++-- indra/newview/llvoavatar.h | 95 ++++++++- indra/newview/llvovolume.cpp | 32 ++- 3 files changed, 580 insertions(+), 20 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 74fcae9849..c3bc51ad12 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -682,6 +682,7 @@ LLVOAvatar::LLVOAvatar(const LLUUID& id, mUpdatePeriod(1), mOverallAppearance(AOA_INVISIBLE), mVisualComplexityStale(true), + mLastFullComplexityUpdate(0.0), mVisuallyMuteSetting(AV_RENDER_NORMALLY), mMutedAVColor(LLColor4::white /* used for "uninitialize" */), mFirstFullyVisible(true), @@ -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()); 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(); } } @@ -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,432 @@ 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 data and control avatar. mVisualComplexityStale = true; } +// Constants for body part costs +static const F32 CACHE_LIFETIME_SECONDS = 30.0; // Todo: should be indefinite, until something actually changes + +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; + } + + // Reuse the existing accountRenderComplexityForObject logic + // This ensures compatibility with the legacy implementation + hud_complexity_list_t attachment_hud_list; + object_complexity_list_t attachment_object_list; + cache.render_cost = 0; + cache.textures.clear(); + cache.triangle_count = 0; + cache.est_triangle_count = 0; + cache.surface_area = 0; + + accountRenderComplexityForObject( + attached_object, + max_attachment_complexity, + cache.textures, + cache.render_cost, + cache.triangle_count, + cache.est_triangle_count, + cache.surface_area, + attachment_hud_list, + attachment_object_list + ); + + // Store HUD/object complexity info for later reporting + if (!attachment_hud_list.empty()) + { + cache.hud_complexity = attachment_hud_list.front(); + } + if (!attachment_object_list.empty()) + { + cache.object_complexity = attachment_object_list.front(); + } + + 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; + } + + 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(); + + // For control avatars (animated objects), we need to account for + // the skeleton and animation system overhead + 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; + } + + hud_complexity_list_t hud_list; + object_complexity_list_t object_list; + cache.render_cost = 0; + cache.textures.clear(); + cache.triangle_count = 0; + cache.est_triangle_count = 0; + cache.surface_area = 0; + + accountRenderComplexityForObject( + volp, + max_attachment_complexity, + cache.textures, + cache.render_cost, + cache.triangle_count, + cache.est_triangle_count, + cache.surface_area, + hud_list, + object_list + ); + + // Store HUD/object complexity info for later reporting + if (!hud_list.empty()) + { + cache.hud_complexity = hud_list.front(); + } + if (!object_list.empty()) + { + cache.object_complexity = object_list.front(); + } + + cache.last_update_time = LLFrameTimer::getTotalSeconds(); + cache.needs_update = false; + + return true; +} + +void LLVOAvatar::accumulateComplexityComponent(const ComplexityComponent& component, + U32& total_cost, + LLVOVolume::texture_cost_t& all_textures, + 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; + + // Merge textures (avoid double-counting) + all_textures.insert(component.textures.begin(), component.textures.end()); + + // 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) +{ + mDirtyComplexityAttachments.insert(object_id); + + // Also mark the cache entry if it exists + complexity_cache_map_t::iterator it = mComplexityCache.find(object_id); + if (it != mComplexityCache.end()) + { + it->second.needs_update = true; + } + + // Launch update process if not already scheduled + mVisualComplexityStale = true; +} + +void LLVOAvatar::markAllComplexityDirty() +{ + for (complexity_cache_map_t::iterator it = mComplexityCache.begin(); + it != mComplexityCache.end(); ++it) + { + it->second.needs_update = true; + mDirtyComplexityAttachments.insert(it->first); + } + mBodyPartsComplexity.needs_update = true; + mControlAvatarComplexity.needs_update = true; + + // Launch update process if not already scheduled + mVisualComplexityStale = true; +} + +void LLVOAvatar::markBodyPartsComplexityDirty() +{ + mBodyPartsComplexity.needs_update = true; + + // Launch update process if not already scheduled + mVisualComplexityStale = true; +} + +void LLVOAvatar::updateDirtyAttachmentComplexity(const F32 max_attachment_complexity) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; + + if (mDirtyComplexityAttachments.empty()) + { + return; + } + + // Update only attachments marked as dirty + for (std::set::iterator dirty_it = mDirtyComplexityAttachments.begin(); + dirty_it != mDirtyComplexityAttachments.end(); ++dirty_it) + { + const LLUUID& object_id = *dirty_it; + + // Find the actual attachment object + LLViewerObject* attached_obj = gObjectList.findObject(object_id); + if (attached_obj && !attached_obj->isDead() && attached_obj->getAttachmentItemID().notNull()) + { + ComplexityComponent& cache = mComplexityCache[object_id]; + calculateAttachmentComplexity(attached_obj, max_attachment_complexity, cache); + } + else + { + // Object no longer exists, remove from cache + mComplexityCache.erase(object_id); + } + } + + mDirtyComplexityAttachments.clear(); +} + +void LLVOAvatar::performFullComplexityUpdate(const F32 max_attachment_complexity) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; + + // Clear the cache and recalculate everything + mComplexityCache.clear(); + mDirtyComplexityAttachments.clear(); + + // Recalculate all attachments + 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]; + calculateAttachmentComplexity(attached_object, max_attachment_complexity, cache); + } + } + } + + // Recalculate body parts + calculateBodyPartsComplexity(mBodyPartsComplexity); + + mLastFullComplexityUpdate = LLFrameTimer::getTotalSeconds(); +} + +void LLVOAvatar::performPartialComplexityUpdate(const F32 max_attachment_complexity) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; + + // Update any attachments marked as dirty + updateDirtyAttachmentComplexity(max_attachment_complexity); + + // Update stale cache entries + 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 + if (shouldUpdateComplexityComponent(cache)) + { + calculateAttachmentComplexity(attached_object, max_attachment_complexity, cache); + } + } + } + } + + // Update body parts if stale + if (shouldUpdateComplexityComponent(mBodyPartsComplexity)) + { + calculateBodyPartsComplexity(mBodyPartsComplexity); + } +} + +// Calculations for mVisualComplexity value +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); + + // Reset per-run counters + mAttachmentSurfaceArea = 0.f; + mAttachmentVisibleTriangleCount = 0; + mAttachmentEstTriangleCount = 0.f; + + U32 total_cost = 0; + LLVOVolume::texture_cost_t all_textures; // Not currently in use. Perhaps add cost from unique textures? + hud_complexity_list_t hud_complexity_list; + object_complexity_list_t object_complexity_list; + + // Determine update strategy + F64 current_time = LLFrameTimer::getTotalSeconds(); + + // Todo: check if it changes anything, then get rid of FULL_UPDATE_INTERVAL, + // full recalculation can take over 10ms for very complex avatars, + // thus is not practical to do. Leave only partial updates. + constexpr F64 FULL_UPDATE_INTERVAL = 60.0; // seconds + bool do_full_update = (current_time - mLastFullComplexityUpdate) > FULL_UPDATE_INTERVAL; + + // Perform update based on strategy + if (do_full_update) + { + // Expensive! Todo: don't do. + performFullComplexityUpdate(max_attachment_complexity); + } + else + { + performPartialComplexityUpdate(max_attachment_complexity); + } + + // 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, + all_textures, + hud_complexity_list, + object_complexity_list); + } + + // Accumulate body parts complexity + accumulateComplexityComponent(mBodyPartsComplexity, total_cost, + all_textures, 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()) + { + to_remove.push_back(object_id); + continue; + } + + // Accumulate this attachment's complexity + accumulateComplexityComponent(cache_iter->second, total_cost, + all_textures, 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); + } + + // 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() { @@ -11227,15 +11657,18 @@ void LLVOAvatar::accountRenderComplexityForObject( const F32 max_attachment_complexity, LLVOVolume::texture_cost_t& textures, U32& cost, + U32& visible_triangle_count, + F32& est_triangle_count, + F32& surface_area, hud_complexity_list_t& hud_complexity_list, object_complexity_list_t& object_complexity_list) { 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; @@ -11306,7 +11739,7 @@ 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) @@ -11373,7 +11806,7 @@ void LLVOAvatar::accountRenderComplexityForObject( } // Calculations for mVisualComplexity value -void LLVOAvatar::calculateUpdateRenderComplexity() +void LLVOAvatar::calculateUpdateRenderComplexityLegacy() { /***************************************************************** * This calculation should not be modified by third party viewers, @@ -11412,8 +11845,16 @@ void LLVOAvatar::calculateUpdateRenderComplexity() LLVOVolume *volp = control_av->mRootVolp; if (volp && !volp->isAttachment()) { - accountRenderComplexityForObject(volp, max_attachment_complexity, - textures, cost, hud_complexity_list, object_complexity_list); + accountRenderComplexityForObject( + volp, + max_attachment_complexity, + textures, + cost, + mAttachmentVisibleTriangleCount, + mAttachmentEstTriangleCount, + mAttachmentSurfaceArea, + hud_complexity_list, + object_complexity_list); } } @@ -11428,8 +11869,16 @@ void LLVOAvatar::calculateUpdateRenderComplexity() ++attachment_iter) { LLViewerObject* attached_object = attachment_iter->get(); - accountRenderComplexityForObject(attached_object, max_attachment_complexity, - textures, cost, hud_complexity_list, object_complexity_list); + accountRenderComplexityForObject( + attached_object, + max_attachment_complexity, + textures, + cost, + mAttachmentVisibleTriangleCount, + mAttachmentEstTriangleCount, + mAttachmentSurfaceArea, + hud_complexity_list, + object_complexity_list); } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 0e719bbd6f..79a03527fb 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -302,11 +302,19 @@ public: const F32 max_attachment_complexity, LLVOVolume::texture_cost_t& textures, U32& cost, + U32& visible_triangle_count, + F32& est_triangle_count, + F32& surface_area, hud_complexity_list_t& hud_complexity_list, object_complexity_list_t& object_complexity_list); void calculateUpdateRenderComplexity(); + void calculateUpdateRenderComplexityLegacy(); static const U32 VISUAL_COMPLEXITY_UNKNOWN; void updateVisualComplexity(); + // Mark that an attachment needs complexity recalculation + void markAttachmentComplexityDirty(const LLUUID& object_id); + void markAllComplexityDirty(); + void markBodyPartsComplexityDirty(); void placeProfileQuery(); void readProfileQuery(S32 retries); @@ -581,12 +589,6 @@ private: // 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,9 +596,90 @@ private: 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, + LLVOVolume::texture_cost_t& all_textures, + hud_complexity_list_t& hud_list, + object_complexity_list_t& object_list); + + bool shouldUpdateComplexityComponent(const ComplexityComponent& component) const; + void updateDirtyAttachmentComplexity(const F32 max_attachment_complexity); + void performFullComplexityUpdate(const F32 max_attachment_complexity); + void performPartialComplexityUpdate(const F32 max_attachment_complexity); + void processComplexityCostChange(hud_complexity_list_t hud_complexity_list, 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 + F64 mLastFullComplexityUpdate; + + // Attachments marked for update, + // 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. + std::set mDirtyComplexityAttachments; + + // 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 //-------------------------------------------------------------------- diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index c12f65babf..1cb6d2ecc5 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 + // markCotrolAvatarComplexityDirty() 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() -- cgit v1.3 From e2d8ace323fa4980a6a6e12f4585c2b90f388934 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 27 Mar 2026 01:06:27 +0200 Subject: #5579 Cleanup --- indra/newview/llvoavatar.cpp | 337 ++++++++----------------------------------- indra/newview/llvoavatar.h | 19 +-- indra/newview/llvovolume.cpp | 4 +- 3 files changed, 65 insertions(+), 295 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index c3bc51ad12..bb152ced40 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -682,7 +682,6 @@ LLVOAvatar::LLVOAvatar(const LLUUID& id, mUpdatePeriod(1), mOverallAppearance(AOA_INVISIBLE), mVisualComplexityStale(true), - mLastFullComplexityUpdate(0.0), mVisuallyMuteSetting(AV_RENDER_NORMALLY), mMutedAVColor(LLColor4::white /* used for "uninitialize" */), mFirstFullyVisible(true), @@ -7872,7 +7871,7 @@ bool LLVOAvatar::detachObject(LLViewerObject *viewer_object) if (attachment->isObjectAttached(viewer_object)) { - markAttachmentComplexityDirty(viewer_object->getID()); + markAttachmentComplexityDirty(viewer_object->getID(), true); bool is_animated_object = viewer_object->isAnimatedObject(); cleanupAttachedMesh(viewer_object); @@ -11155,13 +11154,10 @@ void LLVOAvatar::updateVisualComplexity() { LL_DEBUGS("AvatarRender") << "avatar " << getID() << " appearance changed" << LL_ENDL; // Trigger cache recalculation on next idle update. - // Will recalculate stale data and control avatar. + // Will recalculate stale, missing data and control avatar. mVisualComplexityStale = true; } -// Constants for body part costs -static const F32 CACHE_LIFETIME_SECONDS = 30.0; // Todo: should be indefinite, until something actually changes - void LLVOAvatar::calculateAttachmentComplexity(LLViewerObject* attached_object, const F32 max_attachment_complexity, ComplexityComponent& cache) @@ -11170,21 +11166,12 @@ void LLVOAvatar::calculateAttachmentComplexity(LLViewerObject* attached_object, cache.reset(); - if (!attached_object || attached_object->isDead()) + if (!attached_object + || attached_object->isDead()) { return; } - // Reuse the existing accountRenderComplexityForObject logic - // This ensures compatibility with the legacy implementation - hud_complexity_list_t attachment_hud_list; - object_complexity_list_t attachment_object_list; - cache.render_cost = 0; - cache.textures.clear(); - cache.triangle_count = 0; - cache.est_triangle_count = 0; - cache.surface_area = 0; - accountRenderComplexityForObject( attached_object, max_attachment_complexity, @@ -11193,20 +11180,10 @@ void LLVOAvatar::calculateAttachmentComplexity(LLViewerObject* attached_object, cache.triangle_count, cache.est_triangle_count, cache.surface_area, - attachment_hud_list, - attachment_object_list + cache.hud_complexity, + cache.object_complexity ); - // Store HUD/object complexity info for later reporting - if (!attachment_hud_list.empty()) - { - cache.hud_complexity = attachment_hud_list.front(); - } - if (!attachment_object_list.empty()) - { - cache.object_complexity = attachment_object_list.front(); - } - cache.last_update_time = LLFrameTimer::getTotalSeconds(); cache.needs_update = false; } @@ -11235,6 +11212,7 @@ bool LLVOAvatar::shouldUpdateComplexityComponent(const ComplexityComponent& comp 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; } @@ -11245,8 +11223,6 @@ bool LLVOAvatar::calculateControlAvatarComplexity(ComplexityComponent& cache, co cache.reset(); - // For control avatars (animated objects), we need to account for - // the skeleton and animation system overhead if (!isControlAvatar()) { return false; @@ -11264,14 +11240,6 @@ bool LLVOAvatar::calculateControlAvatarComplexity(ComplexityComponent& cache, co return false; } - hud_complexity_list_t hud_list; - object_complexity_list_t object_list; - cache.render_cost = 0; - cache.textures.clear(); - cache.triangle_count = 0; - cache.est_triangle_count = 0; - cache.surface_area = 0; - accountRenderComplexityForObject( volp, max_attachment_complexity, @@ -11280,20 +11248,11 @@ bool LLVOAvatar::calculateControlAvatarComplexity(ComplexityComponent& cache, co cache.triangle_count, cache.est_triangle_count, cache.surface_area, - hud_list, - object_list + cache.hud_complexity, + cache.object_complexity ); - // Store HUD/object complexity info for later reporting - if (!hud_list.empty()) - { - cache.hud_complexity = hud_list.front(); - } - if (!object_list.empty()) - { - cache.object_complexity = object_list.front(); - } - + // todo: store 'expires' time instead or make it indefinite? cache.last_update_time = LLFrameTimer::getTotalSeconds(); cache.needs_update = false; @@ -11302,7 +11261,6 @@ bool LLVOAvatar::calculateControlAvatarComplexity(ComplexityComponent& cache, co void LLVOAvatar::accumulateComplexityComponent(const ComplexityComponent& component, U32& total_cost, - LLVOVolume::texture_cost_t& all_textures, hud_complexity_list_t& hud_list, object_complexity_list_t& object_list) { @@ -11311,9 +11269,6 @@ void LLVOAvatar::accumulateComplexityComponent(const ComplexityComponent& compon mAttachmentVisibleTriangleCount += component.triangle_count; mAttachmentEstTriangleCount += component.est_triangle_count; - // Merge textures (avoid double-counting) - all_textures.insert(component.textures.begin(), component.textures.end()); - // Add HUD/object complexity info if present if (component.hud_complexity.objectId.notNull()) { @@ -11325,33 +11280,23 @@ void LLVOAvatar::accumulateComplexityComponent(const ComplexityComponent& compon } } -void LLVOAvatar::markAttachmentComplexityDirty(const LLUUID& object_id) +void LLVOAvatar::markAttachmentComplexityDirty(const LLUUID& object_id, bool force_reset_attachment) { - mDirtyComplexityAttachments.insert(object_id); - - // Also mark the cache entry if it exists + // 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 - mVisualComplexityStale = true; -} - -void LLVOAvatar::markAllComplexityDirty() -{ - for (complexity_cache_map_t::iterator it = mComplexityCache.begin(); - it != mComplexityCache.end(); ++it) - { - it->second.needs_update = true; - mDirtyComplexityAttachments.insert(it->first); - } - mBodyPartsComplexity.needs_update = true; - mControlAvatarComplexity.needs_update = true; - - // Launch update process if not already scheduled + // It will add any missing attachments. mVisualComplexityStale = true; } @@ -11363,83 +11308,15 @@ void LLVOAvatar::markBodyPartsComplexityDirty() mVisualComplexityStale = true; } -void LLVOAvatar::updateDirtyAttachmentComplexity(const F32 max_attachment_complexity) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; - - if (mDirtyComplexityAttachments.empty()) - { - return; - } - - // Update only attachments marked as dirty - for (std::set::iterator dirty_it = mDirtyComplexityAttachments.begin(); - dirty_it != mDirtyComplexityAttachments.end(); ++dirty_it) - { - const LLUUID& object_id = *dirty_it; - - // Find the actual attachment object - LLViewerObject* attached_obj = gObjectList.findObject(object_id); - if (attached_obj && !attached_obj->isDead() && attached_obj->getAttachmentItemID().notNull()) - { - ComplexityComponent& cache = mComplexityCache[object_id]; - calculateAttachmentComplexity(attached_obj, max_attachment_complexity, cache); - } - else - { - // Object no longer exists, remove from cache - mComplexityCache.erase(object_id); - } - } - - mDirtyComplexityAttachments.clear(); -} - -void LLVOAvatar::performFullComplexityUpdate(const F32 max_attachment_complexity) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; - - // Clear the cache and recalculate everything - mComplexityCache.clear(); - mDirtyComplexityAttachments.clear(); - - // Recalculate all attachments - 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]; - calculateAttachmentComplexity(attached_object, max_attachment_complexity, cache); - } - } - } - - // Recalculate body parts - calculateBodyPartsComplexity(mBodyPartsComplexity); - - mLastFullComplexityUpdate = LLFrameTimer::getTotalSeconds(); -} - void LLVOAvatar::performPartialComplexityUpdate(const F32 max_attachment_complexity) { LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; // Update any attachments marked as dirty - updateDirtyAttachmentComplexity(max_attachment_complexity); + // 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. - // Update stale cache entries for (attachment_map_t::iterator iter = mAttachmentPoints.begin(); iter != mAttachmentPoints.end(); ++iter) { @@ -11458,7 +11335,7 @@ void LLVOAvatar::performPartialComplexityUpdate(const F32 max_attachment_complex LLUUID object_id = attached_object->getID(); ComplexityComponent& cache = mComplexityCache[object_id]; - // Update if cache is stale + // Update if cache is stale or a new entry. if (shouldUpdateComplexityComponent(cache)) { calculateAttachmentComplexity(attached_object, max_attachment_complexity, cache); @@ -11475,6 +11352,8 @@ void LLVOAvatar::performPartialComplexityUpdate(const F32 max_attachment_complex } // 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; @@ -11496,36 +11375,25 @@ void LLVOAvatar::calculateUpdateRenderComplexity() 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; - LLVOVolume::texture_cost_t all_textures; // Not currently in use. Perhaps add cost from unique textures? hud_complexity_list_t hud_complexity_list; object_complexity_list_t object_complexity_list; - // Determine update strategy - F64 current_time = LLFrameTimer::getTotalSeconds(); - - // Todo: check if it changes anything, then get rid of FULL_UPDATE_INTERVAL, - // full recalculation can take over 10ms for very complex avatars, - // thus is not practical to do. Leave only partial updates. - constexpr F64 FULL_UPDATE_INTERVAL = 60.0; // seconds - bool do_full_update = (current_time - mLastFullComplexityUpdate) > FULL_UPDATE_INTERVAL; - - // Perform update based on strategy - if (do_full_update) - { - // Expensive! Todo: don't do. - performFullComplexityUpdate(max_attachment_complexity); - } - else - { - performPartialComplexityUpdate(max_attachment_complexity); - } - // Calculate and accumulate control avatar complexity if applicable // For now this is on each run. // Todo: See if mControlAvatarComplexity.needs_update is applicable here. @@ -11534,14 +11402,12 @@ void LLVOAvatar::calculateUpdateRenderComplexity() accumulateComplexityComponent( mControlAvatarComplexity, total_cost, - all_textures, hud_complexity_list, object_complexity_list); } // Accumulate body parts complexity - accumulateComplexityComponent(mBodyPartsComplexity, total_cost, - all_textures, hud_complexity_list, object_complexity_list); + 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 @@ -11554,7 +11420,9 @@ void LLVOAvatar::calculateUpdateRenderComplexity() // Verify object still exists LLViewerObject* obj = gObjectList.findObject(object_id); - if (!obj || obj->isDead()) + if (!obj + || obj->isDead() + || !obj->isAttachment()) { to_remove.push_back(object_id); continue; @@ -11562,7 +11430,7 @@ void LLVOAvatar::calculateUpdateRenderComplexity() // Accumulate this attachment's complexity accumulateComplexityComponent(cache_iter->second, total_cost, - all_textures, hud_complexity_list, object_complexity_list); + hud_complexity_list, object_complexity_list); } // Remove stale cache entries @@ -11571,6 +11439,21 @@ void LLVOAvatar::calculateUpdateRenderComplexity() 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; @@ -11583,7 +11466,7 @@ void LLVOAvatar::calculateUpdateRenderComplexity() U32 LLVOAvatar::calculateBodyPartsComplexity() { - static const U32 COMPLEXITY_BODY_PART_COST = 200; + constexpr U32 COMPLEXITY_BODY_PART_COST = 200; U32 cost = 0; for (U8 baked_index = 0; baked_index < BAKED_NUM_INDICES; baked_index++) { @@ -11615,7 +11498,7 @@ U32 LLVOAvatar::calculateBodyPartsComplexity() return cost; } -void LLVOAvatar::processComplexityCostChange(hud_complexity_list_t hud_complexity_list, object_complexity_list_t object_complexity_list) +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); @@ -11660,8 +11543,8 @@ void LLVOAvatar::accountRenderComplexityForObject( U32& visible_triangle_count, F32& est_triangle_count, F32& surface_area, - hud_complexity_list_t& hud_complexity_list, - object_complexity_list_t& object_complexity_list) + LLHUDComplexity& hud_object_complexity, + LLObjectComplexity& object_complexity) { LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; if (attached_object && !attached_object->isHUDAttachment()) @@ -11723,11 +11606,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); } } } @@ -11745,7 +11626,6 @@ void LLVOAvatar::accountRenderComplexityForObject( 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; @@ -11800,106 +11680,7 @@ void LLVOAvatar::accountRenderComplexityForObject( } } } - hud_complexity_list.push_back(hud_object_complexity); - } - } -} - -// Calculations for mVisualComplexity value -void LLVOAvatar::calculateUpdateRenderComplexityLegacy() -{ - /***************************************************************** - * 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 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; - - cost += calculateBodyPartsComplexity(); - - 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, - mAttachmentVisibleTriangleCount, - mAttachmentEstTriangleCount, - mAttachmentSurfaceArea, - 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, - mAttachmentVisibleTriangleCount, - mAttachmentEstTriangleCount, - mAttachmentSurfaceArea, - 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; - - processComplexityCostChange(hud_complexity_list, object_complexity_list); } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 79a03527fb..580d6ec911 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -305,15 +305,13 @@ public: U32& visible_triangle_count, F32& est_triangle_count, F32& surface_area, - hud_complexity_list_t& hud_complexity_list, - object_complexity_list_t& object_complexity_list); + LLHUDComplexity& hud_object_complexity, + LLObjectComplexity& object_complexity); void calculateUpdateRenderComplexity(); - void calculateUpdateRenderComplexityLegacy(); static const U32 VISUAL_COMPLEXITY_UNKNOWN; void updateVisualComplexity(); // Mark that an attachment needs complexity recalculation - void markAttachmentComplexityDirty(const LLUUID& object_id); - void markAllComplexityDirty(); + void markAttachmentComplexityDirty(const LLUUID& object_id, bool force_reset_attachment = false); void markBodyPartsComplexityDirty(); void placeProfileQuery(); @@ -650,16 +648,13 @@ private: void accumulateComplexityComponent(const ComplexityComponent& component, U32& total_cost, - LLVOVolume::texture_cost_t& all_textures, hud_complexity_list_t& hud_list, object_complexity_list_t& object_list); bool shouldUpdateComplexityComponent(const ComplexityComponent& component) const; - void updateDirtyAttachmentComplexity(const F32 max_attachment_complexity); - void performFullComplexityUpdate(const F32 max_attachment_complexity); void performPartialComplexityUpdate(const F32 max_attachment_complexity); - void processComplexityCostChange(hud_complexity_list_t hud_complexity_list, object_complexity_list_t object_complexity_list); + 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. @@ -667,12 +662,6 @@ private: 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 - F64 mLastFullComplexityUpdate; - - // Attachments marked for update, - // 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. - std::set mDirtyComplexityAttachments; // the isTooComplex method uses these mutable values to avoid recalculating too frequently // DEPRECATED -- obsolete avatar render cost values diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 1cb6d2ecc5..3b41ccb6fc 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -1276,9 +1276,9 @@ void LLVOVolume::updateVisualComplexity() 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 + // due to lack of dirty mechanics, and this might be // where we should implement and call - // markCotrolAvatarComplexityDirty() if !isAttachment(). + // markControlAvatarComplexityDirty() if !isAttachment(). rigged_avatar->markAttachmentComplexityDirty(lobj->getID()); } /*LLVOAvatar* avatar = getAvatarAncestor(); -- cgit v1.3 From a77156ebbdfa61a9c88437a61b78f986e191ac80 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Tue, 31 Mar 2026 18:41:04 +0300 Subject: #5373 add leap api to open Build floater and change selected tool --- indra/llui/llfloaterreglistener.cpp | 9 ++++++ indra/llui/llfloaterreglistener.h | 1 + indra/newview/lltoolmgr.cpp | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) (limited to 'indra') diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp index 17641b8375..7cf09369ec 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 28f6e7c66b..610a2831dc 100644 --- a/indra/llui/llfloaterreglistener.h +++ b/indra/llui/llfloaterreglistener.h @@ -46,6 +46,7 @@ private: 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/newview/lltoolmgr.cpp b/indra/newview/lltoolmgr.cpp index 07963a7bed..c7e6f774dc 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 -- cgit v1.3 From b4f1072a492f5baa8338bd1a5dc2d87ccfacc086 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Wed, 1 Apr 2026 19:23:41 +0300 Subject: #5367 Leap API support for combo boxes --- indra/llui/llcombobox.cpp | 33 +++++++++++++++++++++++++++++++++ indra/llui/llcombobox.h | 4 ++++ indra/newview/lluilistener.cpp | 24 ++++++++++++++++++++++++ indra/newview/lluilistener.h | 1 + 4 files changed, 62 insertions(+) (limited to 'indra') diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp index ae676251ff..01463b4962 100644 --- a/indra/llui/llcombobox.cpp +++ b/indra/llui/llcombobox.cpp @@ -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 d6ea1202d3..dad60ffb65 100644 --- a/indra/llui/llcombobox.h +++ b/indra/llui/llcombobox.h @@ -219,6 +219,10 @@ public: 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/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp index beae71e7bf..d95560b306 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 70455c2c68..228e67ae26 100644 --- a/indra/newview/lluilistener.h +++ b/indra/newview/lluilistener.h @@ -42,6 +42,7 @@ public: private: void call(const LLSD& event) const; void getValue(const LLSD&event) const; + void setSelectedByValue(const LLSD& event) const; }; #endif /* ! defined(LL_LLUILISTENER_H) */ -- cgit v1.3 From de5ff4f6548e377043b534957b34f2b3298e339e Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Thu, 2 Apr 2026 19:56:24 +0300 Subject: #5447 update Dialog buttons to correctly display emoji --- indra/newview/lltoastnotifypanel.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/lltoastnotifypanel.cpp b/indra/newview/lltoastnotifypanel.cpp index 6c0b3bfa13..eb77343fd3 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; -- cgit v1.3 From 210a6a8eb4499b86f548f7afdc90f1660c117f9c Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:33:03 +0300 Subject: #5602 deleteProbe optimization pass glGenQueries synchronizes cpu with gpu, which is expensive --- indra/newview/llreflectionmap.cpp | 5 +++-- indra/newview/llreflectionmapmanager.cpp | 34 ++++++++++++++++++++++++++++++++ indra/newview/llreflectionmapmanager.h | 7 +++++++ 3 files changed, 44 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/newview/llreflectionmap.cpp b/indra/newview/llreflectionmap.cpp index 7f5076bd56..f4f3e39f17 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 c6fa64753c..d2e37fb40a 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 5daed7d1cf..1e1f9ad1a2 100644 --- a/indra/newview/llreflectionmapmanager.h +++ b/indra/newview/llreflectionmapmanager.h @@ -106,6 +106,7 @@ public: // release any GL state void cleanup(); + void cleanupQueryPool(); // maintain reflection probes void update(); @@ -164,6 +165,10 @@ public: 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 @@ private: // 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; -- cgit v1.3 From 5b485bdc6046867e8b03906e8bdd0f02471f9fa8 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:11:15 +0300 Subject: #5612 Improve profiling coverage --- indra/llmessage/llassetstorage.cpp | 3 ++- indra/newview/llfloaterinspect.cpp | 1 + indra/newview/llremoteparcelrequest.cpp | 1 + indra/newview/llviewergenericmessage.cpp | 3 +++ indra/newview/llviewermessage.cpp | 8 ++++++++ indra/newview/llviewerobject.cpp | 1 + indra/newview/llvoavatar.cpp | 1 + indra/newview/llworld.cpp | 1 + indra/newview/llworldmapmessage.cpp | 2 ++ 9 files changed, 20 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/llmessage/llassetstorage.cpp b/indra/llmessage/llassetstorage.cpp index 4c3acb27f4..b6a98575f9 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/newview/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp index c0fe7ad896..163edf0426 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/llremoteparcelrequest.cpp b/indra/newview/llremoteparcelrequest.cpp index f89afd38ab..c1b33f313b 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/llviewergenericmessage.cpp b/indra/newview/llviewergenericmessage.cpp index fd894a5997..f3a0f026b8 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/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 5d8bd45218..812ba76551 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/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 521c6731e0..7c26cb3c9f 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -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/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index bb152ced40..2f39a76156 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); diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp index 47e1815bc2..d02694de7d 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/llworldmapmessage.cpp b/indra/newview/llworldmapmessage.cpp index 3264f8ae8b..c039f9de3f 100644 --- a/indra/newview/llworldmapmessage.cpp +++ b/indra/newview/llworldmapmessage.cpp @@ -154,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; @@ -248,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); -- cgit v1.3 From 3078b209ff2cddde993b9827d1a77bdd997f31ab Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:11:51 +0300 Subject: #5612 Fix fast cache freezing main thread --- indra/newview/llviewertexturelist.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index bfa662e5ef..e4fd947892 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1200,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();) { -- cgit v1.3 From 5cdd65e6a95896ac1c9f4b0ca5dbec85e8b0f485 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:35:28 +0300 Subject: #2892 Make thumbnail drop work the same way picker does --- indra/newview/llfloaterchangeitemthumbnail.cpp | 151 +++++++++++++------------ indra/newview/llfloaterchangeitemthumbnail.h | 4 + 2 files changed, 84 insertions(+), 71 deletions(-) (limited to 'indra') diff --git a/indra/newview/llfloaterchangeitemthumbnail.cpp b/indra/newview/llfloaterchangeitemthumbnail.cpp index fb31361fd9..914d2135ba 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 46f63801fe..980b46b298 100644 --- a/indra/newview/llfloaterchangeitemthumbnail.h +++ b/indra/newview/llfloaterchangeitemthumbnail.h @@ -82,7 +82,11 @@ private: 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, -- cgit v1.3 From 321a6e962bc8bf86b7ef09f035fbc1f60c1d17f2 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:37:17 +0300 Subject: #5612 Reduce delays on resource access for main thread Our main thread was of normal priority, despite being a render thread, so it was forced to compete for some of the resources. Give it high priority. --- indra/llwindow/llwindowwin32.cpp | 101 ++++++++++++++++++++++++--------------- indra/llwindow/llwindowwin32.h | 2 + 2 files changed, 65 insertions(+), 38 deletions(-) (limited to 'indra') diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 2bd9dd053c..211d766f1f 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -523,6 +523,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 +547,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; @@ -1071,6 +1038,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)); @@ -3041,19 +3054,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 diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index 8159092794..d7fc715258 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -148,6 +148,8 @@ protected: void initCursors(); HCURSOR loadColorCursor(LPCTSTR name); bool isValid(); + void setThreadPriorityHigh(); + void setThreadPriorityNormal(); void moveWindow(const LLCoordScreen& position,const LLCoordScreen& size); virtual LLSD getNativeKeyData(); -- cgit v1.3 From 61417e2ba3ba77fe79b9d1d477524cba130106c6 Mon Sep 17 00:00:00 2001 From: Darl Date: Wed, 1 Apr 2026 11:36:51 -0500 Subject: Allow mute list to fallback to cached copy when simulator response somehow isn't received Signed-off-by: Darl --- indra/newview/llmutelist.cpp | 130 ++++++++++++++++++++++++++++++++----------- indra/newview/llmutelist.h | 21 ++++++- 2 files changed, 117 insertions(+), 34 deletions(-) (limited to 'indra') diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index b7bba02b9d..d9e674a423 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -92,7 +92,7 @@ public: const LLUUID& invoice, const sparam_t& strings) { - LLMuteList::getInstance()->setLoaded(); + LLMuteList::getInstance()->setLoaded(LLMuteList::MLS_SERVER_EMPTY); return true; } }; @@ -155,7 +155,9 @@ std::string LLMute::getDisplayType() const //----------------------------------------------------------------------------- LLMuteList::LLMuteList() : mLoadState(ML_INITIAL), - mRequestStartTime(0.f) + mLoadSource(MLS_NONE), + mRequestStartTime(0.f), + mTriedCacheFallback(false) { gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList); @@ -210,7 +212,7 @@ bool LLMuteList::isLinden(const std::string& name) return last_name == "linden"; } -bool LLMuteList::getLoadFailed() const +bool LLMuteList::getLoadFailed() { if (mLoadState == ML_FAILED) { @@ -221,12 +223,78 @@ 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 mLoadState == ML_FAILED; } } return false; } +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; + } +} + +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; +} + static LLVOAvatar* find_avatar(const LLUUID& id) { LLViewerObject *obj = gObjectList.findObject(id); @@ -580,14 +648,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 +663,14 @@ 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. + mMutes.clear(); + mLegacyMutes.clear(); + // *NOTE: Changing the size of these buffers will require changes // in the scanf below. char id_buffer[MAX_STRING]; /*Flawfinder: ignore*/ @@ -627,7 +699,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 +809,11 @@ 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"; + 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 +826,17 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) if (gDisconnected) { LL_WARNS() << "Trying to request mute list when disconnected!" << LL_ENDL; - mLoadState = ML_FAILED; + tryLoadCacheFallback(agent_id, "disconnected before request"); 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 +848,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() && mLoadSource != MLS_FALLBACK_CACHE) { - 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; + } } //----------------------------------------------------------------------------- @@ -812,7 +884,7 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**) 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 +903,7 @@ 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::getInstance()->loadFromFile(LLMuteList::getInstance()->getCacheFilename(gAgent.getID()), MLS_SERVER_CACHE); } void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_status) @@ -845,13 +912,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,9 +975,10 @@ void LLMuteList::removeObserver(LLMuteListObserver* observer) mObservers.erase(observer); } -void LLMuteList::setLoaded() +void LLMuteList::setLoaded(EMuteListSource source) { mLoadState = ML_LOADED; + mLoadSource = source; notifyObservers(); } diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h index b65fd61fcc..c243b5a2bd 100644 --- a/indra/newview/llmutelist.h +++ b/indra/newview/llmutelist.h @@ -82,6 +82,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 @@ -116,7 +125,7 @@ public: static bool isLinden(const std::string& name); bool isLoaded() const { return mLoadState == ML_LOADED; } - bool getLoadFailed() const; + bool getLoadFailed(); std::vector getMutes() const; @@ -127,10 +136,14 @@ public: void cache(const LLUUID& agent_id); private: - bool loadFromFile(const std::string& filename); + 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 +190,9 @@ private: observer_set_t mObservers; EMuteListState mLoadState; + EMuteListSource mLoadSource; F64 mRequestStartTime; + bool mTriedCacheFallback; friend class LLDispatchEmptyMuteList; }; -- cgit v1.3 From 03c13540cb38d2e0ada6bf62ad2d4666487fa090 Mon Sep 17 00:00:00 2001 From: Darl Date: Thu, 2 Apr 2026 10:23:50 -0500 Subject: Treat emptymutelist dispatch from the simulator as authoritative when in a cache fallback status. This yields to the simulator's responsibility as source-of-truth. e.g. Bob has Alice blocked for a while across all his devices 1. Bob unblocked Alice on his laptop 2. Bob logs in on his desktop with a cached mutelist 3. LLDispatchEmptyMuteList fires 4. LLMuteList becomes eventually-correct, reflecting most recent signaled user intent Signed-off-by: Darl --- indra/newview/llmutelist.cpp | 13 ++++++++++--- indra/newview/llmutelist.h | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'indra') diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index d9e674a423..154e826196 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -92,7 +92,9 @@ public: const LLUUID& invoice, const sparam_t& strings) { - LLMuteList::getInstance()->setLoaded(LLMuteList::MLS_SERVER_EMPTY); + LLMuteList* mute_list = LLMuteList::getInstance(); + mute_list->clearCachedMutes(); + mute_list->setLoaded(LLMuteList::MLS_SERVER_EMPTY); return true; } }; @@ -231,6 +233,12 @@ bool LLMuteList::getLoadFailed() return false; } +void LLMuteList::clearCachedMutes() +{ + mMutes.clear(); + mLegacyMutes.clear(); +} + const char* LLMuteList::sourceToString(EMuteListSource source) { switch (source) @@ -668,8 +676,7 @@ bool LLMuteList::loadFromFile(const std::string& filename, EMuteListSource sourc } // Replace previous server-backed state so fallback can be superseded by authoritative data. - mMutes.clear(); - mLegacyMutes.clear(); + clearCachedMutes(); // *NOTE: Changing the size of these buffers will require changes // in the scanf below. diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h index c243b5a2bd..34857e2006 100644 --- a/indra/newview/llmutelist.h +++ b/indra/newview/llmutelist.h @@ -136,6 +136,7 @@ public: void cache(const LLUUID& agent_id); private: + 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); -- cgit v1.3 From 0abb59a959d16b1163076ff0bd804d59ccf57bdb Mon Sep 17 00:00:00 2001 From: Darl Date: Thu, 2 Apr 2026 11:27:48 -0500 Subject: Rename LLMuteList state machine touch points for clarity getLoadFailed -> updateLoadState - No longer labeled as a plain getter, but instead as a state machine advancement point - This name reflects its role in advancing the state according to design parameters when called from the idle loop - Call site in LLIMProcessing::requestOfflineMessages simplified by internalizing our readiness checks isFailed - Reintroduced const to match isLoaded for determining state Signed-off-by: Darl --- indra/newview/llimprocessing.cpp | 2 +- indra/newview/llmutelist.cpp | 6 +++--- indra/newview/llmutelist.h | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) (limited to 'indra') diff --git a/indra/newview/llimprocessing.cpp b/indra/newview/llimprocessing.cpp index b1e42e11fb..3f60dc5d26 100644 --- a/indra/newview/llimprocessing.cpp +++ b/indra/newview/llimprocessing.cpp @@ -1534,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/llmutelist.cpp b/indra/newview/llmutelist.cpp index 154e826196..02a5a43c6b 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -214,9 +214,9 @@ bool LLMuteList::isLinden(const std::string& name) return last_name == "linden"; } -bool LLMuteList::getLoadFailed() +bool LLMuteList::updateLoadState() { - if (mLoadState == ML_FAILED) + if (isLoaded() || isFailed()) { return true; } @@ -227,7 +227,7 @@ bool LLMuteList::getLoadFailed() { LL_WARNS() << "Mute list request timed out; trying cache fallback once" << LL_ENDL; tryLoadCacheFallback(gAgent.getID(), "request timeout"); - return mLoadState == ML_FAILED; + return isLoaded() || isFailed(); } } return false; diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h index 34857e2006..ff010a02e8 100644 --- a/indra/newview/llmutelist.h +++ b/indra/newview/llmutelist.h @@ -125,7 +125,11 @@ public: static bool isLinden(const std::string& name); bool isLoaded() const { return mLoadState == ML_LOADED; } - bool getLoadFailed(); + bool isFailed() const { return mLoadState == ML_FAILED; } + + // Advance the load state machine, trying cache fallback if necessary. + // Return value indicates mute list consumption readiness. + bool updateLoadState(); std::vector getMutes() const; -- cgit v1.3 From 840383def31edd7b60f49bbc82303bf642c0d1ec Mon Sep 17 00:00:00 2001 From: Darl Date: Thu, 2 Apr 2026 11:56:42 -0500 Subject: Eliminate pointless mutelist cache fallback attempt, instead manually set state to guard against a possible cache write to disk to be safe Signed-off-by: Darl --- indra/newview/llmutelist.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index 02a5a43c6b..39588ba51d 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -833,7 +833,9 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) if (gDisconnected) { LL_WARNS() << "Trying to request mute list when disconnected!" << LL_ENDL; - tryLoadCacheFallback(agent_id, "disconnected before request"); + // Guard against potentially writing back to disk since we're not recovering our connection + mLoadState = ML_LOADED; + mLoadSource = MLS_FALLBACK_CACHE; return; } if (!gAgent.getRegion()) -- cgit v1.3 From 9978c669b42c1a7ebd96fbdf34abb211590af66a Mon Sep 17 00:00:00 2001 From: Darl Date: Fri, 3 Apr 2026 18:33:00 -0500 Subject: Improve documentation and logging of LLMuteList changes Signed-off-by: Darl --- indra/newview/llmutelist.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index 39588ba51d..f8bdc453b2 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -92,8 +92,16 @@ public: const LLUUID& invoice, const sparam_t& strings) { + // 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(); - mute_list->clearCachedMutes(); + 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; } @@ -237,6 +245,7 @@ void LLMuteList::clearCachedMutes() { mMutes.clear(); mLegacyMutes.clear(); + LL_WARNS() << "Cached mutes cleared" << LL_ENDL; } const char* LLMuteList::sourceToString(EMuteListSource source) @@ -276,6 +285,7 @@ void LLMuteList::setFailed(const std::string& reason) { LL_WARNS() << "Mute list unavailable: " << reason << " (last source=" << sourceToString(mLoadSource) << ")" << LL_ENDL; } + notifyObservers(); } bool LLMuteList::tryLoadCacheFallback(const LLUUID& agent_id, const std::string& reason) @@ -880,7 +890,7 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**) 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; @@ -986,9 +996,14 @@ void LLMuteList::removeObserver(LLMuteListObserver* observer) 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; } void LLMuteList::notifyObservers() -- cgit v1.3 From 581cad9cc45c375b8c60692e565b27ac1568f4c9 Mon Sep 17 00:00:00 2001 From: Darl Date: Fri, 3 Apr 2026 18:36:10 -0500 Subject: Make LLBlockList more reactive to LLMuteList changes This allows the UI to update if already initialized before fallback or load occurs Signed-off-by: Darl --- indra/newview/llblocklist.cpp | 8 ++++++++ indra/newview/llblocklist.h | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llblocklist.cpp b/indra/newview/llblocklist.cpp index 89516a8a84..230aa19ad2 100644 --- a/indra/newview/llblocklist.cpp +++ b/indra/newview/llblocklist.cpp @@ -101,6 +101,14 @@ BlockListActionType LLBlockList::getCurrentMuteListActionType() return type; } +void LLBlockList::onChange() +{ + // Something changed, not sure what so force a refresh. + mShouldAddAll = true; + mActionType = NONE; + setDirty(); +} + void LLBlockList::onChangeDetailed(const LLMute &mute) { mActionType = getCurrentMuteListActionType(); diff --git a/indra/newview/llblocklist.h b/indra/newview/llblocklist.h index 64e8246f43..92bb910c57 100644 --- a/indra/newview/llblocklist.h +++ b/indra/newview/llblocklist.h @@ -58,7 +58,7 @@ public: LLToggleableMenu* getContextMenu() const { return mContextMenu.get(); } LLBlockedListItem* getBlockedItem() const; - virtual void onChange() { } + virtual void onChange(); virtual void onChangeDetailed(const LLMute& ); virtual void draw(); -- cgit v1.3 From 901ad6193b767b07028b816a1b3284927491c3db Mon Sep 17 00:00:00 2001 From: Darl Date: Fri, 3 Apr 2026 18:38:08 -0500 Subject: Add noitems text to the block list UI, with translation friendly helper strings to denote loading or failed states Signed-off-by: Darl --- indra/newview/llblocklist.cpp | 27 +++++++++++++++++++++- indra/newview/llblocklist.h | 5 ++++ indra/newview/llpanelblockedlist.cpp | 4 ++++ .../default/xui/en/panel_block_list_sidetray.xml | 12 ++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llblocklist.cpp b/indra/newview/llblocklist.cpp index 230aa19ad2..05c882e3f1 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,16 +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; @@ -205,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 @@ -216,6 +237,8 @@ void LLBlockList::refresh() clear(); createList(); mShouldAddAll = false; + // Full rebuild supersedes any queued incremental action. This ensures list consistency. + mActionType = NONE; } else { @@ -254,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 92bb910c57..9266584256 100644 --- a/indra/newview/llblocklist.h +++ b/indra/newview/llblocklist.h @@ -68,6 +68,8 @@ public: 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 @@ private: bool isMenuItemVisible(const LLSD& userdata); void toggleMute(U32 flags); void createList(); + void updateNoItemsCommentText(); BlockListActionType getCurrentMuteListActionType(); @@ -100,6 +103,8 @@ private: LLMute::EType mCurItemType; U32 mCurItemFlags; std::string mPrevNameFilter; + std::string mLoadingItemsMsg; + std::string mFailedItemsMsg; }; diff --git a/indra/newview/llpanelblockedlist.cpp b/indra/newview/llpanelblockedlist.cpp index 69f51b03b6..178f994beb 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/skins/default/xui/en/panel_block_list_sidetray.xml b/indra/newview/skins/default/xui/en/panel_block_list_sidetray.xml index 6bb891db63..d907a050b6 100644 --- a/indra/newview/skins/default/xui/en/panel_block_list_sidetray.xml +++ b/indra/newview/skins/default/xui/en/panel_block_list_sidetray.xml @@ -10,6 +10,18 @@ min_height="350" min_width="240" width="323"> + + + + Date: Fri, 3 Apr 2026 22:57:53 -0500 Subject: Attempt one additional mute list request from simulator after region change Signed-off-by: Darl --- indra/newview/llmutelist.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++-- indra/newview/llmutelist.h | 16 +++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index f8bdc453b2..b72301c566 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -190,6 +190,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)); } //----------------------------------------------------------------------------- @@ -203,6 +205,10 @@ LLMuteList::~LLMuteList() void LLMuteList::cleanupSingleton() { LLAvatarNameCache::getInstance()->setAccountNameChangedCallback(nullptr); + if (mRegionChangedCallback.connected()) + { + mRegionChangedCallback.disconnect(); + } } bool LLMuteList::isLinden(const std::string& name) @@ -826,6 +832,11 @@ bool LLMuteList::isMuted(const std::string& username, U32 flags) const //----------------------------------------------------------------------------- void LLMuteList::requestFromServer(const LLUUID& agent_id) { + 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); @@ -886,6 +897,11 @@ 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()) @@ -901,7 +917,6 @@ 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::getTotalSeconds(); @@ -922,7 +937,12 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**) void LLMuteList::processUseCachedMuteList(LLMessageSystem* msg, void**) { LL_INFOS() << "LLMuteList::processUseCachedMuteList()" << LL_ENDL; - LLMuteList::getInstance()->loadFromFile(LLMuteList::getInstance()->getCacheFilename(gAgent.getID()), MLS_SERVER_CACHE); + 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) @@ -1004,6 +1024,11 @@ void LLMuteList::setLoaded(EMuteListSource source) 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() @@ -1032,6 +1057,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 ff010a02e8..2781e9b177 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; @@ -124,8 +126,13 @@ public: static bool isLinden(const std::string& name); - bool isLoaded() const { return mLoadState == ML_LOADED; } - bool isFailed() const { return mLoadState == ML_FAILED; } + // Load state accessors. + bool isLoaded() const { return mLoadState == ML_LOADED; } // Loaded, but not necessarily from server. + bool isFailed() const { return mLoadState == ML_FAILED; } // Unable to load any mute list. Server did not reply. + // Loaded from server, which is the only source we consider authoritative. + bool isLoadedFromServer() const { return isLoaded() && (mLoadSource == MLS_SERVER || mLoadSource == MLS_SERVER_EMPTY); } + // Loaded, but from cache. Would be nice to upgrade to a server load from here if possible. + bool isLoadedDegraded() const { return isLoaded() && !isLoadedFromServer(); } // Advance the load state machine, trying cache fallback if necessary. // Return value indicates mute list consumption readiness. @@ -139,6 +146,9 @@ public: // 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: void clearCachedMutes(); bool loadFromFile(const std::string& filename, EMuteListSource source); @@ -198,6 +208,8 @@ private: EMuteListSource mLoadSource; F64 mRequestStartTime; bool mTriedCacheFallback; + bool mTriedRegionChangeRetry; + boost::signals2::connection mRegionChangedCallback; friend class LLDispatchEmptyMuteList; }; -- cgit v1.3 From 66dc10f0e9ab4f8d209f8a04671027c608daaffd Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 7 Apr 2026 19:12:59 -0400 Subject: Release/26.1.1 (#5530) * Integrate Velopack installer and update framework * Add Velopack update support for macOS and VVM integration * Update Velopack version and dependencies * Improve Velopack packaging for macOS * #5346 Uninstall older non-velopack viewer (#5363) * #5335 Fix silent uninstall asking about registry * #5346 Uninstall older non-velopack viewer * Use runtime viewer exe name, handle Velopack URL * Velopack download failure diagnostic (#5520) * Velopack download failure diagnostic * Fix up velopack downloading updates. Handle updates internally then hand them off to velopack. (#5524) * More velopack changes. Should download updates properly now. * Don't include NSI files * Restore optional updates, refine viewer restart behavior. (#5527) * Add support for optional updates. * Don't restart the viewer after the update unless it was optional. * Setup UpdaterServiceSetting with velopack properly. * Refine the restart behavior a bit - readd the old "the viewer must update" UX. * If the update is still downloading, close should just reopen the downloading dialog. --------- Co-authored-by: Jonathan "Geenz" Goodman * Remove SLVersionChecker from the viewer with velopack. (#5528) * Remove SLVersionChecker updater integration * Ensure that the portable install has the correct version number. * Don't produce shortcuts with VPK - we do this with our post install. * Bump viewer version from 26.1.0 to 26.1.1 * Potential fix for uninstaller not being functional. * Fix for UpdaterServiceSetting being ignored. * Filter for release channel when generating shortcuts. * Add some more logging for icons on Windows builds. * More VPK logging. * Move velopack packaging in CI to the sign and package step. * Enable velopack downgrade and skip older updates * Move the version required checking into velopack's checks. * Potential fix for downgrade prompts. * Make sure our macOS flow mirrors Windows. * Make sure to use the dev version of the mac sign and package. * p#553 Only one of two uninstallers displayed * #5346 Don't force user to shutdown velopack build for NSIS uninstall * #5346 Ignore option for the uninstall dialog * #5346 Fix early exit crash * #5346 Properly reset version flag. * Add some autodetect logic on macOS. * p#564 Clear legacy links * p#553 Handle uninstall records * p#549 Permit testing release notes on a test build * p#564 Remake nsis to velopack update flow * p#564 Remake nsis to velopack update flow #2 * p#564 Fix incorrect value type * p#553 Clear velopack's own registry entry in favor of a custom one * #5346 Resolve duplicated window class name * Bump to 2.1.0 of sign and package. --------- Co-authored-by: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> --- .github/workflows/build.yaml | 60 +- .github/workflows/tag-release.yaml | 36 +- autobuild.xml | 50 + indra/cmake/CMakeLists.txt | 1 + indra/cmake/Velopack.cmake | 68 ++ indra/lib/python/indra/util/llmanifest.py | 3 +- indra/llcommon/workqueue.cpp | 18 +- indra/llcommon/workqueue.h | 6 + indra/newview/CMakeLists.txt | 15 + indra/newview/VIEWER_VERSION.txt | 2 +- indra/newview/app_settings/settings.xml | 16 +- .../installers/windows/installer_template.nsi | 16 +- indra/newview/llappviewer.cpp | 90 +- indra/newview/llappviewer.h | 12 +- indra/newview/llappviewerwin32.cpp | 33 +- indra/newview/llappviewerwin32.h | 2 - indra/newview/lllogininstance.cpp | 81 +- indra/newview/lllogininstance.h | 6 - indra/newview/llpanellogin.cpp | 16 + indra/newview/llstartup.cpp | 89 +- indra/newview/llvelopack.cpp | 1201 ++++++++++++++++++++ indra/newview/llvelopack.h | 59 + indra/newview/llversioninfo.cpp | 2 +- indra/newview/llversioninfo.h | 4 +- indra/newview/llviewernetwork.cpp | 8 + indra/newview/llviewerstats.cpp | 6 +- indra/newview/llvvmquery.cpp | 189 +++ indra/newview/llvvmquery.h | 42 + .../newview/skins/default/xui/en/notifications.xml | 18 + indra/newview/tests/lllogininstance_test.cpp | 2 - indra/newview/viewer_manifest.py | 261 ++++- indra/viewer_components/login/lllogin.cpp | 62 - 32 files changed, 2191 insertions(+), 283 deletions(-) create mode 100644 indra/cmake/Velopack.cmake create mode 100644 indra/newview/llvelopack.cpp create mode 100644 indra/newview/llvelopack.h create mode 100644 indra/newview/llvvmquery.cpp create mode 100644 indra/newview/llvvmquery.h (limited to 'indra') diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c55605da0d..866af3dbac 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 @@ -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: @@ -439,6 +487,10 @@ jobs: 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/tag-release.yaml b/.github/workflows/tag-release.yaml index 2922065f99..0f826222a0 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 6e48cbf509..9945672bd2 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2914,6 +2914,56 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors description Voxelized Hierarchical Approximate Convex Decomposition + velopack + + platforms + + windows64 + + archive + + creds + github + hash + 91abbc360640b5b2e0a4c001a36ad411a9a42602 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583560 + + name + windows64 + + darwin64 + + archive + + creds + github + hash + 05563a79bdeb83d66a72ac1e97587dc2a8f64511 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583554 + + name + darwin64 + + + license + MIT + license_file + LICENSES/velopack.txt + copyright + Velopack Ltd. + version + 40232ef.23500976684 + name + velopack + description + Velopack C/C++ Library + package_description diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt index 2ba282bdb7..c10f6ec934 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 0000000000..a1dbe2cbe9 --- /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 1bd65eb57d..0ad0b6b1a9 100755 --- a/indra/lib/python/indra/util/llmanifest.py +++ b/indra/lib/python/indra/util/llmanifest.py @@ -157,7 +157,8 @@ BASE_ARGUMENTS=[ 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/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index 67c23358ed..fbd92d089e 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 573203a5b3..69f3286c1b 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/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 05c71f5f5d..1d1553355e 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) @@ -654,6 +655,7 @@ set(viewer_SOURCE_FILES llurllineeditorctrl.cpp llurlwhitelist.cpp llversioninfo.cpp + llvvmquery.cpp llviewchildren.cpp llviewerassetstats.cpp llviewerassetstorage.cpp @@ -1327,6 +1329,7 @@ set(viewer_HEADER_FILES llurllineeditorctrl.h llurlwhitelist.h llversioninfo.h + llvvmquery.h llviewchildren.h llviewerassetstats.h llviewerassetstorage.h @@ -1446,6 +1449,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 +1513,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 +1939,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 +2063,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 +2264,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}" diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt index 2aaedf9944..124b7a2cd0 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/settings.xml b/indra/newview/app_settings/settings.xml index 0671aadee5..a4a9323892 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -4248,7 +4248,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 @@ -13090,11 +13100,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 diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi index ae40e8830f..07ed0d0824 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/llappviewer.cpp b/indra/newview/llappviewer.cpp index 4bca656108..46e64c2a4e 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), @@ -1116,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 @@ -1715,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; @@ -3159,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")) @@ -3284,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 e1119419af..c977757e48 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -117,9 +117,6 @@ public: 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 @@ protected: 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 @@ private: 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/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 0620b625d9..94a5f7951e 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,7 +962,7 @@ 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); diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 3fad53ec72..0741758a0c 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -59,8 +59,6 @@ protected: std::string generateSerialNumber(); - static const std::string sWindowClass; - private: void disableWinErrorReporting(); diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp index e9d68723d3..0358233637 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 54ce51720f..551ad92d33 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 @@ public: 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 @@ private: 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/llpanellogin.cpp b/indra/newview/llpanellogin.cpp index 868e02f28b..0cfb3ec49a 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/llstartup.cpp b/indra/newview/llstartup.cpp index 018c23963c..9bdfbaedc8 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 diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp new file mode 100644 index 0000000000..28e989c4ba --- /dev/null +++ b/indra/newview/llvelopack.cpp @@ -0,0 +1,1201 @@ +/** + * @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 +#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); + unregister_uninstall_info(); + remove_shortcuts(app_name); +} + +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 0000000000..d04d0db7dc --- /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 4e8320b72a..178f10257d 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 237b37a084..a2b93597e6 100644 --- a/indra/newview/llversioninfo.h +++ b/indra/newview/llversioninfo.h @@ -112,8 +112,8 @@ private: 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/llviewernetwork.cpp b/indra/newview/llviewernetwork.cpp index 890580ddff..6cb3aee20c 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/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index d39d466205..5193514fe8 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/llvvmquery.cpp b/indra/newview/llvvmquery.cpp new file mode 100644 index 0000000000..12dcc1d04d --- /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/llvvmquery.h b/indra/newview/llvvmquery.h new file mode 100644 index 0000000000..977d82af64 --- /dev/null +++ b/indra/newview/llvvmquery.h @@ -0,0 +1,42 @@ +/** + * @file llvvmquery.h + * @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$ + */ + +#ifndef LL_LLVVMQUERY_H +#define LL_LLVVMQUERY_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/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index 93ee27b196..fcdea66500 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. + + 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 @@ class Windows_x86_64_Manifest(ViewerManifest): 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 @@ class Darwin_x86_64_Manifest(ViewerManifest): 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 @@ class Darwin_x86_64_Manifest(ViewerManifest): 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 @@ if __name__ == "__main__": 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/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp index 37b70964c3..144f807852 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! -- cgit v1.3 From 130c50cf8d4de021f510b17fd02fcac88a67c5e6 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:26:40 +0300 Subject: #5611 Select a discrete gpu when possible --- indra/llwindow/llwindowwin32.cpp | 254 ++++++++++++++++++++++++++++++++++++++- indra/llwindow/llwindowwin32.h | 11 ++ 2 files changed, 264 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 211d766f1f..c185fc6c4a 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) { @@ -974,6 +987,7 @@ void LLWindowWin32::close() } mDragDrop->reset(); + clearHighPerformanceGPURequest(); // Go back to screen mode written in the registry. @@ -4643,6 +4657,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; @@ -4671,6 +4917,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 d7fc715258..aab2635a34 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -172,6 +172,17 @@ protected: 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 -- cgit v1.3 From 5f91a3faf3ef0b9d0a6c3424fff119992dc0822a Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:53:56 +0300 Subject: #5626 LLTextBase optimization by caching string width --- indra/llrender/llfontvertexbuffer.cpp | 77 +++++++++++++++++++++++++++++++++++ indra/llrender/llfontvertexbuffer.h | 46 +++++++++++++++++++++ indra/llui/llscrollcontainer.cpp | 1 + indra/llui/lltextbase.cpp | 29 +++++++------ indra/llui/lltextbase.h | 13 +++--- indra/newview/llexpandabletextbox.cpp | 2 +- indra/newview/llviewertexteditor.cpp | 12 ++++-- indra/newview/pipeline.cpp | 8 +++- 8 files changed, 164 insertions(+), 24 deletions(-) (limited to 'indra') diff --git a/indra/llrender/llfontvertexbuffer.cpp b/indra/llrender/llfontvertexbuffer.cpp index a223509d30..2a0115265f 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 a9e1e2337c..94b833d227 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 @@ private: 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/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp index df99c4f636..e36fd45bb4 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 c084b400f6..9fcf2a5f10 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -1640,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 @@ -2855,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; @@ -2946,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) { @@ -3497,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); @@ -3595,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; } @@ -3620,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(); @@ -3851,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; } @@ -3943,6 +3947,7 @@ void LLNormalTextSegment::updateLayout(const class LLTextBase& editor) mFontBufferPreSelection.reset(); mFontBufferSelection.reset(); mFontBufferPostSelection.reset(); + mFontWidthBuffer.reset(); } void LLNormalTextSegment::dump() const @@ -4094,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) { @@ -4186,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; @@ -4223,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 21134fd7c9..9206b3facf 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -68,10 +68,10 @@ public: 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 @@ public: 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 @@ protected: LLFontVertexBuffer mFontBufferPreSelection; LLFontVertexBuffer mFontBufferSelection; LLFontVertexBuffer mFontBufferPostSelection; + LLFontWidthBuffer mFontWidthBuffer; S32 mLastGeneration = -1; }; @@ -254,7 +255,7 @@ public: ~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 @@ public: 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 @@ public: ~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); diff --git a/indra/newview/llexpandabletextbox.cpp b/indra/newview/llexpandabletextbox.cpp index 5c46eb9d80..41ae47b645 100644 --- a/indra/newview/llexpandabletextbox.cpp +++ b/indra/newview/llexpandabletextbox.cpp @@ -52,7 +52,7 @@ public: 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/llviewertexteditor.cpp b/indra/newview/llviewertexteditor.cpp index 210cd62d6f..95e34b4df1 100644 --- a/indra/newview/llviewertexteditor.cpp +++ b/indra/newview/llviewertexteditor.cpp @@ -189,7 +189,12 @@ public: 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 @@ public: } 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/pipeline.cpp b/indra/newview/pipeline.cpp index c9d53bbcbc..4ef88f5deb 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() -- cgit v1.3 From ead40173830602f6bf0b1b62573de22523c263f6 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Sun, 22 Feb 2026 03:53:34 -0600 Subject: Add texture streaming "channels" (#5442) * Add channel based streaming priority. * Lessen the aggressiveness of the discards. --- indra/llxml/llcontrol.cpp | 45 ++++++++++++++++++++++++++++++++ indra/llxml/llcontrol.h | 8 ++++++ indra/newview/app_settings/settings.xml | 16 ++++++++++++ indra/newview/llfloatersettingsdebug.cpp | 42 +++++++++++++++++++++++++++++ indra/newview/llviewertexturelist.cpp | 32 +++++++++++++++++++++++ 5 files changed, 143 insertions(+) (limited to 'indra') diff --git a/indra/llxml/llcontrol.cpp b/indra/llxml/llcontrol.cpp index 34643d5f5c..5a37167f18 100644 --- a/indra/llxml/llcontrol.cpp +++ b/indra/llxml/llcontrol.cpp @@ -37,6 +37,7 @@ #include "llstring.h" #include "v3math.h" #include "v3dmath.h" +#include "v4math.h" #include "v4coloru.h" #include "v4color.h" #include "v3color.h" @@ -63,6 +64,7 @@ template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); +template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); @@ -72,6 +74,7 @@ template <> eControlType get_control_type(); template <> LLSD convert_to_llsd(const U32& in); template <> LLSD convert_to_llsd(const LLVector3& in); template <> LLSD convert_to_llsd(const LLVector3d& in); +template <> LLSD convert_to_llsd(const LLVector4& in); template <> LLSD convert_to_llsd(const LLRect& in); template <> LLSD convert_to_llsd(const LLColor4& in); template <> LLSD convert_to_llsd(const LLColor3& in); @@ -85,6 +88,7 @@ template <> std::string convert_from_llsd(const LLSD& sd, eControlT template <> LLWString convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template <> LLVector3 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template <> LLVector3d convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); +template <> LLVector4 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template <> LLRect convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template <> LLColor4 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template <> LLColor4U convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); @@ -124,6 +128,9 @@ bool LLControlVariable::llsd_compare(const LLSD& a, const LLSD & b) case TYPE_VEC3D: result = LLVector3d(a) == LLVector3d(b); break; + case TYPE_VEC4: + result = LLVector4(a) == LLVector4(b); + break; case TYPE_QUAT: result = LLQuaternion(a) == LLQuaternion(b); break; @@ -373,6 +380,7 @@ const std::string LLControlGroup::mTypeString[TYPE_COUNT] = { "U32" ,"Rect" ,"Color4" ,"Color3" + ,"Vector4" ,"LLSD" }; @@ -532,6 +540,11 @@ LLControlVariable* LLControlGroup::declareVec3d(const std::string& name, const L return declareControl(name, TYPE_VEC3D, initial_val.getValue(), comment, persist); } +LLControlVariable* LLControlGroup::declareVec4(const std::string& name, const LLVector4 &initial_val, const std::string& comment, LLControlVariable::ePersist persist) +{ + return declareControl(name, TYPE_VEC4, initial_val.getValue(), comment, persist); +} + LLControlVariable* LLControlGroup::declareQuat(const std::string& name, const LLQuaternion &initial_val, const std::string& comment, LLControlVariable::ePersist persist) { return declareControl(name, TYPE_QUAT, initial_val.getValue(), comment, persist); @@ -614,6 +627,11 @@ LLVector3d LLControlGroup::getVector3d(std::string_view name) return get(name); } +LLVector4 LLControlGroup::getVector4(std::string_view name) +{ + return get(name); +} + LLQuaternion LLControlGroup::getQuaternion(std::string_view name) { return get(name); @@ -714,6 +732,11 @@ void LLControlGroup::setVector3d(std::string_view name, const LLVector3d &val) set(name, val); } +void LLControlGroup::setVector4(std::string_view name, const LLVector4 &val) +{ + set(name, val); +} + void LLControlGroup::setQuaternion(std::string_view name, const LLQuaternion &val) { set(name, val); @@ -1262,6 +1285,11 @@ template <> eControlType get_control_type() return TYPE_VEC3D; } +template <> eControlType get_control_type() +{ + return TYPE_VEC4; +} + template <> eControlType get_control_type() { return TYPE_QUAT; @@ -1302,6 +1330,11 @@ template <> LLSD convert_to_llsd(const LLVector3d& in) { return in.getValue(); } +template <> LLSD convert_to_llsd(const LLVector4& in) +{ + return in.getValue(); +} + template <> LLSD convert_to_llsd(const LLQuaternion& in) { return in.getValue(); @@ -1418,6 +1451,18 @@ LLVector3d convert_from_llsd(const LLSD& sd, eControlType type, std: } } +template<> +LLVector4 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name) +{ + if (type == TYPE_VEC4) + return LLVector4(sd); + else + { + CONTROL_ERRS << "Invalid LLVector4 value for " << control_name << ": " << LLControlGroup::typeEnumToString(type) << " " << sd << LL_ENDL; + return LLVector4(); + } +} + template<> LLQuaternion convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name) { diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index 5aa2b9715e..c2bcd20c85 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -43,6 +43,7 @@ class LLVector3; class LLVector3d; +class LLVector4; class LLQuaternion; class LLColor4; class LLColor3; @@ -61,6 +62,7 @@ typedef enum e_control_type TYPE_RECT, TYPE_COL4, TYPE_COL3, + TYPE_VEC4, TYPE_LLSD, TYPE_COUNT } eControlType; @@ -196,6 +198,7 @@ public: LLControlVariable* declareString(const std::string& name, const std::string &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); LLControlVariable* declareVec3(const std::string& name, const LLVector3 &initial_val,const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); LLControlVariable* declareVec3d(const std::string& name, const LLVector3d &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); + LLControlVariable* declareVec4(const std::string& name, const LLVector4 &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); LLControlVariable* declareQuat(const std::string& name, const LLQuaternion &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); LLControlVariable* declareRect(const std::string& name, const LLRect &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); LLControlVariable* declareColor4(const std::string& name, const LLColor4 &initial_val, const std::string& comment, LLControlVariable::ePersist persist = LLControlVariable::PERSIST_NONDFT); @@ -212,6 +215,7 @@ public: LLWString getWString(std::string_view name); LLVector3 getVector3(std::string_view name); LLVector3d getVector3d(std::string_view name); + LLVector4 getVector4(std::string_view name); LLRect getRect(std::string_view name); LLSD getLLSD(std::string_view name); LLQuaternion getQuaternion(std::string_view name); @@ -250,6 +254,7 @@ public: void setString(std::string_view name, const std::string& val); void setVector3(std::string_view name, const LLVector3 &val); void setVector3d(std::string_view name, const LLVector3d &val); + void setVector4(std::string_view name, const LLVector4 &val); void setQuaternion(std::string_view name, const LLQuaternion &val); void setRect(std::string_view name, const LLRect &val); void setColor4(std::string_view name, const LLColor4 &val); @@ -416,6 +421,7 @@ template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); +template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); template <> eControlType get_control_type(); @@ -425,6 +431,7 @@ template <> eControlType get_control_type(); template <> LLSD convert_to_llsd(const U32& in); template <> LLSD convert_to_llsd(const LLVector3& in); template <> LLSD convert_to_llsd(const LLVector3d& in); +template <> LLSD convert_to_llsd(const LLVector4& in); template <> LLSD convert_to_llsd(const LLQuaternion& in); template <> LLSD convert_to_llsd(const LLRect& in); template <> LLSD convert_to_llsd(const LLColor4& in); @@ -434,6 +441,7 @@ template<> std::string convert_from_llsd(const LLSD& sd, eControlTy template<> LLWString convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template<> LLVector3 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template<> LLVector3d convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); +template<> LLVector4 convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template<> LLQuaternion convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template<> LLRect convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); template<> bool convert_from_llsd(const LLSD& sd, eControlType type, std::string_view control_name); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index a4a9323892..d7026d2dce 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11805,6 +11805,22 @@ Value 20.0 + TextureChannelPriority + + Comment + Per-channel texture streaming aggressiveness. X=normals, Y=diffuse, Z=specular/metallic, W=emissive. 1.0=baseline, higher=more aggressive downrez. + Persist + 1 + Type + Vector4 + Value + + 5 + 7.5 + 20 + 7.5 + + TextureCameraBoost Comment diff --git a/indra/newview/llfloatersettingsdebug.cpp b/indra/newview/llfloatersettingsdebug.cpp index 01108b5cfa..92b3c92165 100644 --- a/indra/newview/llfloatersettingsdebug.cpp +++ b/indra/newview/llfloatersettingsdebug.cpp @@ -106,6 +106,7 @@ void LLFloaterSettingsDebug::onCommitSettings() LLVector3 vector; LLVector3d vectord; + LLVector4 vector4; LLQuaternion quat; LLRect rect; LLColor4 col4; @@ -142,6 +143,13 @@ void LLFloaterSettingsDebug::onCommitSettings() vectord.mdV[VZ] = mValSpinner3->getValue().asReal(); controlp->set(vectord.getValue()); break; + case TYPE_VEC4: + vector4.mV[VX] = (F32)mValSpinner1->getValue().asReal(); + vector4.mV[VY] = (F32)mValSpinner2->getValue().asReal(); + vector4.mV[VZ] = (F32)mValSpinner3->getValue().asReal(); + vector4.mV[VW] = (F32)mValSpinner4->getValue().asReal(); + controlp->set(vector4.getValue()); + break; case TYPE_QUAT: quat.mQ[VX] = mValSpinner1->getValueF32(); quat.mQ[VY] = mValSpinner2->getValueF32(); @@ -352,6 +360,40 @@ void LLFloaterSettingsDebug::updateControl(LLControlVariable* controlp) } break; } + case TYPE_VEC4: + { + LLVector4 v; + v.setValue(sd); + mValSpinner1->setVisible(true); + mValSpinner1->setLabel(std::string("X")); + mValSpinner2->setVisible(true); + mValSpinner2->setLabel(std::string("Y")); + mValSpinner3->setVisible(true); + mValSpinner3->setLabel(std::string("Z")); + mValSpinner4->setVisible(true); + mValSpinner4->setLabel(std::string("W")); + if (!mValSpinner1->hasFocus()) + { + mValSpinner1->setPrecision(3); + mValSpinner1->setValue(v[VX]); + } + if (!mValSpinner2->hasFocus()) + { + mValSpinner2->setPrecision(3); + mValSpinner2->setValue(v[VY]); + } + if (!mValSpinner3->hasFocus()) + { + mValSpinner3->setPrecision(3); + mValSpinner3->setValue(v[VZ]); + } + if (!mValSpinner4->hasFocus()) + { + mValSpinner4->setPrecision(3); + mValSpinner4->setValue(v[VW]); + } + break; + } case TYPE_QUAT: { LLQuaternion q; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index e4fd947892..7dd32074cf 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1014,6 +1014,38 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag } imagep->addTextureStats(max_vsize); + + // Derive stream priority channel from face lists. + // Map render texture channels to priority channels: + // 0 = normal, 1 = diffuse, 2 = specular, 3 = emissive + { + static const S32 render_to_priority[] = { + 1, // DIFFUSE_MAP (0) + 0, // NORMAL_MAP / ALTERNATE_DIFFUSE_MAP (1) + 2, // SPECULAR_MAP (2) + 1, // BASECOLOR_MAP (3) + 2, // METALLIC_ROUGHNESS_MAP (4) + 0, // GLTF_NORMAL_MAP (5) + 3, // EMISSIVE_MAP (6) + }; + + S32 priority_channel = 1; // default to diffuse + for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i) + { + if (imagep->getNumFaces(i) > 0) + { + priority_channel = llmin(priority_channel, render_to_priority[i]); + } + } + + static LLCachedControl channel_priority(gSavedSettings, "TextureChannelPriority", + LLVector4(10.0f, 20.0f, 40.0f, 20.0f)); + F32 factor = llmax(channel_priority().mV[priority_channel], 0.1f); + if (factor != 1.0f) + { + imagep->mMaxVirtualSize /= factor; + } + } } #if 0 -- cgit v1.3 From b9e15a2a611c33e593caf72cd3512b32d7f7a9fe Mon Sep 17 00:00:00 2001 From: Cosmic Linden Date: Thu, 23 Apr 2026 10:53:10 -0700 Subject: secondlife/viewer#5702: Ensure non-visible preloaded media has PRIORITY_HIDDEN --- indra/newview/llmediactrl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llmediactrl.cpp b/indra/newview/llmediactrl.cpp index c7b60b2fd5..4a808ba052 100644 --- a/indra/newview/llmediactrl.cpp +++ b/indra/newview/llmediactrl.cpp @@ -739,7 +739,7 @@ bool LLMediaCtrl::ensureMediaSourceExists() mMediaSource->setUsedInUI(true); mMediaSource->setHomeURL(mHomePageUrl, mHomePageMimeType); mMediaSource->setTarget(mTarget); - mMediaSource->setVisible( getVisible() ); + mMediaSource->setVisible( isInVisibleChain() ); mMediaSource->addObserver( this ); mMediaSource->setBackgroundColor( getBackgroundColor() ); mMediaSource->setTrustedBrowser(mTrusted); -- cgit v1.3 From 5d5c74c27f7354dba499acb3534c64a9a7fa3dc7 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 29 Apr 2026 18:03:03 -0400 Subject: Get texture quality settings setup and tuned. --- indra/newview/app_settings/settings.xml | 13 ++++++++- indra/newview/featuretable.txt | 20 +++++++------- indra/newview/featuretable_mac.txt | 20 +++++++------- indra/newview/llviewercontrol.cpp | 32 ++++++++++++++++++++++ .../en/floater_preferences_graphics_advanced.xml | 32 ++++++++++++---------- 5 files changed, 82 insertions(+), 35 deletions(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d7026d2dce..9de8892c5e 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8005,13 +8005,24 @@ RenderMaxTextureResolution Comment - Maximum texture resolution to download for non-boosted textures. + Maximum texture resolution to download for non-boosted textures. Driven by RenderTextureQuality. Persist 1 Type U32 Value 2048 + + RenderTextureQuality + + Comment + Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution and TextureChannelPriority. + Persist + 1 + Type + U32 + Value + 2 RenderDownScaleMethod diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt index 1090dd8ffb..c408edf963 100644 --- a/indra/newview/featuretable.txt +++ b/indra/newview/featuretable.txt @@ -1,4 +1,4 @@ -version 74 +version 75 // The version number above should be incremented IF AND ONLY IF some // change has been made that is sufficiently important to justify // resetting the graphics preferences of all users to the recommended @@ -85,7 +85,7 @@ RenderExposure 1 4 RenderTonemapType 1 1 RenderTonemapMix 1 1 RenderDisableVintageMode 1 1 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -128,7 +128,7 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 512 +RenderTextureQuality 1 0 RenderReflectionProbeCount 1 1 // @@ -171,7 +171,7 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 1024 +RenderTextureQuality 1 0 RenderReflectionProbeCount 1 32 // @@ -213,7 +213,7 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -255,7 +255,7 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -297,7 +297,7 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 1 128 // @@ -339,7 +339,7 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 1 256 // @@ -381,7 +381,7 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -415,7 +415,7 @@ RenderShadowDetail 0 0 RenderReflectionProbeDetail 0 -1 RenderMirrors 0 0 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 0 0 list Intel diff --git a/indra/newview/featuretable_mac.txt b/indra/newview/featuretable_mac.txt index c3e2dd0c41..7bb0e3f7f1 100644 --- a/indra/newview/featuretable_mac.txt +++ b/indra/newview/featuretable_mac.txt @@ -1,4 +1,4 @@ -version 73 +version 74 // The version number above should be incremented IF AND ONLY IF some // change has been made that is sufficiently important to justify // resetting the graphics preferences of all users to the recommended @@ -85,7 +85,7 @@ RenderTonemapType 1 1 RenderTonemapMix 1 1 RenderDisableVintageMode 1 1 RenderDownScaleMethod 1 0 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -128,7 +128,7 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 512 +RenderTextureQuality 1 0 RenderReflectionProbeCount 1 1 // @@ -171,7 +171,7 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 1024 +RenderTextureQuality 1 0 RenderReflectionProbeCount 1 32 // @@ -213,7 +213,7 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -255,7 +255,7 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -297,7 +297,7 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 1 128 // @@ -339,7 +339,7 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 1 256 // @@ -381,7 +381,7 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -414,7 +414,7 @@ RenderDeferredSSAO 0 0 RenderShadowDetail 0 0 RenderMirrors 0 0 RenderDisableVintageMode 1 0 -RenderMaxTextureResolution 1 2048 +RenderTextureQuality 1 2 RenderReflectionProbeCount 0 0 list TexUnit8orLess diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 0c93b24751..d4d07f24ea 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -112,6 +112,37 @@ static bool handleRenderAvatarMouselookChanged(const LLSD& newvalue) return true; } +static bool handleRenderTextureQualityChanged(const LLSD& newvalue) +{ + // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives max-resolution and per-channel + // streaming aggressiveness. Channel order is X=normals, Y=diffuse, + // Z=specular/metallic, W=emissive (matches TextureChannelPriority). + U32 quality = (U32)newvalue.asInteger(); + U32 max_res = 2048; + LLVector4 channel_priority(5.f, 7.5f, 20.f, 7.5f); + switch (quality) + { + case 0: // Low + max_res = 1024; + channel_priority.setVec(20.f, 30.f, 80.f, 30.f); + break; + case 1: // Medium + channel_priority.setVec(10.f, 15.f, 40.f, 15.f); + break; + case 2: // High + // defaults above + break; + case 3: // Ultra + default: + if (quality > 3) quality = 3; + channel_priority.setVec(1.f, 1.f, 1.f, 1.f); + break; + } + gSavedSettings.setU32("RenderMaxTextureResolution", max_res); + gSavedSettings.setVector4("TextureChannelPriority", channel_priority); + return true; +} + static bool handleRenderFarClipChanged(const LLSD& newvalue) { if (LLStartUp::getStartupState() >= STATE_STARTED) @@ -815,6 +846,7 @@ void settings_setup_listeners() { LL_PROFILE_ZONE_SCOPED; setting_setup_signal_listener(gSavedSettings, "FirstPersonAvatarVisible", handleRenderAvatarMouselookChanged); + setting_setup_signal_listener(gSavedSettings, "RenderTextureQuality", handleRenderTextureQualityChanged); setting_setup_signal_listener(gSavedSettings, "RenderFarClip", handleRenderFarClipChanged); setting_setup_signal_listener(gSavedSettings, "RenderTerrainScale", handleTerrainScaleChanged); setting_setup_signal_listener(gSavedSettings, "RenderTerrainPBRScale", handlePBRTerrainScaleChanged); diff --git a/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml b/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml index 2a18134491..2942ce67ef 100644 --- a/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml +++ b/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml @@ -127,31 +127,35 @@ top_delta="16" left="30" width="160" - name="MaxTextureResolutionLabel" + name="TextureQualityLabel" text_readonly_color="LabelDisabledColor"> - Maximum LOD resolution: + Texture quality: + label="Low" + name="Low" + value="0"/> + + label="High" + name="High" + value="2"/> + label="Ultra" + name="Ultra" + value="3"/> Date: Mon, 13 Apr 2026 17:12:55 -0700 Subject: secondlife/viewer#5634: Apply version string from janus server if available. Also display the type of connection the version is applicable to. --- indra/newview/llvoicewebrtc.cpp | 69 +++++++++++++++++++++++++++++++++++++++++ indra/newview/llvoicewebrtc.h | 5 +++ 2 files changed, 74 insertions(+) (limited to 'indra') diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 3a700423b3..333046adce 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 2ce575852a..8efbd1778f 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -80,6 +80,7 @@ public: 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 @@ public: 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; -- cgit v1.3 From 186320e448459e57bb13c06d2605504a5ad695e6 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 12 May 2026 14:22:47 -0700 Subject: Remove Vivox-related code Remove the Vivox-related code altogether, including SLVoice. --- autobuild.xml | 58 - indra/cmake/Copy3rdPartyLibs.cmake | 60 - indra/cmake/ViewerMiscLibs.cmake | 2 - indra/newview/CMakeLists.txt | 16 - indra/newview/app_settings/settings.xml | 170 +- .../installers/windows/installer_template.nsi | 3 - indra/newview/llappviewer.cpp | 1 - indra/newview/llimview.cpp | 12 +- indra/newview/llspeakers.cpp | 2 +- indra/newview/llviewermenu.cpp | 1 - indra/newview/llviewerstats.cpp | 3 - indra/newview/llvoiceclient.cpp | 102 +- indra/newview/llvoiceclient.h | 11 +- indra/newview/llvoicevivox.cpp | 7926 -------------------- indra/newview/llvoicevivox.h | 1093 --- indra/newview/llvoicewebrtc.cpp | 6 - indra/newview/llvoicewebrtc.h | 1 - indra/newview/viewer_manifest.py | 28 - 18 files changed, 24 insertions(+), 9471 deletions(-) delete mode 100644 indra/newview/llvoicevivox.cpp delete mode 100644 indra/newview/llvoicevivox.h (limited to 'indra') diff --git a/autobuild.xml b/autobuild.xml index 9945672bd2..1e315b68e5 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2192,64 +2192,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors description Generated headers and sources for OpenXR loader. - slvoice - - platforms - - darwin64 - - archive - - hash - 1e70b06fe6eb9796097010871b32d8e95167e373 - hash_algorithm - sha1 - url - https://automated-builds-secondlife-com.s3.amazonaws.com/gh/secondlife/3p-slvoice/slvoice-4.10.0000.32327.5fc3fe7c.5942f08-darwin64-5942f08.tar.zst - - name - darwin64 - - linux64 - - archive - - hash - 92b0ae08832bd0e99c34ef8f3e6346ad - url - http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/613/1289/slvoice-3.2.0002.10426.500605-linux64-500605.tar.bz2 - - name - linux64 - - windows64 - - archive - - hash - ddfb7c30d9756915e8b26f44e2ee3a69ee87fb9a - hash_algorithm - sha1 - url - https://automated-builds-secondlife-com.s3.amazonaws.com/gh/secondlife/3p-slvoice/slvoice-4.10.0000.32327.5fc3fe7c.5942f08-windows64-5942f08.tar.zst - - name - windows64 - - - license - Mixed - license_file - LICENSES/vivox_licenses.txt - copyright - 2010 Vivox, including audio coding using Polycom¨ Siren14TM (ITU-T Rec. G.722.1 Annex C) - version - 4.10.0000.32327.5fc3fe7c.5942f08 - name - slvoice - description - Vivox SDK components - sse2neon platforms diff --git a/indra/cmake/Copy3rdPartyLibs.cmake b/indra/cmake/Copy3rdPartyLibs.cmake index 680f2f3ac2..667a04a1c5 100644 --- a/indra/cmake/Copy3rdPartyLibs.cmake +++ b/indra/cmake/Copy3rdPartyLibs.cmake @@ -28,29 +28,6 @@ endmacro() # set up platform specific lists of files that need to be copied ################################################################### if(WINDOWS) - #******************************* - # VIVOX - *NOTE: no debug version - set(vivox_lib_dir "${ARCH_PREBUILT_DIRS_RELEASE}") - - # ND, it seems there is no such thing defined. At least when building a viewer - # Does this maybe matter on some LL buildserver? Otherwise this and the snippet using slvoice_src_dir - # can all go - if( ARCH_PREBUILT_BIN_RELEASE ) - set(slvoice_src_dir "${ARCH_PREBUILT_BIN_RELEASE}") - endif() - set(slvoice_files SLVoice.exe ) - if (ADDRESS_SIZE EQUAL 64) - list(APPEND vivox_libs - vivoxsdk_x64.dll - ortp_x64.dll - ) - else (ADDRESS_SIZE EQUAL 64) - list(APPEND vivox_libs - vivoxsdk.dll - ortp.dll - ) - endif (ADDRESS_SIZE EQUAL 64) - #******************************* # Misc shared libs @@ -159,12 +136,6 @@ if(WINDOWS) endforeach() elseif(DARWIN) - set(vivox_lib_dir "${ARCH_PREBUILT_DIRS_RELEASE}") - set(slvoice_files SLVoice) - set(vivox_libs - libortp.dylib - libvivoxsdk.dylib - ) set(debug_src_dir "${ARCH_PREBUILT_DIRS_DEBUG}") set(debug_files ) @@ -188,15 +159,6 @@ elseif(LINUX) set(SHARED_LIB_STAGING_DIR_RELWITHDEBINFO "${SHARED_LIB_STAGING_DIR}") set(SHARED_LIB_STAGING_DIR_RELEASE "${SHARED_LIB_STAGING_DIR}") - set(vivox_lib_dir "${ARCH_PREBUILT_DIRS_RELEASE}") - set(vivox_libs - libsndfile.so.1 - libortp.so - libvivoxoal.so.1 - libvivoxsdk.so - ) - set(slvoice_files SLVoice) - # *TODO - update this to use LIBS_PREBUILT_DIR and LL_ARCH_DIR variables # or ARCH_PREBUILT_DIRS set(debug_src_dir "${ARCH_PREBUILT_DIRS_DEBUG}") @@ -226,8 +188,6 @@ elseif(LINUX) else(WINDOWS) message(STATUS "WARNING: unrecognized platform for staging 3rd party libs, skipping...") - set(vivox_lib_dir "${CMAKE_SOURCE_DIR}/newview/vivox-runtime/i686-linux") - set(vivox_libs "") # *TODO - update this to use LIBS_PREBUILT_DIR and LL_ARCH_DIR variables # or ARCH_PREBUILT_DIRS set(debug_src_dir "${CMAKE_SOURCE_DIR}/../libraries/i686-linux/lib/debug") @@ -249,26 +209,6 @@ endif(WINDOWS) # Done building the file lists, now set up the copy commands. ################################################################ -# Curiously, slvoice_files are only copied to SHARED_LIB_STAGING_DIR_RELEASE. -# It's unclear whether this is oversight or intentional, but anyway leave the -# single copy_if_different command rather than using to_staging_dirs. - -if( slvoice_src_dir ) - copy_if_different( - ${slvoice_src_dir} - "${SHARED_LIB_STAGING_DIR_RELEASE}" - out_targets - ${slvoice_files} - ) - list(APPEND third_party_targets ${out_targets}) -endif() - -to_staging_dirs( - ${vivox_lib_dir} - third_party_targets - ${vivox_libs} - ) - to_staging_dirs( ${release_src_dir} third_party_targets diff --git a/indra/cmake/ViewerMiscLibs.cmake b/indra/cmake/ViewerMiscLibs.cmake index cae68fbc11..f91222aa75 100644 --- a/indra/cmake/ViewerMiscLibs.cmake +++ b/indra/cmake/ViewerMiscLibs.cmake @@ -16,8 +16,6 @@ if( NOT USE_CONAN ) use_prebuilt_binary(libhunspell) endif() -use_prebuilt_binary(slvoice) - use_prebuilt_binary(nanosvg) use_prebuilt_binary(viewer-fonts) use_prebuilt_binary(emoji_shortcodes) diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 1d1553355e..861712fb58 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -723,7 +723,6 @@ set(viewer_SOURCE_FILES llvoicechannel.cpp llvoiceclient.cpp llvoicevisualizer.cpp - llvoicevivox.cpp llvoicewebrtc.cpp llvoinventorylistener.cpp llvopartgroup.cpp @@ -1396,7 +1395,6 @@ set(viewer_HEADER_FILES llvoicechannel.h llvoiceclient.h llvoicevisualizer.h - llvoicevivox.h llvoicewebrtc.h llvoinventorylistener.h llvopartgroup.h @@ -1785,9 +1783,7 @@ if (WINDOWS) ${CMAKE_SOURCE_DIR}/../scripts/messages/message_template.msg ${SHARED_LIB_STAGING_DIR}/openjp2.dll ${SHARED_LIB_STAGING_DIR}/llwebrtc.dll - #${SHARED_LIB_STAGING_DIR}/${LL_INTDIR}/SLVoice.exe #${SHARED_LIB_STAGING_DIR}/${LL_INTDIR}/libsndfile-1.dll - #${SHARED_LIB_STAGING_DIR}/${LL_INTDIR}/vivoxoal.dll ${AUTOBUILD_INSTALL_DIR}/ca-bundle.crt ${CMAKE_CURRENT_SOURCE_DIR}/licenses-win32.txt ${CMAKE_CURRENT_SOURCE_DIR}/featuretable.txt @@ -1798,18 +1794,6 @@ if (WINDOWS) media_plugin_example ) - if (ADDRESS_SIZE EQUAL 64) - list(APPEND COPY_INPUT_DEPENDENCIES - ${SHARED_LIB_STAGING_DIR}/vivoxsdk_x64.dll - ${SHARED_LIB_STAGING_DIR}/ortp_x64.dll - ) - else (ADDRESS_SIZE EQUAL 64) - list(APPEND COPY_INPUT_DEPENDENCIES - ${SHARED_LIB_STAGING_DIR}/vivoxsdk.dll - ${SHARED_LIB_STAGING_DIR}/ortp.dll - ) - endif (ADDRESS_SIZE EQUAL 64) - if (TARGET ll::discord_sdk) list(APPEND COPY_INPUT_DEPENDENCIES ${SHARED_LIB_STAGING_DIR}/discord_partner_sdk.dll diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d7026d2dce..c41a1e5b88 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -13498,141 +13498,8 @@ Value 1 - VivoxAutoPostCrashDumps - - Comment - If true, SLVoice will automatically send crash dumps directly to Vivox. - Persist - 1 - Type - Boolean - Value - 1 - - VivoxDebugLevel - - Comment - Logging level to use when launching the vivox daemon - Persist - 1 - Type - String - Value - 0 - - VivoxLogDirectory - - Comment - Default log path is Application Support/SecondLife/logs specify alternate absolute path here. - Persist - 1 - Type - String - Value - - - VivoxShutdownTimeout - - Comment - shutdown timeout in miliseconds. The amount of time to wait for the service to shutdown gracefully after the last disconnect - Persist - 1 - Type - String - Value - 5 - - VivoxDebugSIPURIHostName - - Comment - Hostname portion of vivox SIP URIs (empty string for the default). - Persist - 1 - Type - String - Value - - - VivoxDebugVoiceAccountServerURI - - Comment - URI to the vivox account management server (empty string for the default). - Persist - 1 - Type - String - Value - - - VivoxVoiceHost - - Comment - Client SLVoice host to connect to - Persist - 1 - Type - String - Value - 127.0.0.1 - - VivoxVoicePort - - Comment - Client SLVoice port to connect to - Persist - 1 - Type - U32 - Value - 44125 - - - VivoxVadAuto - - Comment - A flag indicating if the automatic VAD is enabled (1) or disabled (0). The individual settings are ignored if the auto-mode is enabled - Persist - 1 - Type - U32 - Value - 0 - - VivoxVadHangover - - Comment - The time (in milliseconds) that it takes or the VAD to switch back to silence from speech mode after the last speech frame has been detected - Persist - 1 - Type - U32 - Value - 2000 - - VivoxVadNoiseFloor - - Comment - A dimensionless value between 0 and 20000 (default 576) that controls the maximum level at which the noise floor may be set at by the VAD's noise tracking - Persist - 1 - Type - U32 - Value - 576 - - VivoxVadSensitivity - - Comment - A dimensionless value between 0 and 100, indicating the 'sensitivity of the VAD'. Increasing this value corresponds to decreasing the sensitivity of the VAD and 0 is turned off altogether - Persist - 1 - Type - U32 - Value - 0 - - VoiceCallsFriendsOnly - + VoiceCallsFriendsOnly + Comment (Deprecated) Only accept voice calls from residents on your friends list @@ -13709,17 +13576,6 @@ Value 0 - VoiceHost - - Comment - Client SLVoice host to connect to - Persist - 1 - Type - String - Value - 127.0.0.1 - VoiceImageLevel0 Comment @@ -13808,17 +13664,6 @@ Value Default - VoiceLogFile - - Comment - Log file to use when launching the voice daemon - Persist - 1 - Type - String - Value - - VoiceOutputAudioDevice Comment @@ -13841,17 +13686,6 @@ Value 10 - VoicePort - - Comment - Client SLVoice port to connect to - Persist - 1 - Type - U32 - Value - 44125 - VoiceEchoCancellation Comment diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi index 07ed0d0824..cf12be91c3 100644 --- a/indra/newview/installers/windows/installer_template.nsi +++ b/indra/newview/installers/windows/installer_template.nsi @@ -865,9 +865,6 @@ Delete "$INSTDIR\trial\*.html" Delete "$INSTDIR\newview.exe" Delete "$INSTDIR\SecondLife.exe" -# MAINT-3099 workaround - prevent these log files, if present, from causing a user alert -Delete "$INSTDIR\VivoxVoiceService-*.log" - # Remove entire help directory RMDir /r "$INSTDIR\help" diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 46e64c2a4e..59b8ff5c70 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -2983,7 +2983,6 @@ bool LLAppViewer::initConfiguration() { // This is the second instance of SL. Mute voice, // but make sure the setting is *not* persisted. - // Also see LLVivoxVoiceClient::voiceEnabled() LLControlVariable* enable_voice = gSavedSettings.getControl("EnableVoiceChat"); if (enable_voice) { diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp index ad01e11d48..252eb32e7c 100644 --- a/indra/newview/llimview.cpp +++ b/indra/newview/llimview.cpp @@ -89,8 +89,7 @@ const S32 XL8_PADDING = 3; // XL8_START_TAG.size() + XL8_END_TAG.size() const static U32 SESSION_INITIALIZATION_TIMEOUT = 30; // This enum corresponds to the sim's and adds P2P_CHAT_SESSION, -// as webrtc uses the multiagent chat mechanism for p2p calls, -// instead of relying on vivox calling. +// as webrtc uses the multiagent chat mechanism for p2p calls. // Don't change this without consulting a server developer. enum EMultiAgentChatSessionType { @@ -1943,12 +1942,6 @@ void LLIMModel::sendMessage(const std::string& utf8_text, info = LLAvatarTracker::instance().getBuddyInfo(other_participant_id); U8 offline = (!info || info->isOnline()) ? IM_ONLINE : IM_OFFLINE; - // Old call to send messages to SLim client, no longer supported. - //if((offline == IM_OFFLINE) && (LLVoiceClient::getInstance()->isOnlineSIP(other_participant_id))) - //{ - // // User is online through the OOW connector, but not with a regular viewer. Try to send the message via SLVoice. - // sent = LLVoiceClient::getInstance()->sendTextMessage(other_participant_id, utf8_text); - //} if(!sent) { @@ -3088,8 +3081,7 @@ void LLIncomingCallDialog::processCallResponse(S32 response, const LLSD &payload { if (type == IM_SESSION_P2P_INVITE) { - // decline p2p voice, either via the vivox-style call mechanism - // or via the webrtc-style "decline p2p" mechanism. + // decline p2p voice, via the webrtc-style "decline p2p" mechanism. LLVoiceP2PIncomingCallInterfacePtr call = LLVoiceClient::getInstance()->getIncomingCallInterface(payload["voice_channel_info"]); if (call) { diff --git a/indra/newview/llspeakers.cpp b/indra/newview/llspeakers.cpp index f079c70c6c..c75314825c 100644 --- a/indra/newview/llspeakers.cpp +++ b/indra/newview/llspeakers.cpp @@ -438,7 +438,7 @@ void LLSpeakerMgr::update(bool resort_ok) { if(speakerp->mType == LLSpeaker::SPEAKER_EXTERNAL) { - // external speakers should be timed out when they leave the voice channel (since they only exist via SLVoice) + // external speakers should be timed out when they leave the voice channel speakerp->mStatus = LLSpeaker::STATUS_NOT_IN_CHANNEL; } else diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index dbcf4fbbf4..5a79aa91bf 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -127,7 +127,6 @@ #include "llviewerstatsrecorder.h" #include "llvlcomposition.h" #include "llvoavatarself.h" -#include "llvoicevivox.h" #include "llworld.h" #include "llworldmap.h" #include "pipeline.h" diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 5193514fe8..4dcd1cc03a 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -62,7 +62,6 @@ #include "llsdserialize.h" #include "llsdutil.h" #include "llcorehttputil.h" -#include "llvoicevivox.h" #include "llinventorymodel.h" #include "lluiusage.h" #include "lltranslate.h" @@ -796,8 +795,6 @@ void send_viewer_stats(bool include_preferences) body["ui"] = LLUIUsage::instance().asLLSD(); - body["stats"]["voice"] = LLVoiceVivoxStats::getInstance()->read(); - // Misc stats, two strings and two ints // These are not expecticed to persist across multiple releases // Comment any changes with your name and the expected release revision diff --git a/indra/newview/llvoiceclient.cpp b/indra/newview/llvoiceclient.cpp index 71a9e71a9f..51a7204782 100644 --- a/indra/newview/llvoiceclient.cpp +++ b/indra/newview/llvoiceclient.cpp @@ -25,7 +25,6 @@ */ #include "llvoiceclient.h" -#include "llvoicevivox.h" #include "llvoicewebrtc.h" #include "llviewernetwork.h" #include "llviewercontrol.h" @@ -116,11 +115,7 @@ std::string LLVoiceClientStatusObserver::status2string(LLVoiceClientStatusObserv LLVoiceModuleInterface *getVoiceModule(const std::string &voice_server_type) { - if (voice_server_type == VIVOX_VOICE_SERVER_TYPE || voice_server_type.empty()) - { - return (LLVoiceModuleInterface *) LLVivoxVoiceClient::getInstance(); - } - else if (voice_server_type == WEBRTC_VOICE_SERVER_TYPE) + if (voice_server_type == WEBRTC_VOICE_SERVER_TYPE) { return (LLVoiceModuleInterface *) LLWebRTCVoiceClient::getInstance(); } @@ -166,7 +161,6 @@ void LLVoiceClient::init(LLPumpIO *pump) // Initialize all of the voice modules m_servicePump = pump; LLWebRTCVoiceClient::getInstance()->init(pump); - LLVivoxVoiceClient::getInstance()->init(pump); } void LLVoiceClient::userAuthorized(const std::string& user_id, const LLUUID &agentID) @@ -177,7 +171,6 @@ void LLVoiceClient::userAuthorized(const std::string& user_id, const LLUUID &age } mRegionChangedCallbackSlot = gAgent.addRegionChangedCallback(boost::bind(&LLVoiceClient::onRegionChanged, this)); LLWebRTCVoiceClient::getInstance()->userAuthorized(user_id, agentID); - LLVivoxVoiceClient::getInstance()->userAuthorized(user_id, agentID); } void LLVoiceClient::handleSimulatorFeaturesReceived(const LLSD &simulatorFeatures) @@ -185,7 +178,7 @@ void LLVoiceClient::handleSimulatorFeaturesReceived(const LLSD &simulatorFeature std::string voiceServerType = simulatorFeatures["VoiceServerType"].asString(); if (voiceServerType.empty()) { - voiceServerType = VIVOX_VOICE_SERVER_TYPE; + voiceServerType = WEBRTC_VOICE_SERVER_TYPE; } if (mSpatialVoiceModule && !mNonSpatialVoiceModule) @@ -287,19 +280,14 @@ void LLVoiceClient::setHidden(bool hidden) { LL_INFOS("Voice") << "( " << (hidden ? "true" : "false") << " )" << LL_ENDL; LLWebRTCVoiceClient::getInstance()->setHidden(hidden); - LLVivoxVoiceClient::getInstance()->setHidden(hidden); } void LLVoiceClient::terminate() { - if (LLVivoxVoiceClient::instanceExists()) + if (LLWebRTCVoiceClient::instanceExists()) { LLWebRTCVoiceClient::getInstance()->terminate(); } - if (LLVivoxVoiceClient::instanceExists()) - { - LLVivoxVoiceClient::getInstance()->terminate(); - } mSpatialVoiceModule = NULL; m_servicePump = NULL; @@ -335,7 +323,6 @@ void LLVoiceClient::updateSettings() updateMicMuteLogic(); LLWebRTCVoiceClient::getInstance()->updateSettings(); - LLVivoxVoiceClient::getInstance()->updateSettings(); } //-------------------------------------------------- @@ -344,13 +331,11 @@ void LLVoiceClient::updateSettings() void LLVoiceClient::tuningStart() { LLWebRTCVoiceClient::getInstance()->tuningStart(); - LLVivoxVoiceClient::getInstance()->tuningStart(); } void LLVoiceClient::tuningStop() { LLWebRTCVoiceClient::getInstance()->tuningStop(); - LLVivoxVoiceClient::getInstance()->tuningStop(); } bool LLVoiceClient::inTuningMode() @@ -393,13 +378,11 @@ void LLVoiceClient::refreshDeviceLists(bool clearCurrentList) void LLVoiceClient::setCaptureDevice(const std::string& name) { - LLVivoxVoiceClient::getInstance()->setCaptureDevice(name); LLWebRTCVoiceClient::getInstance()->setCaptureDevice(name); } void LLVoiceClient::setRenderDevice(const std::string& name) { - LLVivoxVoiceClient::getInstance()->setRenderDevice(name); LLWebRTCVoiceClient::getInstance()->setRenderDevice(name); } @@ -421,13 +404,11 @@ const LLVoiceDeviceList& LLVoiceClient::getRenderDevices() void LLVoiceClient::getParticipantList(std::set &participants) const { LLWebRTCVoiceClient::getInstance()->getParticipantList(participants); - LLVivoxVoiceClient::getInstance()->getParticipantList(participants); } bool LLVoiceClient::isParticipant(const LLUUID &speaker_id) const { - return LLWebRTCVoiceClient::getInstance()->isParticipant(speaker_id) || - LLVivoxVoiceClient::getInstance()->isParticipant(speaker_id); + return LLWebRTCVoiceClient::getInstance()->isParticipant(speaker_id); } @@ -519,14 +500,12 @@ void LLVoiceClient::activateSpatialChannel(bool activate) bool LLVoiceClient::isCurrentChannel(const LLSD& channelInfo) { - return LLWebRTCVoiceClient::getInstance()->isCurrentChannel(channelInfo) || - LLVivoxVoiceClient::getInstance()->isCurrentChannel(channelInfo); + return LLWebRTCVoiceClient::getInstance()->isCurrentChannel(channelInfo); } bool LLVoiceClient::compareChannels(const LLSD &channelInfo1, const LLSD &channelInfo2) { - return LLWebRTCVoiceClient::getInstance()->compareChannels(channelInfo1, channelInfo2) || - LLVivoxVoiceClient::getInstance()->compareChannels(channelInfo1, channelInfo2); + return LLWebRTCVoiceClient::getInstance()->compareChannels(channelInfo1, channelInfo2); } LLVoiceP2PIncomingCallInterfacePtr LLVoiceClient::getIncomingCallInterface(const LLSD& voice_call_info) @@ -554,8 +533,8 @@ LLVoiceP2POutgoingCallInterface *LLVoiceClient::getOutgoingCallInterface(const L if (voiceChannelInfo.has("voice_server_type") && voiceChannelInfo["voice_server_type"] != voice_server_type) { // there's a mismatch between what the peer is offering and what our server - // can handle, so downgrade to vivox - voice_server_type = VIVOX_VOICE_SERVER_TYPE; + // can handle, so default to webrtc + voice_server_type = WEBRTC_VOICE_SERVER_TYPE; } LLVoiceModuleInterface *module = getVoiceModule(voice_server_type); return dynamic_cast(module); @@ -568,13 +547,11 @@ LLVoiceP2POutgoingCallInterface *LLVoiceClient::getOutgoingCallInterface(const L void LLVoiceClient::setVoiceVolume(F32 volume) { LLWebRTCVoiceClient::getInstance()->setVoiceVolume(volume); - LLVivoxVoiceClient::getInstance()->setVoiceVolume(volume); } void LLVoiceClient::setMicGain(F32 gain) { LLWebRTCVoiceClient::getInstance()->setMicGain(gain); - LLVivoxVoiceClient::getInstance()->setMicGain(gain); } @@ -624,10 +601,6 @@ void LLVoiceClient::setVoiceEnabled(bool enabled) { LLWebRTCVoiceClient::getInstance()->setVoiceEnabled(enabled); } - if (LLVivoxVoiceClient::instanceExists()) - { - LLVivoxVoiceClient::getInstance()->setVoiceEnabled(enabled); - } } void LLVoiceClient::updateMicMuteLogic() @@ -647,7 +620,6 @@ void LLVoiceClient::updateMicMuteLogic() new_mic_mute = true; } LLWebRTCVoiceClient::getInstance()->setMuteMic(new_mic_mute); - LLVivoxVoiceClient::getInstance()->setMuteMic(new_mic_mute); } void LLVoiceClient::setMuteMic(bool muted) @@ -742,17 +714,12 @@ bool LLVoiceClient::getVoiceEnabled(const LLUUID& id) const std::string LLVoiceClient::getDisplayName(const LLUUID& id) const { std::string result = LLWebRTCVoiceClient::getInstance()->getDisplayName(id); - if (result.empty()) - { - result = LLVivoxVoiceClient::getInstance()->getDisplayName(id); - } return result; } bool LLVoiceClient::isVoiceWorking() const { - return LLVivoxVoiceClient::getInstance()->isVoiceWorking() || - LLWebRTCVoiceClient::getInstance()->isVoiceWorking(); + return LLWebRTCVoiceClient::getInstance()->isVoiceWorking(); } bool LLVoiceClient::isParticipantAvatar(const LLUUID& id) @@ -767,22 +734,19 @@ bool LLVoiceClient::isOnlineSIP(const LLUUID& id) bool LLVoiceClient::getIsSpeaking(const LLUUID& id) { - return LLWebRTCVoiceClient::getInstance()->getIsSpeaking(id) || - LLVivoxVoiceClient::getInstance()->getIsSpeaking(id); + return LLWebRTCVoiceClient::getInstance()->getIsSpeaking(id); } bool LLVoiceClient::getIsModeratorMuted(const LLUUID& id) { // don't bother worrying about p2p calls, as // p2p calls don't have mute. - return LLWebRTCVoiceClient::getInstance()->getIsModeratorMuted(id) || - LLVivoxVoiceClient::getInstance()->getIsModeratorMuted(id); + return LLWebRTCVoiceClient::getInstance()->getIsModeratorMuted(id); } F32 LLVoiceClient::getCurrentPower(const LLUUID& id) { - return std::fmax(LLVivoxVoiceClient::getInstance()->getCurrentPower(id), - LLWebRTCVoiceClient::getInstance()->getCurrentPower(id)); + return LLWebRTCVoiceClient::getInstance()->getCurrentPower(id); } bool LLVoiceClient::getOnMuteList(const LLUUID& id) @@ -794,13 +758,12 @@ bool LLVoiceClient::getOnMuteList(const LLUUID& id) F32 LLVoiceClient::getUserVolume(const LLUUID& id) { - return std::fmax(LLVivoxVoiceClient::getInstance()->getUserVolume(id), LLWebRTCVoiceClient::getInstance()->getUserVolume(id)); + return LLWebRTCVoiceClient::getInstance()->getUserVolume(id); } void LLVoiceClient::setUserVolume(const LLUUID& id, F32 volume) { LLWebRTCVoiceClient::getInstance()->setUserVolume(id, volume); - LLVivoxVoiceClient::getInstance()->setUserVolume(id, volume); } //-------------------------------------------------- @@ -808,16 +771,11 @@ void LLVoiceClient::setUserVolume(const LLUUID& id, F32 volume) void LLVoiceClient::addObserver(LLVoiceClientStatusObserver* observer) { - LLVivoxVoiceClient::getInstance()->addObserver(observer); LLWebRTCVoiceClient::getInstance()->addObserver(observer); } void LLVoiceClient::removeObserver(LLVoiceClientStatusObserver* observer) { - if (LLVivoxVoiceClient::instanceExists()) - { - LLVivoxVoiceClient::getInstance()->removeObserver(observer); - } if (LLWebRTCVoiceClient::instanceExists()) { LLWebRTCVoiceClient::getInstance()->removeObserver(observer); @@ -826,16 +784,11 @@ void LLVoiceClient::removeObserver(LLVoiceClientStatusObserver* observer) void LLVoiceClient::addObserver(LLFriendObserver* observer) { - LLVivoxVoiceClient::getInstance()->addObserver(observer); LLWebRTCVoiceClient::getInstance()->addObserver(observer); } void LLVoiceClient::removeObserver(LLFriendObserver* observer) { - if (LLVivoxVoiceClient::instanceExists()) - { - LLVivoxVoiceClient::getInstance()->removeObserver(observer); - } if (LLWebRTCVoiceClient::instanceExists()) { LLWebRTCVoiceClient::getInstance()->removeObserver(observer); @@ -844,38 +797,17 @@ void LLVoiceClient::removeObserver(LLFriendObserver* observer) void LLVoiceClient::addObserver(LLVoiceClientParticipantObserver* observer) { - LLVivoxVoiceClient::getInstance()->addObserver(observer); LLWebRTCVoiceClient::getInstance()->addObserver(observer); } void LLVoiceClient::removeObserver(LLVoiceClientParticipantObserver* observer) { - if (LLVivoxVoiceClient::instanceExists()) - { - LLVivoxVoiceClient::getInstance()->removeObserver(observer); - } if (LLWebRTCVoiceClient::instanceExists()) { LLWebRTCVoiceClient::getInstance()->removeObserver(observer); } } -std::string LLVoiceClient::sipURIFromID(const LLUUID &id) const -{ - if (mNonSpatialVoiceModule) - { - return mNonSpatialVoiceModule->sipURIFromID(id); - } - else if (mSpatialVoiceModule) - { - return mSpatialVoiceModule->sipURIFromID(id); - } - else - { - return std::string(); - } -} - LLSD LLVoiceClient::getP2PChannelInfoTemplate(const LLUUID& id) const { if (mNonSpatialVoiceModule) @@ -908,7 +840,7 @@ class LLViewerRequiredVoiceVersion : public LLHTTPNode const LLSD& context, const LLSD& input) const { - std::string voice_server_type = "vivox"; + std::string voice_server_type = "webrtc"; if (input.has("body") && input["body"].has("voice_server_type")) { voice_server_type = input["body"]["voice_server_type"].asString(); @@ -916,11 +848,7 @@ class LLViewerRequiredVoiceVersion : public LLHTTPNode LLVoiceModuleInterface *voiceModule = NULL; - if (voice_server_type == "vivox" || voice_server_type.empty()) - { - voiceModule = (LLVoiceModuleInterface *) LLVivoxVoiceClient::getInstance(); - } - else if (voice_server_type == "webrtc") + if (voice_server_type == "webrtc" || voice_server_type.empty()) { voiceModule = (LLVoiceModuleInterface *) LLWebRTCVoiceClient::getInstance(); } diff --git a/indra/newview/llvoiceclient.h b/indra/newview/llvoiceclient.h index 788ea3b3b3..70bce055f9 100644 --- a/indra/newview/llvoiceclient.h +++ b/indra/newview/llvoiceclient.h @@ -106,7 +106,7 @@ struct LLVoiceVersionInfo /// @class LLVoiceP2POutgoingCallInterface /// @brief Outgoing call interface /// -/// For providers that support P2P signaling (vivox) +/// For providers that support P2P signaling ///////////////////////////////// class LLVoiceP2POutgoingCallInterface @@ -121,7 +121,7 @@ class LLVoiceP2POutgoingCallInterface /// @class LLVoiceP2PIncomingCallInterface /// @brief Incoming call interface /// -/// For providers that support P2P signaling (vivox) +/// For providers that support P2P signaling ///////////////////////////////// class LLVoiceP2PIncomingCallInterface { @@ -180,7 +180,6 @@ public: virtual bool deviceSettingsAvailable()=0; virtual bool deviceSettingsUpdated() = 0; - // Requery the vivox daemon for the current list of input/output devices. // If you pass true for clearCurrentList, deviceSettingsAvailable() will be false until the query has completed // (use this if you want to know when it's done). // If you pass false, you'll have no way to know when the query finishes, but the device lists will not appear empty in the interim. @@ -280,7 +279,6 @@ public: virtual void removeObserver(LLVoiceClientParticipantObserver* observer)=0; //@} - virtual std::string sipURIFromID(const LLUUID &id) const=0; virtual LLSD getP2PChannelInfoTemplate(const LLUUID& id) const=0; //@} @@ -384,7 +382,7 @@ public: bool deviceSettingsAvailable(); bool deviceSettingsUpdated(); // returns true when the device list has been updated recently. - // Requery the vivox daemon for the current list of input/output devices. + // Requery the voice driver for the current list of input/output devices. // If you pass true for clearCurrentList, deviceSettingsAvailable() will be false until the query has completed // (use this if you want to know when it's done). // If you pass false, you'll have no way to know when the query finishes, but the device lists will not appear empty in the interim. @@ -488,7 +486,6 @@ public: static void addObserver(LLVoiceClientParticipantObserver* observer); static void removeObserver(LLVoiceClientParticipantObserver* observer); - std::string sipURIFromID(const LLUUID &id) const; LLSD getP2PChannelInfoTemplate(const LLUUID& id) const; ////////////////////////// @@ -513,7 +510,7 @@ protected: LLVoiceModuleInterface* mSpatialVoiceModule; LLVoiceModuleInterface* mNonSpatialVoiceModule; - LLSD mSpatialCredentials; // used to store spatial credentials for vivox + LLSD mSpatialCredentials; // used to store spatial credentials for the voice subsystem // so they're available when the region voice // server is retrieved. LLPumpIO *m_servicePump; diff --git a/indra/newview/llvoicevivox.cpp b/indra/newview/llvoicevivox.cpp deleted file mode 100644 index 107a08fdf8..0000000000 --- a/indra/newview/llvoicevivox.cpp +++ /dev/null @@ -1,7926 +0,0 @@ - /** - * @file LLVivoxVoiceClient.cpp - * @brief Implementation of LLVivoxVoiceClient class which is the interface to the voice client process. - * - * $LicenseInfo:firstyear=2001&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "llviewerprecompiledheaders.h" -#include -#include "llvoicevivox.h" - -#include "llsdutil.h" - -// Linden library includes -#include "llavatarnamecache.h" -#include "llvoavatarself.h" -#include "llbufferstream.h" -#include "llfile.h" -#include "llmenugl.h" -#ifdef LL_USESYSTEMLIBS -# include "expat.h" -#else -# include "expat/expat.h" -#endif -#include "llcallbacklist.h" -#include "llviewerregion.h" -#include "llviewernetwork.h" // for gGridChoice -#include "llbase64.h" -#include "llviewercontrol.h" -#include "llappviewer.h" // for gDisconnected, gDisableVoice -#include "llprocess.h" - -// Viewer includes -#include "llmutelist.h" // to check for muted avatars -#include "llagent.h" -#include "llcachename.h" -#include "llimview.h" // for LLIMMgr -#include "llparcel.h" -#include "llviewerparcelmgr.h" -#include "llfirstuse.h" -#include "llspeakers.h" -#include "lltrans.h" -#include "llviewerwindow.h" -#include "llviewercamera.h" -#include "llversioninfo.h" - -#include "llviewernetwork.h" -#include "llnotificationsutil.h" - -#include "llcorehttputil.h" -#include "lleventfilter.h" - -#include "stringize.h" - -// for base64 decoding -#include "apr_base64.h" - -#define USE_SESSION_GROUPS 0 -#define VX_NULL_POSITION -2147483648.0 /*The Silence*/ - -extern LLMenuBarGL* gMenuBarView; -extern void handle_voice_morphing_subscribe(); - -const std::string VIVOX_VOICE_SERVER_TYPE = "vivox"; - -namespace { - const F32 VOLUME_SCALE_VIVOX = 0.01f; - - const F32 SPEAKING_TIMEOUT = 1.f; - - static const std::string VISIBLE_VOICE_SERVER_TYPE = "Vivox"; - - // Don't retry connecting to the daemon more frequently than this: - const F32 DAEMON_CONNECT_THROTTLE_SECONDS = 1.0f; - const int DAEMON_CONNECT_RETRY_MAX = 3; - - // Don't send positional updates more frequently than this: - const F32 UPDATE_THROTTLE_SECONDS = 0.5f; - - // Timeout for connection to Vivox - const F32 CONNECT_ATTEMPT_TIMEOUT = 300.0f; - const F32 CONNECT_DNS_TIMEOUT = 5.0f; - const int CONNECT_RETRY_MAX = 3; - - const F32 LOGIN_ATTEMPT_TIMEOUT = 30.0f; - const F32 LOGOUT_ATTEMPT_TIMEOUT = 5.0f; - const int LOGIN_RETRY_MAX = 3; - - const F32 PROVISION_RETRY_TIMEOUT = 2.0; - const int PROVISION_RETRY_MAX = 5; - - // Cosine of a "trivially" small angle - const F32 FOUR_DEGREES = 4.0f * (F_PI / 180.0f); - const F32 MINUSCULE_ANGLE_COS = (F32) cos(0.5f * FOUR_DEGREES); - - const F32 SESSION_JOIN_TIMEOUT = 30.0f; - - // Defines the maximum number of times(in a row) "stateJoiningSession" case for spatial channel is reached in stateMachine() - // which is treated as normal. The is the number of frames to wait for a channel join before giving up. This was changed - // from the original count of 50 for two reason. Modern PCs have higher frame rates and sometimes the SLVoice process - // backs up processing join requests. There is a log statement that records when channel joins take longer than 100 frames. - const int MAX_NORMAL_JOINING_SPATIAL_NUM = 1500; - - // How often to check for expired voice fonts in seconds - const F32 VOICE_FONT_EXPIRY_INTERVAL = 10.f; - // Time of day at which Vivox expires voice font subscriptions. - // Used to replace the time portion of received expiry timestamps. - static const std::string VOICE_FONT_EXPIRY_TIME = "T05:00:00Z"; - - // Maximum length of capture buffer recordings in seconds. - const F32 CAPTURE_BUFFER_MAX_TIME = 10.f; - - const int ERROR_VIVOX_OBJECT_NOT_FOUND = 1001; - const int ERROR_VIVOX_NOT_LOGGED_IN = 1007; -} - -static int scale_mic_volume(float volume) -{ - // incoming volume has the range [0.0 ... 2.0], with 1.0 as the default. - // Map it to Vivox levels as follows: 0.0 -> 30, 1.0 -> 50, 2.0 -> 70 - return 30 + (int)(volume * 20.0f); -} - -static int scale_speaker_volume(float volume) -{ - // incoming volume has the range [0.0 ... 1.0], with 0.5 as the default. - // Map it to Vivox levels as follows: 0.0 -> 30, 0.5 -> 50, 1.0 -> 70 - return 30 + (int)(volume * 40.0f); - -} - - -/////////////////////////////////////////////////////////////////////////////////////////////// - -class LLVivoxVoiceClientMuteListObserver : public LLMuteListObserver -{ - /* virtual */ void onChange() { LLVivoxVoiceClient::getInstance()->muteListChanged();} -}; - - -void LLVoiceVivoxStats::reset() -{ - mStartTime = -1.0f; - mConnectCycles = 0; - mConnectTime = -1.0f; - mConnectAttempts = 0; - mProvisionTime = -1.0f; - mProvisionAttempts = 0; - mEstablishTime = -1.0f; - mEstablishAttempts = 0; -} - -LLVoiceVivoxStats::LLVoiceVivoxStats() -{ - reset(); -} - -LLVoiceVivoxStats::~LLVoiceVivoxStats() -{ -} - -void LLVoiceVivoxStats::connectionAttemptStart() -{ - if (!mConnectAttempts) - { - mStartTime = LLTimer::getTotalTime(); - mConnectCycles++; - } - mConnectAttempts++; -} - -void LLVoiceVivoxStats::connectionAttemptEnd(bool success) -{ - if ( success ) - { - mConnectTime = (LLTimer::getTotalTime() - mStartTime) / USEC_PER_SEC; - } -} - -void LLVoiceVivoxStats::provisionAttemptStart() -{ - if (!mProvisionAttempts) - { - mStartTime = LLTimer::getTotalTime(); - } - mProvisionAttempts++; -} - -void LLVoiceVivoxStats::provisionAttemptEnd(bool success) -{ - if ( success ) - { - mProvisionTime = (LLTimer::getTotalTime() - mStartTime) / USEC_PER_SEC; - } -} - -void LLVoiceVivoxStats::establishAttemptStart() -{ - if (!mEstablishAttempts) - { - mStartTime = LLTimer::getTotalTime(); - } - mEstablishAttempts++; -} - -void LLVoiceVivoxStats::establishAttemptEnd(bool success) -{ - if ( success ) - { - mEstablishTime = (LLTimer::getTotalTime() - mStartTime) / USEC_PER_SEC; - } -} - -LLSD LLVoiceVivoxStats::read() -{ - LLSD stats(LLSD::emptyMap()); - - stats["connect_cycles"] = LLSD::Integer(mConnectCycles); - stats["connect_attempts"] = LLSD::Integer(mConnectAttempts); - stats["connect_time"] = LLSD::Real(mConnectTime); - - stats["provision_attempts"] = LLSD::Integer(mProvisionAttempts); - stats["provision_time"] = LLSD::Real(mProvisionTime); - - stats["establish_attempts"] = LLSD::Integer(mEstablishAttempts); - stats["establish_time"] = LLSD::Real(mEstablishTime); - - return stats; -} - -static LLVivoxVoiceClientMuteListObserver mutelist_listener; -static bool sMuteListListener_listening = false; - -/////////////////////////////////////////////////////////////////////////////////////////////// -static LLProcessPtr sGatewayPtr; -static LLEventStream sGatewayPump("VivoxDaemonPump", true); - -static bool isGatewayRunning() -{ - return sGatewayPtr && sGatewayPtr->isRunning(); -} - -static void killGateway() -{ - if (sGatewayPtr) - { - LL_DEBUGS("Voice") << "SLVoice " << sGatewayPtr->getStatusString() << LL_ENDL; - - sGatewayPump.stopListening("VivoxDaemonPump"); - sGatewayPtr->kill(__FUNCTION__); - sGatewayPtr=NULL; - } - else - { - LL_DEBUGS("Voice") << "no gateway" << LL_ENDL; - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////// - -bool LLVivoxVoiceClient::sShuttingDown = false; -bool LLVivoxVoiceClient::sConnected = false; -LLPumpIO *LLVivoxVoiceClient::sPump = nullptr; - -LLVivoxVoiceClient::LLVivoxVoiceClient() : - mWriteOffset(0), - mHidden(true), - mSessionTerminateRequested(false), - mRelogRequested(false), - mTerminateDaemon(false), - mSpatialJoiningNum(0), - - mTuningMode(false), - mTuningEnergy(0.0f), - mTuningMicVolume(0), - mTuningMicVolumeDirty(true), - mTuningSpeakerVolume(50), // Set to 50 so the user can hear himself when he sets his mic volume - mTuningSpeakerVolumeDirty(true), - mDevicesListUpdated(false), - - mAudioSession(), // TBD - should be NULL - mNextAudioSession(), - - mCurrentParcelLocalID(0), - mConnectorEstablished(false), - mAccountLoggedIn(false), - mNumberOfAliases(0), - mCommandCookie(0), - mLoginRetryCount(0), - - mBuddyListMapPopulated(false), - mBlockRulesListReceived(false), - mAutoAcceptRulesListReceived(false), - - mCaptureDeviceDirty(false), - mRenderDeviceDirty(false), - mSpatialCoordsDirty(false), - mIsInitialized(false), - - mMuteMic(false), - mMuteMicDirty(false), - mFriendsListDirty(true), - - mEarLocation(0), - mSpeakerVolumeDirty(true), - mSpeakerMuteDirty(true), - mMicVolume(0), - mMicVolumeDirty(true), - - mVoiceEnabled(false), - mProcessChannels(false), - mWriteInProgress(false), - - mVoiceFontsReceived(false), - mVoiceFontsNew(false), - mVoiceFontListDirty(false), - - mCaptureBufferMode(false), - mCaptureBufferRecording(false), - mCaptureBufferRecorded(false), - mCaptureBufferPlaying(false), - mShutdownComplete(true), - mPlayRequestCount(0), - - mAvatarNameCacheConnection(), - mIsInTuningMode(false), - mIsInChannel(false), - mIsJoiningSession(false), - mIsWaitingForFonts(false), - mIsLoggingIn(false), - mIsLoggedIn(false), - mIsProcessingChannels(false), - mIsCoroutineActive(false), - mVivoxPump("vivoxClientPump") -{ - sShuttingDown = false; - sConnected = false; - sPump = nullptr; - - mSpeakerVolume = scale_speaker_volume(0); - - mVoiceVersion.serverVersion = ""; - mVoiceVersion.voiceServerType = VISIBLE_VOICE_SERVER_TYPE; - mVoiceVersion.internalVoiceServerType = VIVOX_VOICE_SERVER_TYPE; - mVoiceVersion.majorVersion = 1; - mVoiceVersion.minorVersion = 0; - mVoiceVersion.mBuildVersion = ""; - mVoiceVersion.serverVersion = ""; - - // gMuteListp isn't set up at this point, so we defer this until later. -// gMuteListp->addObserver(&mutelist_listener); - - -#if LL_DARWIN || LL_LINUX - // HACK: THIS DOES NOT BELONG HERE - // When the vivox daemon dies, the next write attempt on our socket generates a SIGPIPE, which kills us. - // This should cause us to ignore SIGPIPE and handle the error through proper channels. - // This should really be set up elsewhere. Where should it go? - signal(SIGPIPE, SIG_IGN); - - // Since we're now launching the gateway with fork/exec instead of system(), we need to deal with zombie processes. - // Ignoring SIGCHLD should prevent zombies from being created. Alternately, we could use wait(), but I'd rather not do that. - signal(SIGCHLD, SIG_IGN); -#endif - - - gIdleCallbacks.addFunction(idle, this); -} - -//--------------------------------------------------- - -LLVivoxVoiceClient::~LLVivoxVoiceClient() -{ - if (mAvatarNameCacheConnection.connected()) - { - mAvatarNameCacheConnection.disconnect(); - } - sShuttingDown = true; -} - -//--------------------------------------------------- - -void LLVivoxVoiceClient::init(LLPumpIO *pump) -{ - // constructor will set up LLVoiceClient::getInstance() - sPump = pump; - -// LLCoros::instance().launch("LLVivoxVoiceClient::voiceControlCoro", -// boost::bind(&LLVivoxVoiceClient::voiceControlCoro, LLVivoxVoiceClient::getInstance())); - -} - -void LLVivoxVoiceClient::terminate() -{ - if (sShuttingDown) - { - return; - } - - // needs to be done manually here since we will not get another pass in - // coroutines... that mechanism is long since gone. - if (mIsLoggedIn) - { - logoutOfVivox(false); - } - - if(sConnected) - { - breakVoiceConnection(false); - sConnected = false; - } - else - { - mRelogRequested = false; - killGateway(); - } - - sShuttingDown = true; - sPump = NULL; -} - -//--------------------------------------------------- - -void LLVivoxVoiceClient::cleanUp() -{ - LL_DEBUGS("Voice") << LL_ENDL; - - deleteAllSessions(); - deleteAllVoiceFonts(); - deleteVoiceFontTemplates(); - LL_DEBUGS("Voice") << "exiting" << LL_ENDL; -} - -//--------------------------------------------------- - -const LLVoiceVersionInfo& LLVivoxVoiceClient::getVersion() -{ - return mVoiceVersion; -} - -//--------------------------------------------------- - -void LLVivoxVoiceClient::updateSettings() -{ - setVoiceEnabled(LLVoiceClient::getInstance()->voiceEnabled()); - setEarLocation(gSavedSettings.getS32("VoiceEarLocation")); - - std::string inputDevice = gSavedSettings.getString("VoiceInputAudioDevice"); - setCaptureDevice(inputDevice); - std::string outputDevice = gSavedSettings.getString("VoiceOutputAudioDevice"); - setRenderDevice(outputDevice); - F32 mic_level = gSavedSettings.getF32("AudioLevelMic"); - setMicGain(mic_level); -} - -///////////////////////////// -// utility functions - -bool LLVivoxVoiceClient::writeString(const std::string &str) -{ - bool result = false; - LL_DEBUGS("LowVoice") << "sending:\n" << str << LL_ENDL; - - if(sConnected) - { - apr_status_t err; - apr_size_t size = (apr_size_t)str.size(); - apr_size_t written = size; - - //MARK: Turn this on to log outgoing XML - // LL_DEBUGS("Voice") << "sending: " << str << LL_ENDL; - - // check return code - sockets will fail (broken, etc.) - err = apr_socket_send( - mSocket->getSocket(), - (const char*)str.data(), - &written); - - if(err == 0 && written == size) - { - // Success. - result = true; - } - else if (err == 0 && written != size) { - // Did a short write, log it for now - LL_WARNS("Voice") << ") short write on socket sending data to vivox daemon." << "Sent " << written << "bytes instead of " << size <getExpandedFilename(LL_PATH_LOGS, ""); - - // Transition to stateConnectorStarted when the connector handle comes back. - std::string vivoxLogLevel = gSavedSettings.getString("VivoxDebugLevel"); - if ( vivoxLogLevel.empty() ) - { - vivoxLogLevel = "0"; - } - LL_DEBUGS("Voice") << "creating connector with log level " << vivoxLogLevel << LL_ENDL; - - stream - << "" - << "V2 SDK" - << "" << mVoiceAccountServerURI << "" - << "Normal" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" - << "" << logdir << "" - << "Connector" - << ".log" - << "" << vivoxLogLevel << "" - << "" - << "" << LLVersionInfo::instance().getChannel() << " " << LLVersionInfo::instance().getVersion() << "" - //<< "" //Name can cause problems per vivox. - << "12" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::connectorShutdown() -{ - if(mConnectorEstablished) - { - std::ostringstream stream; - stream - << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" - << "\n\n\n"; - - mShutdownComplete = false; - mConnectorEstablished = false; - - writeString(stream.str()); - } - else - { - mShutdownComplete = true; - } -} - -void LLVivoxVoiceClient::userAuthorized(const std::string& user_id, const LLUUID &agentID) -{ - - mAccountDisplayName = user_id; - - LL_INFOS("Voice") << "name \"" << mAccountDisplayName << "\" , ID " << agentID << LL_ENDL; - - mAccountName = nameFromID(agentID); -} - -void LLVivoxVoiceClient::setLoginInfo( - const std::string& account_name, - const std::string& password, - const std::string& voice_sip_uri_hostname, - const std::string& voice_account_server_uri) -{ - mVoiceSIPURIHostName = voice_sip_uri_hostname; - mVoiceAccountServerURI = voice_account_server_uri; - - if(mAccountLoggedIn) - { - // Already logged in. - LL_WARNS("Voice") << "Called while already logged in." << LL_ENDL; - - // Don't process another login. - return; - } - else if ( account_name != mAccountName ) - { - LL_WARNS("Voice") << "Mismatched account name! " << account_name - << " instead of " << mAccountName << LL_ENDL; - } - else - { - mAccountPassword = password; - } - - std::string debugSIPURIHostName = gSavedSettings.getString("VivoxDebugSIPURIHostName"); - - if( !debugSIPURIHostName.empty() ) - { - LL_INFOS("Voice") << "Overriding account server based on VivoxDebugSIPURIHostName: " - << debugSIPURIHostName << LL_ENDL; - mVoiceSIPURIHostName = debugSIPURIHostName; - } - - if( mVoiceSIPURIHostName.empty() ) - { - // we have an empty account server name - // so we fall back to hardcoded defaults - - if(LLGridManager::getInstance()->isInProductionGrid()) - { - // Use the release account server - mVoiceSIPURIHostName = "bhr.vivox.com"; - } - else - { - // Use the development account server - mVoiceSIPURIHostName = "bhd.vivox.com"; - } - LL_INFOS("Voice") << "Defaulting SIP URI host: " - << mVoiceSIPURIHostName << LL_ENDL; - - } - - std::string debugAccountServerURI = gSavedSettings.getString("VivoxDebugVoiceAccountServerURI"); - - if( !debugAccountServerURI.empty() ) - { - LL_INFOS("Voice") << "Overriding account server based on VivoxDebugVoiceAccountServerURI: " - << debugAccountServerURI << LL_ENDL; - mVoiceAccountServerURI = debugAccountServerURI; - } - - if( mVoiceAccountServerURI.empty() ) - { - // If the account server URI isn't specified, construct it from the SIP URI hostname - mVoiceAccountServerURI = "https://www." + mVoiceSIPURIHostName + "/api2/"; - LL_INFOS("Voice") << "Inferring account server based on SIP URI Host name: " - << mVoiceAccountServerURI << LL_ENDL; - } -} - -void LLVivoxVoiceClient::idle(void* user_data) -{ -} - -//========================================================================= -// the following are methods to support the coroutine implementation of the -// voice connection and processing. They should only be called in the context -// of a coroutine. -// -// - -typedef enum e_voice_control_coro_state -{ - VOICE_STATE_ERROR = -1, - VOICE_STATE_DONE = 0, - VOICE_STATE_TP_WAIT, // entry point - VOICE_STATE_START_DAEMON, - VOICE_STATE_PROVISION_ACCOUNT, - VOICE_STATE_START_SESSION, - VOICE_STATE_SESSION_RETRY, - VOICE_STATE_SESSION_ESTABLISHED, - VOICE_STATE_WAIT_FOR_CHANNEL, - VOICE_STATE_DISCONNECT, - VOICE_STATE_WAIT_FOR_EXIT, -} EVoiceControlCoroState; - -void LLVivoxVoiceClient::voiceControlCoro() -{ - int state = 0; - try - { - // state is passed as a reference instead of being - // a member due to unresolved issues with coroutine - // surviving longer than LLVivoxVoiceClient - voiceControlStateMachine(state); - } - catch (const LLCoros::Stop&) - { - LL_DEBUGS("LLVivoxVoiceClient") << "Received a shutdown exception" << LL_ENDL; - } - catch (const LLContinueError&) - { - LOG_UNHANDLED_EXCEPTION("LLVivoxVoiceClient"); - } - catch (...) - { - // Ideally for Windows need to log SEH exception instead or to set SEH - // handlers but bugsplat shows local variables for windows, which should - // be enough - LL_WARNS("Voice") << "voiceControlStateMachine crashed in state " << state << LL_ENDL; - throw; - } -} - -void LLVivoxVoiceClient::voiceControlStateMachine(S32 &coro_state) -{ - if (sShuttingDown) - { - return; - } - - LL_DEBUGS("Voice") << "starting" << LL_ENDL; - mIsCoroutineActive = true; - LLCoros::set_consuming(true); - - U32 retry = 0; - - coro_state = VOICE_STATE_TP_WAIT; - - do - { - if (sShuttingDown) - { - // Vivox singleton performed the exit, logged out, - // cleaned sockets, gateway and no longer cares - // about state of coroutine, so just stop - return; - } - - switch (coro_state) - { - case VOICE_STATE_TP_WAIT: - // starting point for voice - if (gAgent.getTeleportState() != LLAgent::TELEPORT_NONE) - { - LL_DEBUGS("Voice") << "Suspending voiceControlCoro() momentarily for teleport. Tuning: " << mTuningMode << ". Relog: " << mRelogRequested << LL_ENDL; - llcoro::suspendUntilTimeout(1.0); - } - else - { - coro_state = VOICE_STATE_START_DAEMON; - } - break; - - case VOICE_STATE_START_DAEMON: - LL_DEBUGS("Voice") << "Launching daemon" << LL_ENDL; - LLVoiceVivoxStats::getInstance()->reset(); - if (startAndLaunchDaemon()) - { - coro_state = VOICE_STATE_PROVISION_ACCOUNT; - } - else - { - coro_state = VOICE_STATE_SESSION_RETRY; - } - break; - - case VOICE_STATE_PROVISION_ACCOUNT: - if (provisionVoiceAccount()) - { - coro_state = VOICE_STATE_START_SESSION; - } - else - { - coro_state = VOICE_STATE_SESSION_RETRY; - } - break; - - case VOICE_STATE_START_SESSION: - if (establishVoiceConnection()) - { - coro_state = VOICE_STATE_SESSION_ESTABLISHED; - } - else - { - coro_state = VOICE_STATE_SESSION_RETRY; - } - break; - - case VOICE_STATE_SESSION_RETRY: - giveUp(); // cleans sockets and session - if (mRelogRequested) - { - // We failed to connect, give it a bit time before retrying. - retry++; - F32 full_delay = llmin(5.f * (F32)retry, 60.f); - F32 current_delay = 0.f; - LL_INFOS("Voice") << "Voice failed to establish session after " << retry - << " tries. Will attempt to reconnect in " << full_delay - << " seconds" << LL_ENDL; - while (current_delay < full_delay && !sShuttingDown) - { - // Assuming that a second has passed is not accurate, - // but we don't need accurancy here, just to make sure - // that some time passed and not to outlive voice itself - current_delay++; - llcoro::suspendUntilTimeout(1.f); - } - coro_state = VOICE_STATE_WAIT_FOR_EXIT; - } - else - { - coro_state = VOICE_STATE_DONE; - } - break; - - case VOICE_STATE_SESSION_ESTABLISHED: - { - // enable/disable the automatic VAD and explicitly set the initial values of - // the VAD variables ourselves when it is off - see SL-15072 for more details - // note: we set the other parameters too even if the auto VAD is on which is ok - unsigned int vad_auto = gSavedSettings.getU32("VivoxVadAuto"); - unsigned int vad_hangover = gSavedSettings.getU32("VivoxVadHangover"); - unsigned int vad_noise_floor = gSavedSettings.getU32("VivoxVadNoiseFloor"); - unsigned int vad_sensitivity = gSavedSettings.getU32("VivoxVadSensitivity"); - setupVADParams(vad_auto, vad_hangover, vad_noise_floor, vad_sensitivity); - - // watch for changes to the VAD settings via Debug Settings UI and act on them accordingly - gSavedSettings.getControl("VivoxVadAuto")->getSignal()->connect(boost::bind(&LLVivoxVoiceClient::onVADSettingsChange, this)); - gSavedSettings.getControl("VivoxVadHangover")->getSignal()->connect(boost::bind(&LLVivoxVoiceClient::onVADSettingsChange, this)); - gSavedSettings.getControl("VivoxVadNoiseFloor")->getSignal()->connect(boost::bind(&LLVivoxVoiceClient::onVADSettingsChange, this)); - gSavedSettings.getControl("VivoxVadSensitivity")->getSignal()->connect(boost::bind(&LLVivoxVoiceClient::onVADSettingsChange, this)); - - if (mTuningMode) - { - performMicTuning(); - } - - coro_state = VOICE_STATE_WAIT_FOR_CHANNEL; - } - break; - - case VOICE_STATE_WAIT_FOR_CHANNEL: - waitForChannel(); // todo: split into more states like login/fonts - coro_state = VOICE_STATE_DISCONNECT; - break; - - case VOICE_STATE_DISCONNECT: - LL_DEBUGS("Voice") << "lost channel RelogRequested=" << mRelogRequested << LL_ENDL; - endAndDisconnectSession(); - retry = 0; // Connected without issues - coro_state = VOICE_STATE_WAIT_FOR_EXIT; - break; - - case VOICE_STATE_WAIT_FOR_EXIT: - if (isGatewayRunning()) - { - LL_INFOS("Voice") << "waiting for SLVoice to exit" << LL_ENDL; - llcoro::suspendUntilTimeout(1.0); - } - else if (mRelogRequested && mVoiceEnabled) - { - LL_INFOS("Voice") << "will attempt to reconnect to voice" << LL_ENDL; - coro_state = VOICE_STATE_TP_WAIT; - } - else - { - coro_state = VOICE_STATE_DONE; - } - break; - - case VOICE_STATE_DONE: - break; - } - } while (coro_state > 0); - - if (sShuttingDown) - { - // LLVivoxVoiceClient might be already dead - return; - } - - mIsCoroutineActive = false; - LL_INFOS("Voice") << "exiting" << LL_ENDL; -} - -bool LLVivoxVoiceClient::endAndDisconnectSession() -{ - LL_DEBUGS("Voice") << LL_ENDL; - - breakVoiceConnection(true); - - killGateway(); - - return true; -} - -bool LLVivoxVoiceClient::callbackEndDaemon(const LLSD& data) -{ - if (!sShuttingDown && mVoiceEnabled) - { - LL_WARNS("Voice") << "SLVoice terminated " << ll_stream_notation_sd(data) << LL_ENDL; - terminateAudioSession(false); - closeSocket(); - cleanUp(); - LLVoiceClient::getInstance()->setUserPTTState(false); - gAgent.setVoiceConnected(false); - mRelogRequested = true; - } - sGatewayPump.stopListening("VivoxDaemonPump"); - return false; -} - -bool LLVivoxVoiceClient::startAndLaunchDaemon() -{ - //--------------------------------------------------------------------- - if (!LLVoiceClient::getInstance()->voiceEnabled()) - { - // Voice is locked out, we must not launch the vivox daemon. - LL_WARNS("Voice") << "voice disabled; not starting daemon" << LL_ENDL; - return false; - } - - if (!isGatewayRunning()) - { -#ifndef VIVOXDAEMON_REMOTEHOST - // Launch the voice daemon -#if LL_WINDOWS - // On windows use exe (not work or RO) directory - std::string exe_path = gDirUtilp->getExecutableDir(); - gDirUtilp->append(exe_path, "SLVoice.exe"); -#elif LL_DARWIN - // On MAC use resource directory - std::string exe_path = gDirUtilp->getAppRODataDir(); - gDirUtilp->append(exe_path, "SLVoice"); -#else - std::string exe_path = gDirUtilp->getExecutableDir(); - gDirUtilp->append(exe_path, "SLVoice"); -#endif - // See if the vivox executable exists - llstat s; - if (!LLFile::stat(exe_path, &s)) - { - // vivox executable exists. Build the command line and launch the daemon. - LLProcess::Params params; - params.executable = exe_path; - - // VOICE-88: Cycle through [portbase..portbase+portrange) on - // successive tries because attempting to relaunch (after manually - // disabling and then re-enabling voice) with the same port can - // cause SLVoice's bind() call to fail with EADDRINUSE. We expect - // that eventually the OS will time out previous ports, which is - // why we cycle instead of incrementing indefinitely. - - static LLCachedControl portbase(gSavedSettings, "VivoxVoicePort"); - static LLCachedControl host(gSavedSettings, "VivoxVoiceHost"); - static LLCachedControl loglevel(gSavedSettings, "VivoxDebugLevel"); - static LLCachedControl log_folder(gSavedSettings, "VivoxLogDirectory"); - static LLCachedControl shutdown_timeout(gSavedSettings, "VivoxShutdownTimeout"); - static const U32 portrange = 100; - static U32 portoffset = 0; - U32 port = 0; - - if (LLAppViewer::instance()->isSecondInstance()) - { - // Ideally need to know amount of instances and - // to increment instance_offset on EADDRINUSE. - // But for now just use rand - static U32 instance_offset = portrange * ll_rand(20); - port = portbase + portoffset + instance_offset; - } - else - { - // leave main thread with exclusive port set - port = portbase + portoffset; - } - portoffset = (portoffset + 1) % portrange; - params.args.add("-i"); - params.args.add(STRINGIZE(host() << ':' << port)); - - params.args.add("-ll"); - if (loglevel().empty()) - { - params.args.add("0"); - } - else - { - params.args.add(loglevel); - } - - params.args.add("-lf"); - if (log_folder().empty()) - { - params.args.add(gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "")); - } - else - { - params.args.add(log_folder); - } - - // set log file basename and .log - params.args.add("-lp"); - params.args.add("SLVoice"); - params.args.add("-ls"); - params.args.add(".log"); - - // rotate any existing log - std::string new_log = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "SLVoice.log"); - std::string old_log = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "SLVoice.old"); - if (gDirUtilp->fileExists(new_log)) - { - LLFile::rename(new_log, old_log); - } - - if (!shutdown_timeout().empty()) - { - params.args.add("-st"); - params.args.add(shutdown_timeout); - } - params.cwd = gDirUtilp->getAppRODataDir(); - -# ifdef VIVOX_HANDLE_ARGS - params.args.add("-ah"); - params.args.add(LLVivoxSecurity::getInstance()->accountHandle()); - - params.args.add("-ch"); - params.args.add(LLVivoxSecurity::getInstance()->connectorHandle()); -# endif // VIVOX_HANDLE_ARGS - - params.postend = sGatewayPump.getName(); - sGatewayPump.listen("VivoxDaemonPump", boost::bind(&LLVivoxVoiceClient::callbackEndDaemon, this, _1)); - - LL_INFOS("Voice") << "Launching SLVoice" << LL_ENDL; - LL_DEBUGS("Voice") << "SLVoice params " << params << LL_ENDL; - - sGatewayPtr = LLProcess::create(params); - - mDaemonHost = LLHost(host().c_str(), port); - } - else - { - LL_WARNS("Voice") << exe_path << " not found." << LL_ENDL; - return false; - } -#else - // SLIM SDK: port changed from 44124 to 44125. - // We can connect to a client gateway running on another host. This is useful for testing. - // To do this, launch the gateway on a nearby host like this: - // vivox-gw.exe -p tcp -i 0.0.0.0:44125 - // and put that host's IP address here. - mDaemonHost = LLHost(gSavedSettings.getString("VivoxVoiceHost"), gSavedSettings.getU32("VivoxVoicePort")); -#endif - - // Dirty the states we'll need to sync with the daemon when it comes up. - mMuteMicDirty = true; - mMicVolumeDirty = true; - mSpeakerVolumeDirty = true; - mSpeakerMuteDirty = true; - // These only need to be set if they're not default (i.e. empty string). - mCaptureDeviceDirty = !mCaptureDevice.empty(); - mRenderDeviceDirty = !mRenderDevice.empty(); - - mMainSessionGroupHandle.clear(); - } - else - { - LL_DEBUGS("Voice") << " gateway running; not attempting to start" << LL_ENDL; - } - - //--------------------------------------------------------------------- - llcoro::suspendUntilTimeout(UPDATE_THROTTLE_SECONDS); - - LL_DEBUGS("Voice") << "Connecting to vivox daemon:" << mDaemonHost << LL_ENDL; - - int retryCount(0); - LLVoiceVivoxStats::getInstance()->reset(); - while (!sConnected && !sShuttingDown && retryCount++ <= DAEMON_CONNECT_RETRY_MAX) - { - LLVoiceVivoxStats::getInstance()->connectionAttemptStart(); - LL_DEBUGS("Voice") << "Attempting to connect to vivox daemon: " << mDaemonHost << LL_ENDL; - closeSocket(); - if (!mSocket) - { - mSocket = LLSocket::create(gAPRPoolp, LLSocket::STREAM_TCP); - } - - sConnected = mSocket->blockingConnect(mDaemonHost); - LLVoiceVivoxStats::getInstance()->connectionAttemptEnd(sConnected); - if (!sConnected) - { - llcoro::suspendUntilTimeout(DAEMON_CONNECT_THROTTLE_SECONDS); - } - } - - //--------------------------------------------------------------------- - if (sShuttingDown && !sConnected) - { - return false; - } - - llcoro::suspendUntilTimeout(UPDATE_THROTTLE_SECONDS); - - while (!sPump && !sShuttingDown) - { // Can't use the pump until we have it available. - llcoro::suspendUntilNextFrame(); - } - - if (sShuttingDown) - { - return false; - } - - // MBW -- Note to self: pumps and pipes examples in - // indra/test/io.cpp - // indra/test/llpipeutil.{cpp|h} - - // Attach the pumps and pipes - - LLPumpIO::chain_t readChain; - - readChain.push_back(LLIOPipe::ptr_t(new LLIOSocketReader(mSocket))); - readChain.push_back(LLIOPipe::ptr_t(new LLVivoxProtocolParser())); - - - sPump->addChain(readChain, NEVER_CHAIN_EXPIRY_SECS); - - - //--------------------------------------------------------------------- - llcoro::suspendUntilTimeout(UPDATE_THROTTLE_SECONDS); - - // Initial devices query - getCaptureDevicesSendMessage(); - getRenderDevicesSendMessage(); - - mLoginRetryCount = 0; - - return true; -} - -bool LLVivoxVoiceClient::provisionVoiceAccount() -{ - LL_INFOS("Voice") << "Provisioning voice account." << LL_ENDL; - - while ((!gAgent.getRegion() || !gAgent.getRegion()->capabilitiesReceived()) && !sShuttingDown) - { - LL_DEBUGS("Voice") << "no capabilities for voice provisioning; waiting " << LL_ENDL; - // *TODO* Pump a message for wake up. - llcoro::suspend(); - } - - if (sShuttingDown) - { - return false; - } - - std::string url = gAgent.getRegionCapability("ProvisionVoiceAccountRequest"); - - LL_DEBUGS("Voice") << "region ready for voice provisioning; url=" << url << LL_ENDL; - - LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); - LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t - httpAdapter = std::make_shared("voiceAccountProvision", httpPolicy); - LLCore::HttpRequest::ptr_t httpRequest = std::make_shared(); - LLCore::HttpOptions::ptr_t httpOpts = std::make_shared(); - int retryCount(0); - - LLSD result; - bool provisioned = false; - do - { - LLVoiceVivoxStats::getInstance()->provisionAttemptStart(); - LLSD body; - body["voice_server_type"] = "vivox"; - result = httpAdapter->postAndSuspend(httpRequest, url, body, httpOpts); - - if (sShuttingDown) - { - return false; - } - - LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; - LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - - if (status == LLCore::HttpStatus(404)) - { - F32 timeout = pow(PROVISION_RETRY_TIMEOUT, static_cast(retryCount)); - LL_WARNS("Voice") << "Provision CAP 404. Retrying in " << timeout << " seconds. Retries: " << (S32)retryCount << LL_ENDL; - llcoro::suspendUntilTimeout(timeout); - - if (sShuttingDown) - { - return false; - } - } - else if (!status) - { - LL_WARNS("Voice") << "Unable to provision voice account." << LL_ENDL; - LLVoiceVivoxStats::getInstance()->provisionAttemptEnd(false); - return false; - } - else - { - provisioned = true; - } - } while (!provisioned && ++retryCount <= PROVISION_RETRY_MAX && !sShuttingDown); - - if (sShuttingDown && !provisioned) - { - return false; - } - - LLVoiceVivoxStats::getInstance()->provisionAttemptEnd(provisioned); - if (!provisioned) - { - LL_WARNS("Voice") << "Could not access voice provision cap after " << retryCount << " attempts." << LL_ENDL; - return false; - } - LL_DEBUGS("Voice") << "Voice Provision Result." << result << LL_ENDL; - std::string voiceSipUriHostname; - std::string voiceAccountServerUri; - std::string voiceUserName = result["username"].asString(); - std::string voicePassword = result["password"].asString(); - - if (result.has("voice_sip_uri_hostname")) - { - voiceSipUriHostname = result["voice_sip_uri_hostname"].asString(); - } - - // this key is actually misnamed -- it will be an entire URI, not just a hostname. - if (result.has("voice_account_server_name")) - { - voiceAccountServerUri = result["voice_account_server_name"].asString(); - } - - LL_DEBUGS("Voice") << "ProvisionVoiceAccountRequest response" - << " user " << (voiceUserName.empty() ? "not set" : "set") - << " password " << (voicePassword.empty() ? "not set" : "set") - << " sip uri " << voiceSipUriHostname - << " account uri " << voiceAccountServerUri - << LL_ENDL; - - setLoginInfo(voiceUserName, voicePassword, voiceSipUriHostname, voiceAccountServerUri); - - return true; -} - -bool LLVivoxVoiceClient::establishVoiceConnection() -{ - if (!mVoiceEnabled && mIsInitialized) - { - LL_WARNS("Voice") << "cannot establish connection; enabled "<establishAttemptStart(); - connectorCreate(); - do - { - result = llcoro::suspendUntilEventOn(mVivoxPump); - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - - if (result.has("connector")) - { - connected = LLSD::Boolean(result["connector"]); - LLVoiceVivoxStats::getInstance()->establishAttemptEnd(connected); - if (!connected) - { - if (result.has("retry") && ++retries <= CONNECT_RETRY_MAX && !sShuttingDown) - { - F32 timeout = (F32)LLSD::Real(result["retry"]); - timeout *= retries; - LL_INFOS("Voice") << "Retry connection to voice service in " << timeout << " seconds" << LL_ENDL; - llcoro::suspendUntilTimeout(timeout); - - if (mVoiceEnabled) // user may have switched it off - { - // try again - LLVoiceVivoxStats::getInstance()->establishAttemptStart(); - connectorCreate(); - } - else - { - // stop if they've turned off voice - giving_up = true; - } - } - else - { - giving_up=true; - } - } - } - LL_DEBUGS("Voice") << (connected ? "" : "not ") << "connected, " - << (giving_up ? "" : "not ") << "giving up" - << LL_ENDL; - } while (!connected && !giving_up && !sShuttingDown); - - if (giving_up) - { - LLSD args; - args["HOSTID"] = LLURI(mVoiceAccountServerURI).authority(); - LLNotificationsUtil::add("NoVoiceConnect", args); - } - - return connected; -} - -bool LLVivoxVoiceClient::breakVoiceConnection(bool corowait) -{ - LL_DEBUGS("Voice") << "( wait=" << corowait << ")" << LL_ENDL; - bool retval(true); - - mShutdownComplete = false; - connectorShutdown(); - - if (corowait) - { - LLSD timeoutResult(LLSDMap("connector", "timeout")); - - LLSD result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, LOGOUT_ATTEMPT_TIMEOUT, timeoutResult); - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - - retval = result.has("connector"); - } - else - { - mRelogRequested = false; //stop the control coro - // If we are not doing a corowait then we must sleep until the connector has responded - // otherwise we may very well close the socket too early. -#if LL_WINDOWS - if (!mShutdownComplete) - { - // The situation that brings us here is a call from ::terminate() - // At this point message system is already down so we can't wait for - // the message, yet we need to receive "connector shutdown response". - // Either wait a bit and emulate it or check gMessageSystem for specific message - _sleep(1000); - if (sConnected) - { - sConnected = false; - LLSD vivoxevent(LLSDMap("connector", LLSD::Boolean(false))); - mVivoxPump.post(vivoxevent); - } - mShutdownComplete = true; - } -#endif - } - - LL_DEBUGS("Voice") << "closing SLVoice socket" << LL_ENDL; - closeSocket(); // Need to do this now -- bad things happen if the destructor does it later. - cleanUp(); - sConnected = false; - - return retval; -} - -bool LLVivoxVoiceClient::loginToVivox() -{ - LLSD timeoutResult(LLSDMap("login", "timeout")); - - int loginRetryCount(0); - - bool response_ok(false); - bool account_login(false); - bool send_login(true); - - do - { - mIsLoggingIn = true; - if (send_login) - { - loginSendMessage(); - send_login = false; - } - - LLSD result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, LOGIN_ATTEMPT_TIMEOUT, timeoutResult); - - if (sShuttingDown) - { - return false; - } - - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - - if (result.has("login")) - { - std::string loginresp = result["login"]; - - if (((loginresp == "retry") || (loginresp == "timeout")) && !sShuttingDown) - { - LL_WARNS("Voice") << "login failed with status '" << loginresp << "' " - << " count " << loginRetryCount << "/" << LOGIN_RETRY_MAX - << LL_ENDL; - if (++loginRetryCount > LOGIN_RETRY_MAX) - { - // We've run out of retries - tell the user - LL_WARNS("Voice") << "too many login retries (" << loginRetryCount << "); giving up." << LL_ENDL; - LLSD args; - args["HOSTID"] = LLURI(mVoiceAccountServerURI).authority(); - mTerminateDaemon = true; - LLNotificationsUtil::add("NoVoiceConnect", args); - - mIsLoggingIn = false; - return false; - } - response_ok = false; - account_login = false; - send_login = true; - - // an exponential backoff gets too long too quickly; stretch it out, but not too much - F32 timeout = loginRetryCount * LOGIN_ATTEMPT_TIMEOUT; - - // tell the user there is a problem - LL_WARNS("Voice") << "login " << loginresp << " will retry login in " << timeout << " seconds." << LL_ENDL; - - if (!sShuttingDown) - { - // Todo: this is way to long, viewer can get stuck waiting during shutdown - // either make it listen to pump or split in smaller waits with checks for shutdown - llcoro::suspendUntilTimeout(timeout); - } - } - else if (loginresp == "failed") - { - mIsLoggingIn = false; - return false; - } - else if (loginresp == "response_ok") - { - response_ok = true; - } - else if (loginresp == "account_login") - { - account_login = true; - } - else if (sShuttingDown) - { - mIsLoggingIn = false; - return false; - } - } - - } while ((!response_ok || !account_login) && !sShuttingDown); - - if (sShuttingDown) - { - return false; - } - - mRelogRequested = false; - mIsLoggedIn = true; - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGGED_IN); - - // Set up the mute list observer if it hasn't been set up already. - if ((!sMuteListListener_listening)) - { - LLMuteList::getInstance()->addObserver(&mutelist_listener); - sMuteListListener_listening = true; - } - - // Set the initial state of mic mute, local speaker volume, etc. - sendLocalAudioUpdates(); - mIsLoggingIn = false; - - return true; -} - -void LLVivoxVoiceClient::logoutOfVivox(bool wait) -{ - if (mIsLoggedIn) - { - // Ensure that we'll re-request provisioning before logging in again - mAccountPassword.clear(); - mVoiceAccountServerURI.clear(); - - logoutSendMessage(); - - if (wait) - { - LLSD timeoutResult(LLSDMap("logout", "timeout")); - LLSD result; - - do - { - LL_DEBUGS("Voice") - << "waiting for logout response on " - << mVivoxPump.getName() - << LL_ENDL; - - result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, LOGOUT_ATTEMPT_TIMEOUT, timeoutResult); - - if (sShuttingDown) - { - break; - } - - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - // Don't get confused by prior queued events -- note that it's - // very important that mVivoxPump is an LLEventMailDrop, which - // does queue events. - } while (! result["logout"]); - } - else - { - LL_DEBUGS("Voice") << "not waiting for logout" << LL_ENDL; - } - - mIsLoggedIn = false; - } -} - - -bool LLVivoxVoiceClient::retrieveVoiceFonts() -{ - // Request the set of available voice fonts. - refreshVoiceEffectLists(true); - - mIsWaitingForFonts = true; - LLSD result; - do - { - result = llcoro::suspendUntilEventOn(mVivoxPump); - - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - if (result.has("voice_fonts")) - break; - } while (true); - mIsWaitingForFonts = false; - - mVoiceFontExpiryTimer.start(); - mVoiceFontExpiryTimer.setTimerExpirySec(VOICE_FONT_EXPIRY_INTERVAL); - - return result["voice_fonts"].asBoolean(); -} - -bool LLVivoxVoiceClient::requestParcelVoiceInfo() -{ - //_INFOS("Voice") << "Requesting voice info for Parcel" << LL_ENDL; - - LLViewerRegion * region = gAgent.getRegion(); - if (region == NULL || !region->capabilitiesReceived()) - { - LL_DEBUGS("Voice") << "ParcelVoiceInfoRequest capability not yet available, deferring" << LL_ENDL; - return false; - } - - // grab the cap. - std::string url = gAgent.getRegion()->getCapability("ParcelVoiceInfoRequest"); - if (url.empty()) - { - // Region dosn't have the cap. Stop probing. - LL_DEBUGS("Voice") << "ParcelVoiceInfoRequest capability not available in this region" << LL_ENDL; - return false; - } - - // update the parcel - checkParcelChanged(true); - - LL_DEBUGS("Voice") << "sending ParcelVoiceInfoRequest (" << mCurrentRegionName << ", " << mCurrentParcelLocalID << ")" << LL_ENDL; - - LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); - LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t - httpAdapter = std::make_shared("parcelVoiceInfoRequest", httpPolicy); - LLCore::HttpRequest::ptr_t httpRequest = std::make_shared(); - - LLSD result = httpAdapter->postAndSuspend(httpRequest, url, LLSD()); - - if (sShuttingDown) - { - return false; - } - - LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; - LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - - if (mSessionTerminateRequested || (!mVoiceEnabled && mIsInitialized)) - { - // if a terminate request has been received, - // bail and go to the stateSessionTerminated - // state. If the cap request is still pending, - // the responder will check to see if we've moved - // to a new session and won't change any state. - LL_DEBUGS("Voice") << "terminate requested " << mSessionTerminateRequested - << " enabled " << mVoiceEnabled - << " initialized " << mIsInitialized - << LL_ENDL; - terminateAudioSession(true); - return false; - } - - if ((!status) || (mSessionTerminateRequested || (!mVoiceEnabled && mIsInitialized))) - { - if (mSessionTerminateRequested || (!mVoiceEnabled && mIsInitialized)) - { - LL_WARNS("Voice") << "Session terminated." << LL_ENDL; - } - - LL_WARNS("Voice") << "No voice on parcel" << LL_ENDL; - sessionTerminate(); - return false; - } - - if (!result.has("voice_credentials")) - { - if (LLViewerParcelMgr::getInstance()->allowAgentVoice()) - { - LL_WARNS("Voice") << "No voice credentials" << LL_ENDL; - } - else - { - LL_DEBUGS("Voice") << "No voice credentials" << LL_ENDL; - } - } - - // set the spatial channel. If no voice credentials or uri are - // available, then we simply drop out of voice spatially. - return setSpatialChannel(result["voice_credentials"]); -} - -bool LLVivoxVoiceClient::addAndJoinSession(const sessionStatePtr_t &nextSession) -{ - mIsJoiningSession = true; - - sessionStatePtr_t oldSession = mAudioSession; - - LL_INFOS("Voice") << "Adding or joining voice session " << nextSession->mHandle << LL_ENDL; - - mAudioSession = nextSession; - if (!mAudioSession || !mAudioSession->mReconnect) - { - mNextAudioSession.reset(); - } - - // The old session may now need to be deleted. - reapSession(oldSession); - - if (mAudioSession) - { - if (!mAudioSession->mHandle.empty()) - { - // Connect to a session by session handle - - sessionMediaConnectSendMessage(mAudioSession); - } - else - { - // Connect to a session by URI - sessionCreateSendMessage(mAudioSession, true, false); - } - } - - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINING); - - llcoro::suspend(); - - if (sShuttingDown) - { - return false; - } - - LLSD result; - - if (mSpatialJoiningNum == MAX_NORMAL_JOINING_SPATIAL_NUM) - { - // Notify observers to let them know there is problem with voice - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED); - LL_WARNS() << "There seems to be problem with connection to voice server. Disabling voice chat abilities." << LL_ENDL; - } - - // Increase mSpatialJoiningNum only for spatial sessions- it's normal to reach this case for - // example for p2p many times while waiting for response, so it can't be used to detect errors - if (mAudioSession && mAudioSession->mIsSpatial) - { - mSpatialJoiningNum++; - } - - if (!mVoiceEnabled && mIsInitialized) - { - LL_DEBUGS("Voice") << "Voice no longer enabled. Exiting" - << " enabled " << mVoiceEnabled - << " initialized " << mIsInitialized - << LL_ENDL; - mIsJoiningSession = false; - // User bailed out during connect -- jump straight to teardown. - terminateAudioSession(true); - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED); - return false; - } - else if (mSessionTerminateRequested) - { - LL_DEBUGS("Voice") << "Terminate requested" << LL_ENDL; - if (mAudioSession && !mAudioSession->mHandle.empty()) - { - // Only allow direct exits from this state in p2p calls (for cancelling an invite). - // Terminating a half-connected session on other types of calls seems to break something in the vivox gateway. - if (mAudioSession->mIsP2P) - { - terminateAudioSession(true); - mIsJoiningSession = false; - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); - return false; - } - } - } - - bool added(true); - bool joined(false); - - LLSD timeoutResult(LLSDMap("session", "timeout")); - - // We are about to start a whole new session. Anything that MIGHT still be in our - // maildrop is going to be stale and cause us much wailing and gnashing of teeth. - // Just flush it all out and start new. - mVivoxPump.discard(); - - // It appears that I need to wait for BOTH the SessionGroup.AddSession response and the SessionStateChangeEvent with state 4 - // before continuing from this state. They can happen in either order, and if I don't wait for both, things can get stuck. - // For now, the SessionGroup.AddSession response handler sets mSessionHandle and the SessionStateChangeEvent handler transitions to stateSessionJoined. - // This is a cheap way to make sure both have happened before proceeding. - do - { - result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, SESSION_JOIN_TIMEOUT, timeoutResult); - - if (sShuttingDown) - { - return false; - } - - LL_INFOS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - if (result.has("session")) - { - if (!mAudioSession) - { - LL_WARNS("Voice") << "Message for session handle \"" << result["handle"] << "\" while session is not initialized." << LL_ENDL; - continue; - } - if (result.has("handle") && result["handle"] != mAudioSession->mHandle) - { - LL_WARNS("Voice") << "Message for session handle \"" << result["handle"] << "\" while waiting for \"" << mAudioSession->mHandle << "\"." << LL_ENDL; - continue; - } - - std::string message = result["session"].asString(); - - if ((message == "added") || (message == "created")) - { - added = true; - } - else if (message == "joined") - { - joined = true; - } - else if ((message == "failed") || (message == "removed") || (message == "timeout")) - { // we will get a removed message if a voice call is declined. - LL_INFOS("Voice") << "Result:" << result << LL_ENDL; - if (message == "failed") - { - int reason = result["reason"].asInteger(); - LL_WARNS("Voice") << "Add and join failed for reason " << reason << LL_ENDL; - - if ( (reason == ERROR_VIVOX_NOT_LOGGED_IN) - || (reason == ERROR_VIVOX_OBJECT_NOT_FOUND)) - { - LL_DEBUGS("Voice") << "Requesting reprovision and login." << LL_ENDL; - requestRelog(); - } - } - else - { - LL_WARNS("Voice") << "session '" << message << "' " - << LL_ENDL; - } - - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); - mIsJoiningSession = false; - return false; - } - } - } while (!added || !joined); - - mIsJoiningSession = false; - - if (mSpatialJoiningNum > 100) - { - LL_WARNS("Voice") << "There seems to be problem with connecting to a voice channel. Frames to join were " << mSpatialJoiningNum << LL_ENDL; - } - - mSpatialJoiningNum = 0; - - // Events that need to happen when a session is joined could go here. - // send an initial positional information immediately upon joining. - // - // do an initial update for position and the camera position, then send a - // positional update. - updatePosition(); - enforceTether(); - - // Dirty state that may need to be sync'ed with the daemon. - mMuteMicDirty = true; - mSpeakerVolumeDirty = true; - mSpatialCoordsDirty = true; - - sendPositionAndVolumeUpdate(); - - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINED); - - return true; -} - -bool LLVivoxVoiceClient::terminateAudioSession(bool wait) -{ - - if (mAudioSession) - { - LL_INFOS("Voice") << "terminateAudioSession(" << wait << ") Terminating current voice session " << mAudioSession->mHandle << LL_ENDL; - - if (mIsLoggedIn) - { - if (!mAudioSession->mHandle.empty()) - { - -#if RECORD_EVERYTHING - // HACK: for testing only - // Save looped recording - std::string savepath("/tmp/vivoxrecording"); - { - time_t now = time(NULL); - const size_t BUF_SIZE = 64; - char time_str[BUF_SIZE]; /* Flawfinder: ignore */ - - strftime(time_str, BUF_SIZE, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now)); - savepath += time_str; - } - recordingLoopSave(savepath); -#endif - - sessionMediaDisconnectSendMessage(mAudioSession); - - if (wait) - { - LLSD result; - do - { - LLSD timeoutResult(LLSDMap("session", "timeout")); - - result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, LOGOUT_ATTEMPT_TIMEOUT, timeoutResult); - - if (sShuttingDown) - { - return false; - } - - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - if (result.has("session")) - { - if (result.has("handle")) - { - if (result["handle"] != mAudioSession->mHandle) - { - LL_WARNS("Voice") << "Message for session handle \"" << result["handle"] << "\" while waiting for \"" << mAudioSession->mHandle << "\"." << LL_ENDL; - continue; - } - } - - std::string message = result["session"].asString(); - if (message == "removed" || message == "timeout") - break; - } - } while (true); - - } - } - else - { - LL_WARNS("Voice") << "called with no session handle" << LL_ENDL; - } - } - else - { - LL_WARNS("Voice") << "Session " << mAudioSession->mHandle << " already terminated by logout." << LL_ENDL; - } - - sessionStatePtr_t oldSession = mAudioSession; - - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); // needs mAudioSession for uri - mAudioSession.reset(); - - // The old session may now need to be deleted. - reapSession(oldSession); - } - else - { - LL_WARNS("Voice") << "terminateAudioSession(" << wait << ") with NULL mAudioSession" << LL_ENDL; - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); - } - - - // Always reset the terminate request flag when we get here. - // Some slower PCs have a race condition where they can switch to an incoming P2P call faster than the state machine leaves - // the region chat. - mSessionTerminateRequested = false; - - bool status=((mVoiceEnabled || !mIsInitialized) && !mRelogRequested && !sShuttingDown); - LL_DEBUGS("Voice") << "exiting" - << " VoiceEnabled " << mVoiceEnabled - << " IsInitialized " << mIsInitialized - << " RelogRequested " << mRelogRequested - << " ShuttingDown " << (sShuttingDown ? "True" : "False") - << " returning " << status - << LL_ENDL; - return status; -} - - -typedef enum e_voice_wait_for_channel_state -{ - VOICE_CHANNEL_STATE_LOGIN = 0, // entry point - VOICE_CHANNEL_STATE_CHECK_EFFECTS, - VOICE_CHANNEL_STATE_START_CHANNEL_PROCESSING, - VOICE_CHANNEL_STATE_PROCESS_CHANNEL, - VOICE_CHANNEL_STATE_NEXT_CHANNEL_DELAY, - VOICE_CHANNEL_STATE_NEXT_CHANNEL_CHECK, - VOICE_CHANNEL_STATE_LOGOUT, - VOICE_CHANNEL_STATE_RELOG, - VOICE_CHANNEL_STATE_DONE, -} EVoiceWaitForChannelState; - -bool LLVivoxVoiceClient::waitForChannel() -{ - LL_INFOS("Voice") << "Waiting for channel" << LL_ENDL; - - EVoiceWaitForChannelState state = VOICE_CHANNEL_STATE_LOGIN; - - do - { - if (sShuttingDown) - { - // terminate() forcefully disconects voice, no need for cleanup - return false; - } - - switch (state) - { - case VOICE_CHANNEL_STATE_LOGIN: - if (!loginToVivox()) - { - return false; - } - state = VOICE_CHANNEL_STATE_CHECK_EFFECTS; - break; - - case VOICE_CHANNEL_STATE_CHECK_EFFECTS: - if (LLVoiceClient::instance().getVoiceEffectEnabled()) - { - retrieveVoiceFonts(); - - if (sShuttingDown) - { - return false; - } - - // Request the set of available voice fonts. - refreshVoiceEffectLists(false); - } - -#if USE_SESSION_GROUPS - // Rider: This code is completely unchanged from the original state machine - // It does not seem to be in active use... but I'd rather not rip it out. - // create the main session group - setState(stateCreatingSessionGroup); - sessionGroupCreateSendMessage(); -#endif - - state = VOICE_CHANNEL_STATE_START_CHANNEL_PROCESSING; - break; - - case VOICE_CHANNEL_STATE_START_CHANNEL_PROCESSING: - llcoro::suspend(); - state = VOICE_CHANNEL_STATE_PROCESS_CHANNEL; - break; - - case VOICE_CHANNEL_STATE_PROCESS_CHANNEL: - if (mTuningMode) - { - performMicTuning(); - } - else if (mCaptureBufferMode) - { - recordingAndPlaybackMode(); - } - else if (mProcessChannels && ((mNextAudioSession == NULL) || checkParcelChanged())) - { - // the parcel is changed, or we have no pending audio sessions, - // so try to request the parcel voice info - // if we have the cap, we move to the appropriate state - requestParcelVoiceInfo(); //suspends for http reply - } - else if (sessionNeedsRelog(mNextAudioSession)) - { - LL_INFOS("Voice") << "Session requesting reprovision and login." << LL_ENDL; - requestRelog(); - break; - } - else if (mNextAudioSession) - { - if (!mNextAudioSession->mIsP2P && !mProcessChannels) - { - llcoro::suspend(); - break; - } - - try - { - sessionStatePtr_t joinSession = mNextAudioSession; - mNextAudioSession.reset(); - mIsProcessingChannels = true; - if (!runSession(joinSession)) //suspends - { - mIsProcessingChannels = false; - LL_DEBUGS("Voice") << "runSession returned false; leaving inner loop" << LL_ENDL; - break; - } - else - { - mIsProcessingChannels = false; - LL_DEBUGS("Voice") - << "runSession returned true to inner loop" - << " RelogRequested=" << mRelogRequested - << " VoiceEnabled=" << mVoiceEnabled - << LL_ENDL; - } - } - catch (const LLCoros::Stop&) - { - LL_DEBUGS("LLVivoxVoiceClient") << "Received a shutdown exception" << LL_ENDL; - } - catch (const LLContinueError&) - { - LOG_UNHANDLED_EXCEPTION("LLVivoxVoiceClient"); - } - catch (...) - { - // Ideally for Windows need to log SEH exception instead or to set SEH - // handlers but bugsplat shows local variables for windows, which should - // be enough - LL_WARNS("Voice") << "voiceControlStateMachine crashed in state VOICE_CHANNEL_STATE_PROCESS_CHANNEL" - << " mRelogRequested " << mRelogRequested - << " mVoiceEnabled " << mVoiceEnabled - << " mIsProcessingChannels " << mIsProcessingChannels - << " mProcessChannels " << mProcessChannels - << LL_ENDL; - throw; - } - } - - state = VOICE_CHANNEL_STATE_NEXT_CHANNEL_DELAY; - break; - - case VOICE_CHANNEL_STATE_NEXT_CHANNEL_DELAY: - if (!mNextAudioSession) - { - llcoro::suspendUntilTimeout(1.0); - } - state = VOICE_CHANNEL_STATE_NEXT_CHANNEL_CHECK; - break; - - case VOICE_CHANNEL_STATE_NEXT_CHANNEL_CHECK: - if (mVoiceEnabled && !mRelogRequested) - { - state = VOICE_CHANNEL_STATE_START_CHANNEL_PROCESSING; - break; - } - else - { - mIsProcessingChannels = false; - LL_DEBUGS("Voice") - << "leaving inner waitForChannel loop" - << " RelogRequested=" << mRelogRequested - << " VoiceEnabled=" << mVoiceEnabled - << LL_ENDL; - state = VOICE_CHANNEL_STATE_LOGOUT; - break; - } - - case VOICE_CHANNEL_STATE_LOGOUT: - logoutOfVivox(true /*bool wait*/); - if (mRelogRequested) - { - state = VOICE_CHANNEL_STATE_RELOG; - } - else - { - state = VOICE_CHANNEL_STATE_DONE; - } - break; - - case VOICE_CHANNEL_STATE_RELOG: - LL_DEBUGS("Voice") << "Relog Requested, restarting provisioning" << LL_ENDL; - if (!provisionVoiceAccount()) - { - if (sShuttingDown) - { - return false; - } - LL_WARNS("Voice") << "provisioning voice failed; giving up" << LL_ENDL; - giveUp(); - return false; - } - if (mVoiceEnabled && mRelogRequested && isGatewayRunning()) - { - state = VOICE_CHANNEL_STATE_LOGIN; - } - else - { - state = VOICE_CHANNEL_STATE_DONE; - } - break; - case VOICE_CHANNEL_STATE_DONE: - LL_DEBUGS("Voice") - << "exiting" - << " RelogRequested=" << mRelogRequested - << " VoiceEnabled=" << mVoiceEnabled - << LL_ENDL; - return !sShuttingDown; - } - } while (true); -} - -bool LLVivoxVoiceClient::runSession(const sessionStatePtr_t &session) -{ - LL_INFOS("Voice") << "running new voice session " << session->mHandle << LL_ENDL; - - if (sShuttingDown || !mProcessChannels) - { - return false; - } - - bool joined_session = addAndJoinSession(session); - - if (!joined_session) - { - notifyStatusObservers(LLVoiceClientStatusObserver::ERROR_UNKNOWN); - - if (mSessionTerminateRequested) - { - LL_DEBUGS("Voice") << "runSession terminate requested " << LL_ENDL; - terminateAudioSession(true); - } - // if a relog has been requested then addAndJoineSession - // failed in a spectacular way and we need to back out. - // If this is not the case then we were simply trying to - // make a call and the other party rejected it. - return !mRelogRequested; - } - - notifyParticipantObservers(); - notifyVoiceFontObservers(); - - LLSD timeoutEvent(LLSDMap("timeout", LLSD::Boolean(true))); - - mIsInChannel = true; - mMuteMicDirty = true; - - while (!sShuttingDown - && mVoiceEnabled - && isGatewayRunning() - && !mSessionTerminateRequested - && !mTuningMode - && mProcessChannels) - { - sendCaptureAndRenderDevices(); // suspends - - if (sShuttingDown) - { - return false; - } - - if (mSessionTerminateRequested) - { - break; - } - - if (mAudioSession && mAudioSession->mParticipantsChanged) - { - mAudioSession->mParticipantsChanged = false; - notifyParticipantObservers(); - } - - if (!inSpatialChannel()) - { - // When in a non-spatial channel, never send positional updates. - mSpatialCoordsDirty = false; - } - else - { - updatePosition(); - - if (checkParcelChanged()) - { - // *RIDER: I think I can just return here if the parcel has changed - // and grab the new voice channel from the outside loop. - // - // if the parcel has changed, attempted to request the - // cap for the parcel voice info. If we can't request it - // then we don't have the cap URL so we do nothing and will - // recheck next time around - if (requestParcelVoiceInfo()) // suspends - { // The parcel voice URI has changed.. break out and reconnect. - break; - } - - if (sShuttingDown) - { - return false; - } - } - // Do the calculation that enforces the listener<->speaker tether (and also updates the real camera position) - enforceTether(); - } - sendPositionAndVolumeUpdate(); - - // Do notifications for expiring Voice Fonts. - if (mVoiceFontExpiryTimer.hasExpired()) - { - expireVoiceFonts(); - mVoiceFontExpiryTimer.setTimerExpirySec(VOICE_FONT_EXPIRY_INTERVAL); - } - - // send any requests to adjust mic and speaker settings if they have changed - sendLocalAudioUpdates(); - - mIsInitialized = true; - LLSD result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, UPDATE_THROTTLE_SECONDS, timeoutEvent); - - if (sShuttingDown) - { - return false; - } - - if (!result.has("timeout")) // logging the timeout event spams the log - { - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - } - if (result.has("session")) - { - if (result.has("handle")) - { - if (!mAudioSession) - { - LL_WARNS("Voice") << "Message for session handle \"" << result["handle"] << "\" while session is not initiated." << LL_ENDL; - continue; - } - if (result["handle"] != mAudioSession->mHandle) - { - LL_WARNS("Voice") << "Message for session handle \"" << result["handle"] << "\" while waiting for \"" << mAudioSession->mHandle << "\"." << LL_ENDL; - continue; - } - } - - std::string message = result["session"]; - - if (message == "removed") - { - LL_DEBUGS("Voice") << "session removed" << LL_ENDL; - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); - break; - } - } - else if (result.has("login")) - { - std::string message = result["login"]; - if (message == "account_logout") - { - LL_DEBUGS("Voice") << "logged out" << LL_ENDL; - mIsLoggedIn = false; - mRelogRequested = true; - break; - } - } - } - - if (sShuttingDown) - { - return false; - } - - mIsInChannel = false; - LL_DEBUGS("Voice") << "terminating at end of runSession" << LL_ENDL; - terminateAudioSession(true); - - return true; -} - -void LLVivoxVoiceClient::sendCaptureAndRenderDevices() -{ - if (mCaptureDeviceDirty || mRenderDeviceDirty) - { - std::ostringstream stream; - - buildSetCaptureDevice(stream); - buildSetRenderDevice(stream); - - if (!stream.str().empty()) - { - writeString(stream.str()); - } - - llcoro::suspendUntilTimeout(UPDATE_THROTTLE_SECONDS); - } -} - -void LLVivoxVoiceClient::recordingAndPlaybackMode() -{ - LL_INFOS("Voice") << "In voice capture/playback mode." << LL_ENDL; - - while (true) - { - LLSD command; - do - { - command = llcoro::suspendUntilEventOn(mVivoxPump); - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(command) << LL_ENDL; - } while (!command.has("recplay")); - - if (command["recplay"].asString() == "quit") - { - mCaptureBufferMode = false; - break; - } - else if (command["recplay"].asString() == "record") - { - voiceRecordBuffer(); - } - else if (command["recplay"].asString() == "playback") - { - voicePlaybackBuffer(); - } - } - - LL_INFOS("Voice") << "Leaving capture/playback mode." << LL_ENDL; - mCaptureBufferRecording = false; - mCaptureBufferRecorded = false; - mCaptureBufferPlaying = false; - - return; -} - -int LLVivoxVoiceClient::voiceRecordBuffer() -{ - LLSD timeoutResult(LLSDMap("recplay", "stop")); - - LL_INFOS("Voice") << "Recording voice buffer" << LL_ENDL; - - LLSD result; - - captureBufferRecordStartSendMessage(); - notifyVoiceFontObservers(); - - do - { - result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, CAPTURE_BUFFER_MAX_TIME, timeoutResult); - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - } while (!result.has("recplay")); - - mCaptureBufferRecorded = true; - - captureBufferRecordStopSendMessage(); - mCaptureBufferRecording = false; - - // Update UI, should really use a separate callback. - notifyVoiceFontObservers(); - - return true; - /*TODO expand return to move directly into play*/ -} - -int LLVivoxVoiceClient::voicePlaybackBuffer() -{ - LLSD timeoutResult(LLSDMap("recplay", "stop")); - - LL_INFOS("Voice") << "Playing voice buffer" << LL_ENDL; - - LLSD result; - - do - { - captureBufferPlayStartSendMessage(mPreviewVoiceFont); - - // Store the voice font being previewed, so that we know to restart if it changes. - mPreviewVoiceFontLast = mPreviewVoiceFont; - - do - { - // Update UI, should really use a separate callback. - notifyVoiceFontObservers(); - - result = llcoro::suspendUntilEventOnWithTimeout(mVivoxPump, CAPTURE_BUFFER_MAX_TIME, timeoutResult); - LL_DEBUGS("Voice") << "event=" << ll_stream_notation_sd(result) << LL_ENDL; - } while (!result.has("recplay")); - - if (result["recplay"] == "playback") - continue; // restart playback... May be a font change. - - break; - } while (true); - - // Stop playing. - captureBufferPlayStopSendMessage(); - mCaptureBufferPlaying = false; - - // Update UI, should really use a separate callback. - notifyVoiceFontObservers(); - - return true; -} - - -bool LLVivoxVoiceClient::performMicTuning() -{ - LL_INFOS("Voice") << "Entering voice tuning mode." << LL_ENDL; - mIsInTuningMode = true; - - while (mTuningMode && !sShuttingDown) - { - llcoro::suspendUntilTimeout(UPDATE_THROTTLE_SECONDS); - } - - mIsInTuningMode = false; - - //--------------------------------------------------------------------- - return true; -} - -//========================================================================= - -void LLVivoxVoiceClient::closeSocket(void) -{ - mSocket.reset(); - sConnected = false; - mConnectorEstablished = false; - mAccountLoggedIn = false; -} - -void LLVivoxVoiceClient::loginSendMessage() -{ - std::ostringstream stream; - - bool autoPostCrashDumps = gSavedSettings.getBOOL("VivoxAutoPostCrashDumps"); - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" << mAccountName << "" - << "" << mAccountPassword << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "VerifyAnswer" - << "false" - << "0" - << "Application" - << "5" - << (autoPostCrashDumps?"true":"") - << "\n\n\n"; - - LL_INFOS("Voice") << "Attempting voice login" << LL_ENDL; - writeString(stream.str()); -} - -void LLVivoxVoiceClient::logout() -{ - // Ensure that we'll re-request provisioning before logging in again - mAccountPassword.clear(); - mVoiceAccountServerURI.clear(); - - logoutSendMessage(); -} - -void LLVivoxVoiceClient::logoutSendMessage() -{ - if(mAccountLoggedIn) - { - LL_INFOS("Voice") << "Attempting voice logout" << LL_ENDL; - std::ostringstream stream; - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" - << "\n\n\n"; - - mAccountLoggedIn = false; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::sessionGroupCreateSendMessage() -{ - if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("Voice") << "creating session group" << LL_ENDL; - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "Normal" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::sessionCreateSendMessage(const sessionStatePtr_t &session, bool startAudio, bool startText) -{ - S32 font_index = getVoiceFontIndex(session->mVoiceFontID); - LL_DEBUGS("Voice") << "Requesting create: " << session->mSIPURI - << " with voice font: " << session->mVoiceFontID << " (" << font_index << ")" - << LL_ENDL; - - session->mCreateInProgress = true; - if(startAudio) - { - session->mMediaConnectInProgress = true; - } - - std::ostringstream stream; - stream - << "mSIPURI << "\" action=\"Session.Create.1\">" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" << session->mSIPURI << ""; - - static const std::string allowed_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - "0123456789" - "-._~"; - - if(!session->mHash.empty()) - { - stream - << "" << LLURI::escape(session->mHash, allowed_chars) << "" - << "SHA1UserName"; - } - - stream - << "" << (startAudio?"true":"false") << "" - << "" << (startText?"true":"false") << "" - << "" << mChannelName << "" - << "\n\n\n"; - LL_WARNS("Voice") << "Session.Create: " << stream.str() << LL_ENDL; - writeString(stream.str()); -} - -void LLVivoxVoiceClient::sessionGroupAddSessionSendMessage(const sessionStatePtr_t &session, bool startAudio, bool startText) -{ - LL_DEBUGS("Voice") << "Requesting create: " << session->mSIPURI << LL_ENDL; - - S32 font_index = getVoiceFontIndex(session->mVoiceFontID); - LL_DEBUGS("Voice") << "With voice font: " << session->mVoiceFontID << " (" << font_index << ")" << LL_ENDL; - - session->mCreateInProgress = true; - if(startAudio) - { - session->mMediaConnectInProgress = true; - } - - std::string password; - if(!session->mHash.empty()) - { - static const std::string allowed_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - "0123456789" - "-._~" - ; - password = LLURI::escape(session->mHash, allowed_chars); - } - - std::ostringstream stream; - stream - << "mSIPURI << "\" action=\"SessionGroup.AddSession.1\">" - << "" << session->mGroupHandle << "" - << "" << session->mSIPURI << "" - << "" << mChannelName << "" - << "" << (startAudio?"true":"false") << "" - << "" << (startText?"true":"false") << "" - << "" << password << "" - << "SHA1UserName" - << "\n\n\n" - ; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::sessionMediaConnectSendMessage(const sessionStatePtr_t &session) -{ - S32 font_index = getVoiceFontIndex(session->mVoiceFontID); - LL_DEBUGS("Voice") << "Connecting audio to session handle: " << session->mHandle - << " with voice font: " << session->mVoiceFontID << " (" << font_index << ")" - << LL_ENDL; - - session->mMediaConnectInProgress = true; - - std::ostringstream stream; - - stream - << "mHandle << "\" action=\"Session.MediaConnect.1\">" - << "" << session->mGroupHandle << "" - << "" << session->mHandle << "" - << "Audio" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::sessionTextConnectSendMessage(const sessionStatePtr_t &session) -{ - LL_DEBUGS("Voice") << "connecting text to session handle: " << session->mHandle << LL_ENDL; - - std::ostringstream stream; - - stream - << "mHandle << "\" action=\"Session.TextConnect.1\">" - << "" << session->mGroupHandle << "" - << "" << session->mHandle << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::sessionTerminate() -{ - mSessionTerminateRequested = true; -} - -void LLVivoxVoiceClient::requestRelog() -{ - mSessionTerminateRequested = true; - mRelogRequested = true; -} - - -void LLVivoxVoiceClient::leaveAudioSession() -{ - if(mAudioSession) - { - LL_DEBUGS("Voice") << "leaving session: " << mAudioSession->mSIPURI << LL_ENDL; - - if(!mAudioSession->mHandle.empty()) - { - -#if RECORD_EVERYTHING - // HACK: for testing only - // Save looped recording - std::string savepath("/tmp/vivoxrecording"); - { - time_t now = time(NULL); - const size_t BUF_SIZE = 64; - char time_str[BUF_SIZE]; /* Flawfinder: ignore */ - - strftime(time_str, BUF_SIZE, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now)); - savepath += time_str; - } - recordingLoopSave(savepath); -#endif - - sessionMediaDisconnectSendMessage(mAudioSession); - } - else - { - LL_WARNS("Voice") << "called with no session handle" << LL_ENDL; - } - } - else - { - LL_WARNS("Voice") << "called with no active session" << LL_ENDL; - } - sessionTerminate(); -} - -void LLVivoxVoiceClient::sessionTerminateSendMessage(const sessionStatePtr_t &session) -{ - std::ostringstream stream; - - sessionGroupTerminateSendMessage(session); - return; - /* - LL_DEBUGS("Voice") << "Sending Session.Terminate with handle " << session->mHandle << LL_ENDL; - stream - << "" - << "" << session->mHandle << "" - << "\n\n\n"; - - writeString(stream.str()); - */ -} - -void LLVivoxVoiceClient::sessionGroupTerminateSendMessage(const sessionStatePtr_t &session) -{ - std::ostringstream stream; - - LL_DEBUGS("Voice") << "Sending SessionGroup.Terminate with handle " << session->mGroupHandle << LL_ENDL; - stream - << "" - << "" << session->mGroupHandle << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::sessionMediaDisconnectSendMessage(const sessionStatePtr_t &session) -{ - std::ostringstream stream; - sessionGroupTerminateSendMessage(session); - return; - /* - LL_DEBUGS("Voice") << "Sending Session.MediaDisconnect with handle " << session->mHandle << LL_ENDL; - stream - << "" - << "" << session->mGroupHandle << "" - << "" << session->mHandle << "" - << "Audio" - << "\n\n\n"; - - writeString(stream.str()); - */ - -} - - -void LLVivoxVoiceClient::getCaptureDevicesSendMessage() -{ - std::ostringstream stream; - stream - << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::getRenderDevicesSendMessage() -{ - std::ostringstream stream; - stream - << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::clearCaptureDevices() -{ - LL_DEBUGS("Voice") << "called" << LL_ENDL; - mCaptureDevices.clear(); -} - -void LLVivoxVoiceClient::addCaptureDevice(const LLVoiceDevice& device) -{ - LL_DEBUGS("Voice") << "display: '" << device.display_name << "' device: '" << device.full_name << "'" << LL_ENDL; - mCaptureDevices.push_back(device); -} - -LLVoiceDeviceList& LLVivoxVoiceClient::getCaptureDevices() -{ - return mCaptureDevices; -} - -void LLVivoxVoiceClient::setCaptureDevice(const std::string& name) -{ - if(name == "Default") - { - if(!mCaptureDevice.empty()) - { - mCaptureDevice.clear(); - mCaptureDeviceDirty = true; - } - } - else - { - if(mCaptureDevice != name) - { - mCaptureDevice = name; - mCaptureDeviceDirty = true; - } - } -} -void LLVivoxVoiceClient::setDevicesListUpdated(bool state) -{ - mDevicesListUpdated = state; -} - -void LLVivoxVoiceClient::clearRenderDevices() -{ - LL_DEBUGS("Voice") << "called" << LL_ENDL; - mRenderDevices.clear(); -} - -void LLVivoxVoiceClient::addRenderDevice(const LLVoiceDevice& device) -{ - LL_DEBUGS("Voice") << "display: '" << device.display_name << "' device: '" << device.full_name << "'" << LL_ENDL; - mRenderDevices.push_back(device); -} - -LLVoiceDeviceList& LLVivoxVoiceClient::getRenderDevices() -{ - return mRenderDevices; -} - -void LLVivoxVoiceClient::setRenderDevice(const std::string& name) -{ - if(name == "Default") - { - if(!mRenderDevice.empty()) - { - mRenderDevice.clear(); - mRenderDeviceDirty = true; - } - } - else - { - if(mRenderDevice != name) - { - mRenderDevice = name; - mRenderDeviceDirty = true; - } - } - -} - -void LLVivoxVoiceClient::tuningStart() -{ - LL_DEBUGS("Voice") << "Starting tuning" << LL_ENDL; - mTuningMode = true; - if (!mIsCoroutineActive) - { - LLCoros::instance().launch("LLVivoxVoiceClient::voiceControlCoro", - boost::bind(&LLVivoxVoiceClient::voiceControlCoro, LLVivoxVoiceClient::getInstance())); - } - else if (mIsInChannel) - { - LL_DEBUGS("Voice") << "no channel" << LL_ENDL; - sessionTerminate(); - } -} - -void LLVivoxVoiceClient::tuningStop() -{ - mTuningMode = false; - // force a renegotiation. - mCurrentParcelLocalID = 0; - mCurrentRegionName = ""; -} - -bool LLVivoxVoiceClient::inTuningMode() -{ - return mIsInTuningMode; -} - -void LLVivoxVoiceClient::tuningRenderStartSendMessage(const std::string& name, bool loop) -{ - mTuningAudioFile = name; - std::ostringstream stream; - stream - << "" - << "" << mTuningAudioFile << "" - << "" << (loop?"1":"0") << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::tuningRenderStopSendMessage() -{ - std::ostringstream stream; - stream - << "" - << "" << mTuningAudioFile << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::tuningCaptureStartSendMessage(int loop) -{ - LL_DEBUGS("Voice") << "sending CaptureAudioStart" << LL_ENDL; - - std::ostringstream stream; - stream - << "" - << "-1" - << "" << loop << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::tuningCaptureStopSendMessage() -{ - LL_DEBUGS("Voice") << "sending CaptureAudioStop" << LL_ENDL; - - std::ostringstream stream; - stream - << "" - << "\n\n\n"; - - writeString(stream.str()); - - mTuningEnergy = 0.0f; -} - -void LLVivoxVoiceClient::tuningSetMicVolume(float volume) -{ - int scaled_volume = scale_mic_volume(volume); - - if(scaled_volume != mTuningMicVolume) - { - mTuningMicVolume = scaled_volume; - mTuningMicVolumeDirty = true; - } -} - -void LLVivoxVoiceClient::tuningSetSpeakerVolume(float volume) -{ - int scaled_volume = scale_speaker_volume(volume); - - if(scaled_volume != mTuningSpeakerVolume) - { - mTuningSpeakerVolume = scaled_volume; - mTuningSpeakerVolumeDirty = true; - } -} - -float LLVivoxVoiceClient::tuningGetEnergy(void) -{ - return mTuningEnergy; -} - -bool LLVivoxVoiceClient::deviceSettingsAvailable() -{ - bool result = true; - - if(!sConnected) - result = false; - - if(mRenderDevices.empty()) - result = false; - - return result; -} -bool LLVivoxVoiceClient::deviceSettingsUpdated() -{ - bool updated = mDevicesListUpdated; - if (mDevicesListUpdated) - { - // a hot swap event or a polling of the audio devices has been parsed since the last redraw of the input and output device panel. - mDevicesListUpdated = false; // toggle the setting - } - return updated; -} - -void LLVivoxVoiceClient::refreshDeviceLists(bool clearCurrentList) -{ - if(clearCurrentList) - { - clearCaptureDevices(); - clearRenderDevices(); - } - getCaptureDevicesSendMessage(); - getRenderDevicesSendMessage(); -} - -void LLVivoxVoiceClient::daemonDied() -{ - // The daemon died, so the connection is gone. Reset everything and start over. - LL_WARNS("Voice") << "Connection to vivox daemon lost. Resetting state."<< LL_ENDL; - - //TODO: Try to relaunch the daemon -} - -void LLVivoxVoiceClient::giveUp() -{ - // All has failed. Clean up and stop trying. - LL_WARNS("Voice") << "Terminating Voice Service" << LL_ENDL; - closeSocket(); - cleanUp(); -} - -static void oldSDKTransform (LLVector3 &left, LLVector3 &up, LLVector3 &at, LLVector3d &pos, LLVector3 &vel) -{ - F32 nat[3], nup[3], nl[3]; // the new at, up, left vectors and the new position and velocity -// F32 nvel[3]; - F64 npos[3]; - - // The original XML command was sent like this: - /* - << "" - << "" << pos[VX] << "" - << "" << pos[VZ] << "" - << "" << pos[VY] << "" - << "" - << "" - << "" << mAvatarVelocity[VX] << "" - << "" << mAvatarVelocity[VZ] << "" - << "" << mAvatarVelocity[VY] << "" - << "" - << "" - << "" << l.mV[VX] << "" - << "" << u.mV[VX] << "" - << "" << a.mV[VX] << "" - << "" - << "" - << "" << l.mV[VZ] << "" - << "" << u.mV[VY] << "" - << "" << a.mV[VZ] << "" - << "" - << "" - << "" << l.mV [VY] << "" - << "" << u.mV [VZ] << "" - << "" << a.mV [VY] << "" - << ""; - */ - -#if 1 - // This was the original transform done when building the XML command - nat[0] = left.mV[VX]; - nat[1] = up.mV[VX]; - nat[2] = at.mV[VX]; - - nup[0] = left.mV[VZ]; - nup[1] = up.mV[VY]; - nup[2] = at.mV[VZ]; - - nl[0] = left.mV[VY]; - nl[1] = up.mV[VZ]; - nl[2] = at.mV[VY]; - - npos[0] = pos.mdV[VX]; - npos[1] = pos.mdV[VZ]; - npos[2] = pos.mdV[VY]; - -// nvel[0] = vel.mV[VX]; -// nvel[1] = vel.mV[VZ]; -// nvel[2] = vel.mV[VY]; - - for(int i=0;i<3;++i) { - at.mV[i] = nat[i]; - up.mV[i] = nup[i]; - left.mV[i] = nl[i]; - pos.mdV[i] = npos[i]; - } - - // This was the original transform done in the SDK - nat[0] = at.mV[2]; - nat[1] = 0; // y component of at vector is always 0, this was up[2] - nat[2] = -1 * left.mV[2]; - - // We override whatever the application gives us - nup[0] = 0; // x component of up vector is always 0 - nup[1] = 1; // y component of up vector is always 1 - nup[2] = 0; // z component of up vector is always 0 - - nl[0] = at.mV[0]; - nl[1] = 0; // y component of left vector is always zero, this was up[0] - nl[2] = -1 * left.mV[0]; - - npos[2] = pos.mdV[2] * -1.0; - npos[1] = pos.mdV[1]; - npos[0] = pos.mdV[0]; - - for(int i=0;i<3;++i) { - at.mV[i] = nat[i]; - up.mV[i] = nup[i]; - left.mV[i] = nl[i]; - pos.mdV[i] = npos[i]; - } -#else - // This is the compose of the two transforms (at least, that's what I'm trying for) - nat[0] = at.mV[VX]; - nat[1] = 0; // y component of at vector is always 0, this was up[2] - nat[2] = -1 * up.mV[VZ]; - - // We override whatever the application gives us - nup[0] = 0; // x component of up vector is always 0 - nup[1] = 1; // y component of up vector is always 1 - nup[2] = 0; // z component of up vector is always 0 - - nl[0] = left.mV[VX]; - nl[1] = 0; // y component of left vector is always zero, this was up[0] - nl[2] = -1 * left.mV[VY]; - - npos[0] = pos.mdV[VX]; - npos[1] = pos.mdV[VZ]; - npos[2] = pos.mdV[VY] * -1.0; - - nvel[0] = vel.mV[VX]; - nvel[1] = vel.mV[VZ]; - nvel[2] = vel.mV[VY]; - - for(int i=0;i<3;++i) { - at.mV[i] = nat[i]; - up.mV[i] = nup[i]; - left.mV[i] = nl[i]; - pos.mdV[i] = npos[i]; - } - -#endif -} - -void LLVivoxVoiceClient::setHidden(bool hidden) -{ - mHidden = hidden; - - if (mHidden && inSpatialChannel()) - { - // get out of the channel entirely - leaveAudioSession(); - } - else - { - sendPositionAndVolumeUpdate(); - } -} - -void LLVivoxVoiceClient::sendPositionAndVolumeUpdate(void) -{ - std::ostringstream stream; - - if (mSpatialCoordsDirty && inSpatialChannel()) - { - LLVector3 l, u, a, vel; - LLVector3d pos; - - mSpatialCoordsDirty = false; - - // Always send both speaker and listener positions together. - stream << "" - << "" << getAudioSessionHandle() << ""; - - stream << ""; - - LLMatrix3 avatarRot = mAvatarRot.getMatrix3(); - -// LL_DEBUGS("Voice") << "Sending speaker position " << mAvatarPosition << LL_ENDL; - l = avatarRot.getLeftRow(); - u = avatarRot.getUpRow(); - a = avatarRot.getFwdRow(); - - pos = mAvatarPosition; - vel = mAvatarVelocity; - - // SLIM SDK: the old SDK was doing a transform on the passed coordinates that the new one doesn't do anymore. - // The old transform is replicated by this function. - oldSDKTransform(l, u, a, pos, vel); - - if (mHidden) - { - for (int i=0;i<3;++i) - { - pos.mdV[i] = VX_NULL_POSITION; - } - } - - stream - << "" - << "" << pos.mdV[VX] << "" - << "" << pos.mdV[VY] << "" - << "" << pos.mdV[VZ] << "" - << "" - << "" - << "" << vel.mV[VX] << "" - << "" << vel.mV[VY] << "" - << "" << vel.mV[VZ] << "" - << "" - << "" - << "" << a.mV[VX] << "" - << "" << a.mV[VY] << "" - << "" << a.mV[VZ] << "" - << "" - << "" - << "" << u.mV[VX] << "" - << "" << u.mV[VY] << "" - << "" << u.mV[VZ] << "" - << "" - << "" - << "" << l.mV [VX] << "" - << "" << l.mV [VY] << "" - << "" << l.mV [VZ] << "" - << "" - ; - - stream << ""; - - stream << ""; - - LLVector3d earPosition; - LLVector3 earVelocity; - LLMatrix3 earRot; - - switch(mEarLocation) - { - case earLocCamera: - default: - earPosition = mCameraPosition; - earVelocity = mCameraVelocity; - earRot = mCameraRot; - break; - - case earLocAvatar: - earPosition = mAvatarPosition; - earVelocity = mAvatarVelocity; - earRot = avatarRot; - break; - - case earLocMixed: - earPosition = mAvatarPosition; - earVelocity = mAvatarVelocity; - earRot = mCameraRot; - break; - } - - l = earRot.getLeftRow(); - u = earRot.getUpRow(); - a = earRot.getFwdRow(); - - pos = earPosition; - vel = earVelocity; - - - oldSDKTransform(l, u, a, pos, vel); - - if (mHidden) - { - for (int i=0;i<3;++i) - { - pos.mdV[i] = VX_NULL_POSITION; - } - } - - stream - << "" - << "" << pos.mdV[VX] << "" - << "" << pos.mdV[VY] << "" - << "" << pos.mdV[VZ] << "" - << "" - << "" - << "" << vel.mV[VX] << "" - << "" << vel.mV[VY] << "" - << "" << vel.mV[VZ] << "" - << "" - << "" - << "" << a.mV[VX] << "" - << "" << a.mV[VY] << "" - << "" << a.mV[VZ] << "" - << "" - << "" - << "" << u.mV[VX] << "" - << "" << u.mV[VY] << "" - << "" << u.mV[VZ] << "" - << "" - << "" - << "" << l.mV [VX] << "" - << "" << l.mV [VY] << "" - << "" << l.mV [VZ] << "" - << "" - ; - - stream << ""; - - stream << "1"; //do not generate responses for update requests - stream << "\n\n\n"; - } - - if(mAudioSession && (mAudioSession->mVolumeDirty || mAudioSession->mMuteDirty)) - { - participantMap::iterator iter = mAudioSession->mParticipantsByURI.begin(); - - mAudioSession->mVolumeDirty = false; - mAudioSession->mMuteDirty = false; - - for(; iter != mAudioSession->mParticipantsByURI.end(); iter++) - { - participantStatePtr_t p(iter->second); - - if(p->mVolumeDirty) - { - // Can't set volume/mute for yourself - if(!p->mIsSelf) - { - // scale from the range 0.0-1.0 to vivox volume in the range 0-100 - S32 volume = ll_round(p->mVolume / VOLUME_SCALE_VIVOX); - bool mute = p->mOnMuteList; - - if(mute) - { - // SetParticipantMuteForMe doesn't work in p2p sessions. - // If we want the user to be muted, set their volume to 0 as well. - // This isn't perfect, but it will at least reduce their volume to a minimum. - volume = 0; - // Mark the current volume level as set to prevent incoming events - // changing it to 0, so that we can return to it when unmuting. - p->mVolumeSet = true; - } - - if(volume == 0) - { - mute = true; - } - - LL_DEBUGS("Voice") << "Setting volume/mute for avatar " << p->mAvatarID << " to " << volume << (mute?"/true":"/false") << LL_ENDL; - - // SLIM SDK: Send both volume and mute commands. - - // Send a "volume for me" command for the user. - stream << "" - << "" << getAudioSessionHandle() << "" - << "" << p->mURI << "" - << "" << volume << "" - << "\n\n\n"; - - if(!mAudioSession->mIsP2P) - { - // Send a "mute for me" command for the user - // Doesn't work in P2P sessions - stream << "" - << "" << getAudioSessionHandle() << "" - << "" << p->mURI << "" - << "" << (mute?"1":"0") << "" - << "Audio" - << "\n\n\n"; - } - } - - p->mVolumeDirty = false; - } - } - } - - std::string update(stream.str()); - if(!update.empty()) - { - LL_DEBUGS("VoiceUpdate") << "sending update " << update << LL_ENDL; - writeString(update); - } - -} - -void LLVivoxVoiceClient::buildSetCaptureDevice(std::ostringstream &stream) -{ - if(mCaptureDeviceDirty) - { - LL_DEBUGS("Voice") << "Setting input device = \"" << mCaptureDevice << "\"" << LL_ENDL; - - stream - << "" - << "" << mCaptureDevice << "" - << "" - << "\n\n\n"; - - mCaptureDeviceDirty = false; - } -} - -void LLVivoxVoiceClient::buildSetRenderDevice(std::ostringstream &stream) -{ - if(mRenderDeviceDirty) - { - LL_DEBUGS("Voice") << "Setting output device = \"" << mRenderDevice << "\"" << LL_ENDL; - - stream - << "" - << "" << mRenderDevice << "" - << "" - << "\n\n\n"; - mRenderDeviceDirty = false; - } -} - -void LLVivoxVoiceClient::sendLocalAudioUpdates() -{ - // Check all of the dirty states and then send messages to those needing to be changed. - // Tuningmode hands its own mute settings. - std::ostringstream stream; - - if (mMuteMicDirty && !mTuningMode) - { - mMuteMicDirty = false; - - // Send a local mute command. - - LL_INFOS("Voice") << "Sending MuteLocalMic command with parameter " << (mMuteMic ? "true" : "false") << LL_ENDL; - - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" << (mMuteMic ? "true" : "false") << "" - << "\n\n\n"; - - } - - if (mSpeakerMuteDirty && !mTuningMode) - { - const char *muteval = ((mSpeakerVolume <= scale_speaker_volume(0)) ? "true" : "false"); - - mSpeakerMuteDirty = false; - - LL_INFOS("Voice") << "Setting speaker mute to " << muteval << LL_ENDL; - - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" << muteval << "" - << "\n\n\n"; - - } - - if (mSpeakerVolumeDirty) - { - mSpeakerVolumeDirty = false; - - LL_INFOS("Voice") << "Setting speaker volume to " << mSpeakerVolume << LL_ENDL; - - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" << mSpeakerVolume << "" - << "\n\n\n"; - - } - - if (mMicVolumeDirty) - { - mMicVolumeDirty = false; - - LL_INFOS("Voice") << "Setting mic volume to " << mMicVolume << LL_ENDL; - - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "" << mMicVolume << "" - << "\n\n\n"; - } - - - if (!stream.str().empty()) - { - writeString(stream.str()); - } -} - -/** - * Because of the recurring voice cutout issues (SL-15072) we are going to try - * to disable the automatic VAD (Voice Activity Detection) and set the associated - * parameters directly. We will expose them via Debug Settings and that should - * let us iterate on a collection of values that work for us. Hopefully! - * - * From the VIVOX Docs: - * - * VadAuto: A flag indicating if the automatic VAD is enabled (1) or disabled (0) - * - * VadHangover: The time (in milliseconds) that it takes - * for the VAD to switch back to silence from speech mode after the last speech - * frame has been detected. - * - * VadNoiseFloor: A dimensionless value between 0 and - * 20000 (default 576) that controls the maximum level at which the noise floor - * may be set at by the VAD's noise tracking. Too low of a value will make noise - * tracking ineffective (A value of 0 disables noise tracking and the VAD then - * relies purely on the sensitivity property). Too high of a value will make - * long speech classifiable as noise. - * - * VadSensitivity: A dimensionless value between 0 and - * 100, indicating the 'sensitivity of the VAD'. Increasing this value corresponds - * to decreasing the sensitivity of the VAD (i.e. '0' is most sensitive, - * while 100 is 'least sensitive') - */ -void LLVivoxVoiceClient::setupVADParams(unsigned int vad_auto, - unsigned int vad_hangover, - unsigned int vad_noise_floor, - unsigned int vad_sensitivity) -{ - std::ostringstream stream; - - LL_INFOS("Voice") << "Setting the automatic VAD to " - << (vad_auto ? "True" : "False") - << " and discrete values to" - << " VadHangover = " << vad_hangover - << ", VadSensitivity = " << vad_sensitivity - << ", VadNoiseFloor = " << vad_noise_floor - << LL_ENDL; - - // Create a request to set the VAD parameters: - stream << "" - << "" << vad_auto << "" - << "" << vad_hangover << "" - << "" << vad_sensitivity << "" - << "" << vad_noise_floor << "" - << "\n\n\n"; - - if (!stream.str().empty()) - { - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::onVADSettingsChange() -{ - // pick up the VAD variables (one of which was changed) - unsigned int vad_auto = gSavedSettings.getU32("VivoxVadAuto"); - unsigned int vad_hangover = gSavedSettings.getU32("VivoxVadHangover"); - unsigned int vad_noise_floor = gSavedSettings.getU32("VivoxVadNoiseFloor"); - unsigned int vad_sensitivity = gSavedSettings.getU32("VivoxVadSensitivity"); - - // build a VAD params change request and send it to SLVoice - setupVADParams(vad_auto, vad_hangover, vad_noise_floor, vad_sensitivity); -} - -///////////////////////////// -// Response/Event handlers - -void LLVivoxVoiceClient::connectorCreateResponse(int statusCode, std::string &statusString, std::string &connectorHandle, std::string &versionID) -{ - LLSD result = LLSD::emptyMap(); - - if(statusCode == 0) - { - // Connector created, move forward. - if (connectorHandle == LLVivoxSecurity::getInstance()->connectorHandle()) - { - LL_INFOS("Voice") << "Voice connector succeeded, Vivox SDK version is " << versionID << " connector handle " << connectorHandle << LL_ENDL; - mVoiceVersion.serverVersion = versionID; - mConnectorEstablished = true; - mTerminateDaemon = false; - - result["connector"] = LLSD::Boolean(true); - } - else - { - // This shouldn't happen - we are somehow out of sync with SLVoice - // or possibly there are two things trying to run SLVoice at once - // or someone is trying to hack into it. - LL_WARNS("Voice") << "Connector returned wrong handle " - << "(" << connectorHandle << ")" - << " expected (" << LLVivoxSecurity::getInstance()->connectorHandle() << ")" - << LL_ENDL; - result["connector"] = LLSD::Boolean(false); - // Give up. - mTerminateDaemon = true; - } - } - else if (statusCode == 10028) // web request timeout prior to login - { - // this is usually fatal, but a long timeout might work - result["connector"] = LLSD::Boolean(false); - result["retry"] = LLSD::Real(CONNECT_ATTEMPT_TIMEOUT); - - LL_WARNS("Voice") << "Voice connection failed" << LL_ENDL; - } - else if (statusCode == 10006) // name resolution failure - a shorter retry may work - { - // some networks have slower DNS, but a short timeout might let it catch up - result["connector"] = LLSD::Boolean(false); - result["retry"] = LLSD::Real(CONNECT_DNS_TIMEOUT); - - LL_WARNS("Voice") << "Voice connection DNS lookup failed" << LL_ENDL; - } - else // unknown failure - give up - { - LL_WARNS("Voice") << "Voice connection failure ("<< statusCode << "): " << statusString << LL_ENDL; - mTerminateDaemon = true; - result["connector"] = LLSD::Boolean(false); - } - - mVivoxPump.post(result); -} - -void LLVivoxVoiceClient::loginResponse(int statusCode, std::string &statusString, std::string &accountHandle, int numberOfAliases) -{ - LLSD result = LLSD::emptyMap(); - - LL_DEBUGS("Voice") << "Account.Login response (" << statusCode << "): " << statusString << LL_ENDL; - - // Status code of 20200 means "bad password". We may want to special-case that at some point. - - if ( statusCode == HTTP_UNAUTHORIZED ) - { - // Login failure which is probably caused by the delay after a user's password being updated. - LL_INFOS("Voice") << "Account.Login response failure (" << statusCode << "): " << statusString << LL_ENDL; - result["login"] = LLSD::String("retry"); - } - else if(statusCode != 0) - { - LL_WARNS("Voice") << "Account.Login response failure (" << statusCode << "): " << statusString << LL_ENDL; - result["login"] = LLSD::String("failed"); - } - else - { - // Login succeeded, move forward. - mAccountLoggedIn = true; - mNumberOfAliases = numberOfAliases; - result["login"] = LLSD::String("response_ok"); - } - - mVivoxPump.post(result); - -} - -void LLVivoxVoiceClient::sessionCreateResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle) -{ - sessionStatePtr_t session(findSessionBeingCreatedByURI(requestId)); - - if(session) - { - session->mCreateInProgress = false; - } - - if(statusCode != 0) - { - LL_WARNS("Voice") << "Session.Create response failure (" << statusCode << "): " << statusString << LL_ENDL; - if(session) - { - session->mErrorStatusCode = statusCode; - session->mErrorStatusString = statusString; - if(session == mAudioSession) - { - LLSD vivoxevent(LLSDMap("handle", LLSD::String(sessionHandle)) - ("session", "failed") - ("reason", LLSD::Integer(statusCode))); - - mVivoxPump.post(vivoxevent); - } - else - { - reapSession(session); - } - } - } - else - { - LL_INFOS("Voice") << "Session.Create response received (success), session handle is " << sessionHandle << LL_ENDL; - if(session) - { - setSessionHandle(session, sessionHandle); - } - LLSD vivoxevent(LLSDMap("handle", LLSD::String(sessionHandle)) - ("session", "created")); - - mVivoxPump.post(vivoxevent); - } -} - -void LLVivoxVoiceClient::sessionGroupAddSessionResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle) -{ - sessionStatePtr_t session(findSessionBeingCreatedByURI(requestId)); - - if(session) - { - session->mCreateInProgress = false; - } - - if(statusCode != 0) - { - LL_WARNS("Voice") << "SessionGroup.AddSession response failure (" << statusCode << "): " << statusString << LL_ENDL; - if(session) - { - session->mErrorStatusCode = statusCode; - session->mErrorStatusString = statusString; - if(session == mAudioSession) - { - LLSD vivoxevent(LLSDMap("handle", LLSD::String(sessionHandle)) - ("session", "failed")); - - mVivoxPump.post(vivoxevent); - } - else - { - reapSession(session); - } - } - } - else - { - LL_DEBUGS("Voice") << "SessionGroup.AddSession response received (success), session handle is " << sessionHandle << LL_ENDL; - if(session) - { - setSessionHandle(session, sessionHandle); - } - - LLSD vivoxevent(LLSDMap("handle", LLSD::String(sessionHandle)) - ("session", "added")); - - mVivoxPump.post(vivoxevent); - - } -} - -void LLVivoxVoiceClient::sessionConnectResponse(std::string &requestId, int statusCode, std::string &statusString) -{ - sessionStatePtr_t session(findSession(requestId)); - // 1026 is session already has media, somehow mediaconnect was called twice on the same session. - // set the session info to reflect that the user is already connected. - if (statusCode == 1026) - { - session->mVoiceActive = true; - session->mMediaConnectInProgress = false; - session->mMediaStreamState = streamStateConnected; - //session->mTextStreamState = streamStateConnected; - session->mErrorStatusCode = 0; - } - else if (statusCode != 0) - { - LL_WARNS("Voice") << "Session.Connect response failure (" << statusCode << "): " << statusString << LL_ENDL; - if (session) - { - session->mMediaConnectInProgress = false; - session->mErrorStatusCode = statusCode; - session->mErrorStatusString = statusString; - } - } - else - { - LL_DEBUGS("Voice") << "Session.Connect response received (success)" << LL_ENDL; - } -} - -void LLVivoxVoiceClient::logoutResponse(int statusCode, std::string &statusString) -{ - if(statusCode != 0) - { - LL_WARNS("Voice") << "Account.Logout response failure: " << statusString << LL_ENDL; - // Should this ever fail? do we care if it does? - } - LLSD vivoxevent(LLSDMap("logout", LLSD::Boolean(true))); - - mVivoxPump.post(vivoxevent); -} - -void LLVivoxVoiceClient::connectorShutdownResponse(int statusCode, std::string &statusString) -{ - if(statusCode != 0) - { - LL_WARNS("Voice") << "Connector.InitiateShutdown response failure: " << statusString << LL_ENDL; - // Should this ever fail? do we care if it does? - } - - sConnected = false; - mShutdownComplete = true; - - LLSD vivoxevent(LLSDMap("connector", LLSD::Boolean(false))); - - mVivoxPump.post(vivoxevent); -} - -void LLVivoxVoiceClient::sessionAddedEvent( - std::string &uriString, - std::string &alias, - std::string &sessionHandle, - std::string &sessionGroupHandle, - bool isChannel, - bool incoming, - std::string &nameString, - std::string &applicationString) -{ - sessionStatePtr_t session; - - LL_INFOS("Voice") << "session " << uriString << ", alias " << alias << ", name " << nameString << " handle " << sessionHandle << LL_ENDL; - - session = addSession(uriString, sessionHandle); - if(session) - { - session->mGroupHandle = sessionGroupHandle; - session->mIsChannel = isChannel; - session->mIncoming = incoming; - session->mAlias = alias; - - // Generate a caller UUID -- don't need to do this for channels - if(!session->mIsChannel) - { - if(IDFromName(session->mSIPURI, session->mCallerID)) - { - // Normal URI(base64-encoded UUID) - } - else if(!session->mAlias.empty() && IDFromName(session->mAlias, session->mCallerID)) - { - // Wrong URI, but an alias is available. Stash the incoming URI as an alternate - session->mAlternateSIPURI = session->mSIPURI; - - // and generate a proper URI from the ID. - setSessionURI(session, sipURIFromID(session->mCallerID)); - } - else - { - LL_INFOS("Voice") << "Could not generate caller id from uri, using hash of uri " << session->mSIPURI << LL_ENDL; - session->mCallerID.generate(session->mSIPURI); - session->mSynthesizedCallerID = true; - - // Can't look up the name in this case -- we have to extract it from the URI. - std::string namePortion = nameFromsipURI(session->mSIPURI); - if(namePortion.empty()) - { - // Didn't seem to be a SIP URI, just use the whole provided name. - namePortion = nameString; - } - - // Some incoming names may be separated with an underscore instead of a space. Fix this. - LLStringUtil::replaceChar(namePortion, '_', ' '); - - // Act like we just finished resolving the name (this stores it in all the right places) - avatarNameResolved(session->mCallerID, namePortion); - } - - LL_INFOS("Voice") << "caller ID: " << session->mCallerID << LL_ENDL; - - if(!session->mSynthesizedCallerID) - { - // If we got here, we don't have a proper name. Initiate a lookup. - lookupName(session->mCallerID); - } - } - } -} - -void LLVivoxVoiceClient::sessionGroupAddedEvent(std::string &sessionGroupHandle) -{ - LL_DEBUGS("Voice") << "handle " << sessionGroupHandle << LL_ENDL; - -#if USE_SESSION_GROUPS - if(mMainSessionGroupHandle.empty()) - { - // This is the first (i.e. "main") session group. Save its handle. - mMainSessionGroupHandle = sessionGroupHandle; - } - else - { - LL_DEBUGS("Voice") << "Already had a session group handle " << mMainSessionGroupHandle << LL_ENDL; - } -#endif -} - -void LLVivoxVoiceClient::joinedAudioSession(const sessionStatePtr_t &session) -{ - LL_DEBUGS("Voice") << "Joined Audio Session" << LL_ENDL; - if(mAudioSession != session) - { - sessionStatePtr_t oldSession = mAudioSession; - - mAudioSession = session; - - // The old session may now need to be deleted. - reapSession(oldSession); - } - - // This is the session we're joining. - if(mIsJoiningSession) - { - LLSD vivoxevent(LLSDMap("handle", LLSD::String(session->mHandle)) - ("session", "joined")); - - mVivoxPump.post(vivoxevent); - - // Add the current user as a participant here. - participantStatePtr_t participant(session->addParticipant(sipURIFromName(mAccountName))); - if(participant) - { - participant->mIsSelf = true; - lookupName(participant->mAvatarID); - - LL_INFOS("Voice") << "added self as participant \"" << participant->mAccountName - << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; - } - - if(!session->mIsChannel) - { - // this is a p2p session. Make sure the other end is added as a participant. - participantStatePtr_t participant(session->addParticipant(session->mSIPURI)); - if(participant) - { - if(participant->mAvatarIDValid) - { - lookupName(participant->mAvatarID); - } - else if(!session->mName.empty()) - { - participant->mDisplayName = session->mName; - avatarNameResolved(participant->mAvatarID, session->mName); - } - - // TODO: Question: Do we need to set up mAvatarID/mAvatarIDValid here? - LL_INFOS("Voice") << "added caller as participant \"" << participant->mAccountName - << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; - } - } - } -} - -void LLVivoxVoiceClient::sessionRemovedEvent( - std::string &sessionHandle, - std::string &sessionGroupHandle) -{ - LL_INFOS("Voice") << "handle " << sessionHandle << LL_ENDL; - - sessionStatePtr_t session(findSession(sessionHandle)); - if(session) - { - leftAudioSession(session); - - // This message invalidates the session's handle. Set it to empty. - clearSessionHandle(session); - - // This also means that the session's session group is now empty. - // Terminate the session group so it doesn't leak. - sessionGroupTerminateSendMessage(session); - - // Reset the media state (we now have no info) - session->mMediaStreamState = streamStateUnknown; - //session->mTextStreamState = streamStateUnknown; - - // Conditionally delete the session - reapSession(session); - } - else - { - // Already reaped this session. - LL_DEBUGS("Voice") << "unknown session " << sessionHandle << " removed" << LL_ENDL; - } - -} - -void LLVivoxVoiceClient::reapSession(const sessionStatePtr_t &session) -{ - if(session) - { - - if(session->mCreateInProgress) - { - LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (create in progress)" << LL_ENDL; - } - else if(session->mMediaConnectInProgress) - { - LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (connect in progress)" << LL_ENDL; - } - else if(session == mAudioSession) - { - LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (it's the current session)" << LL_ENDL; - } - else if(session == mNextAudioSession) - { - LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (it's the next session)" << LL_ENDL; - } - else - { - // We don't have a reason to keep tracking this session, so just delete it. - LL_DEBUGS("Voice") << "deleting session " << session->mSIPURI << LL_ENDL; - deleteSession(session); - } - } - else - { -// LL_DEBUGS("Voice") << "session is NULL" << LL_ENDL; - } -} - -// Returns true if the session seems to indicate we've moved to a region on a different voice server -bool LLVivoxVoiceClient::sessionNeedsRelog(const sessionStatePtr_t &session) -{ - bool result = false; - - if(session) - { - // Only make this check for spatial channels (so it won't happen for group or p2p calls) - if(session->mIsSpatial) - { - std::string::size_type atsign; - - atsign = session->mSIPURI.find("@"); - - if(atsign != std::string::npos) - { - std::string urihost = session->mSIPURI.substr(atsign + 1); - if(stricmp(urihost.c_str(), mVoiceSIPURIHostName.c_str())) - { - // The hostname in this URI is different from what we expect. This probably means we need to relog. - - // We could make a ProvisionVoiceAccountRequest and compare the result with the current values of - // mVoiceSIPURIHostName and mVoiceAccountServerURI to be really sure, but this is a pretty good indicator. - - result = true; - } - } - } - } - - return result; -} - -void LLVivoxVoiceClient::leftAudioSession(const sessionStatePtr_t &session) -{ - if (mAudioSession == session) - { - LLSD vivoxevent(LLSDMap("handle", LLSD::String(session->mHandle)) - ("session", "removed")); - - mVivoxPump.post(vivoxevent); - } -} - -void LLVivoxVoiceClient::accountLoginStateChangeEvent( - std::string &accountHandle, - int statusCode, - std::string &statusString, - int state) -{ - LLSD levent = LLSD::emptyMap(); - - /* - According to Mike S., status codes for this event are: - login_state_logged_out=0, - login_state_logged_in = 1, - login_state_logging_in = 2, - login_state_logging_out = 3, - login_state_resetting = 4, - login_state_error=100 - */ - - LL_DEBUGS("Voice") << "state change event: " << state << LL_ENDL; - switch(state) - { - case 1: - levent["login"] = LLSD::String("account_login"); - - mVivoxPump.post(levent); - break; - case 2: - break; - - case 3: - levent["login"] = LLSD::String("account_loggingOut"); - - mVivoxPump.post(levent); - break; - - case 4: - break; - - case 100: - LL_WARNS("Voice") << "account state event error" << LL_ENDL; - break; - - case 0: - levent["login"] = LLSD::String("account_logout"); - - mVivoxPump.post(levent); - break; - - default: - //Used to be a commented out warning - LL_WARNS("Voice") << "unknown account state event: " << state << LL_ENDL; - break; - } -} - -void LLVivoxVoiceClient::mediaCompletionEvent(std::string &sessionGroupHandle, std::string &mediaCompletionType) -{ - LLSD result; - - if (mediaCompletionType == "AuxBufferAudioCapture") - { - mCaptureBufferRecording = false; - result["recplay"] = "end"; - } - else if (mediaCompletionType == "AuxBufferAudioRender") - { - // Ignore all but the last stop event - if (--mPlayRequestCount <= 0) - { - mCaptureBufferPlaying = false; - result["recplay"] = "end"; -// result["recplay"] = "done"; - } - } - else - { - LL_WARNS("Voice") << "Unknown MediaCompletionType: " << mediaCompletionType << LL_ENDL; - } - - if (!result.isUndefined()) - mVivoxPump.post(result); -} - -void LLVivoxVoiceClient::mediaStreamUpdatedEvent( - std::string &sessionHandle, - std::string &sessionGroupHandle, - int statusCode, - std::string &statusString, - int state, - bool incoming) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - - LL_DEBUGS("Voice") << "session " << sessionHandle << ", status code " << statusCode << ", string \"" << statusString << "\"" << LL_ENDL; - - if(session) - { - // We know about this session - - // Save the state for later use - session->mMediaStreamState = state; - - switch(statusCode) - { - case 0: - case HTTP_OK: - // generic success - // Don't change the saved error code (it may have been set elsewhere) - break; - default: - // save the status code for later - session->mErrorStatusCode = statusCode; - break; - } - - switch(state) - { - case streamStateDisconnecting: - case streamStateIdle: - // Standard "left audio session", Vivox state 'disconnected' - session->mVoiceActive = false; - session->mMediaConnectInProgress = false; - leftAudioSession(session); - break; - - case streamStateConnected: - session->mVoiceActive = true; - session->mMediaConnectInProgress = false; - joinedAudioSession(session); - case streamStateConnecting: // do nothing, but prevents a warning getting into the logs. - break; - - case streamStateRinging: - if(incoming) - { - // Send the voice chat invite to the GUI layer - // TODO: Question: Should we correlate with the mute list here? - session->mIMSessionID = LLIMMgr::computeSessionID(IM_SESSION_P2P_INVITE, session->mCallerID); - session->mVoiceInvitePending = true; - if(session->mName.empty()) - { - lookupName(session->mCallerID); - } - else - { - // Act like we just finished resolving the name - avatarNameResolved(session->mCallerID, session->mName); - } - } - break; - - default: - LL_WARNS("Voice") << "unknown state " << state << LL_ENDL; - break; - - } - - } - else - { - // session disconnectintg and disconnected events arriving after we have already left the session. - LL_DEBUGS("Voice") << "session " << sessionHandle << " not found"<< LL_ENDL; - } -} - -void LLVivoxVoiceClient::participantAddedEvent( - std::string &sessionHandle, - std::string &sessionGroupHandle, - std::string &uriString, - std::string &alias, - std::string &nameString, - std::string &displayNameString, - int participantType) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - if(session) - { - participantStatePtr_t participant(session->addParticipant(uriString)); - if(participant) - { - participant->mAccountName = nameString; - - LL_DEBUGS("Voice") << "added participant \"" << participant->mAccountName - << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; - - if(participant->mAvatarIDValid) - { - // Initiate a lookup - lookupName(participant->mAvatarID); - } - else - { - // If we don't have a valid avatar UUID, we need to fill in the display name to make the active speakers floater work. - std::string namePortion = nameFromsipURI(uriString); - if(namePortion.empty()) - { - // Problem with the SIP URI, fall back to the display name - namePortion = displayNameString; - } - if(namePortion.empty()) - { - // Problems with both of the above, fall back to the account name - namePortion = nameString; - } - - // Set the display name (which is a hint to the active speakers window not to do its own lookup) - participant->mDisplayName = namePortion; - avatarNameResolved(participant->mAvatarID, namePortion); - } - } - } -} - -void LLVivoxVoiceClient::participantRemovedEvent( - std::string &sessionHandle, - std::string &sessionGroupHandle, - std::string &uriString, - std::string &alias, - std::string &nameString) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - if(session) - { - participantStatePtr_t participant(session->findParticipant(uriString)); - if(participant) - { - session->removeParticipant(participant); - } - else - { - LL_DEBUGS("Voice") << "unknown participant " << uriString << LL_ENDL; - } - } - else - { - // a late arriving event on a session we have already left. - LL_DEBUGS("Voice") << "unknown session " << sessionHandle << LL_ENDL; - } -} - - -void LLVivoxVoiceClient::participantUpdatedEvent( - std::string &sessionHandle, - std::string &sessionGroupHandle, - std::string &uriString, - std::string &alias, - bool isModeratorMuted, - bool isSpeaking, - int volume, - F32 energy) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - if(session) - { - participantStatePtr_t participant(session->findParticipant(uriString)); - - if(participant) - { - //LL_INFOS("Voice") << "Participant Update for " << participant->mDisplayName << LL_ENDL; - - participant->mIsSpeaking = isSpeaking; - participant->mIsModeratorMuted = isModeratorMuted; - - // SLIM SDK: convert range: ensure that energy is set to zero if is_speaking is false - if (isSpeaking) - { - participant->mSpeakingTimeout.reset(); - participant->mPower = energy; - } - else - { - participant->mPower = 0.0f; - } - - // Ignore incoming volume level if it has been explicitly set, or there - // is a volume or mute change pending. - if ( !participant->mVolumeSet && !participant->mVolumeDirty) - { - participant->mVolume = (F32)volume * VOLUME_SCALE_VIVOX; - } - - // *HACK: mantipov: added while working on EXT-3544 - /* - Sometimes LLVoiceClient::participantUpdatedEvent callback is called BEFORE - LLViewerChatterBoxSessionAgentListUpdates::post() sometimes AFTER. - - participantUpdatedEvent updates voice participant state in particular participantState::mIsModeratorMuted - Originally we wanted to update session Speaker Manager to fire LLSpeakerVoiceModerationEvent to fix the EXT-3544 bug. - Calling of the LLSpeakerMgr::update() method was added into LLIMMgr::processAgentListUpdates. - - But in case participantUpdatedEvent() is called after LLViewerChatterBoxSessionAgentListUpdates::post() - voice participant mIsModeratorMuted is changed after speakers are updated in Speaker Manager - and event is not fired. - - So, we have to call LLSpeakerMgr::update() here. - */ - LLVoiceChannel* voice_cnl = LLVoiceChannel::getCurrentVoiceChannel(); - - // ignore session ID of local chat - if (voice_cnl && voice_cnl->getSessionID().notNull()) - { - LLSpeakerMgr* speaker_manager = LLIMModel::getInstance()->getSpeakerManager(voice_cnl->getSessionID()); - if (speaker_manager) - { - speaker_manager->update(true); - - // also initialize voice moderate_mode depend on Agent's participant. See EXT-6937. - // *TODO: remove once a way to request the current voice channel moderation mode is implemented. - if (gAgent.getID() == participant->mAvatarID) - { - speaker_manager->initVoiceModerateMode(); - } - } - } - } - else - { - LL_WARNS("Voice") << "unknown participant: " << uriString << LL_ENDL; - } - } - else - { - LL_DEBUGS("Voice") << "unknown session " << sessionHandle << LL_ENDL; - } -} - -void LLVivoxVoiceClient::messageEvent( - std::string &sessionHandle, - std::string &uriString, - std::string &alias, - std::string &messageHeader, - std::string &messageBody, - std::string &applicationString) -{ - LL_DEBUGS("Voice") << "Message event, session " << sessionHandle << " from " << uriString << LL_ENDL; -// LL_DEBUGS("Voice") << " header " << messageHeader << ", body: \n" << messageBody << LL_ENDL; - - LL_INFOS("Voice") << "Vivox raw message:" << std::endl << messageBody << LL_ENDL; - - if(messageHeader.find(HTTP_CONTENT_TEXT_HTML) != std::string::npos) - { - std::string message; - - { - const std::string startMarker = ", try looking for a instead. - start = messageBody.find(startSpan); - start = messageBody.find(startMarker2, start); - end = messageBody.find(endSpan); - - if(start != std::string::npos) - { - start += startMarker2.size(); - - if(end != std::string::npos) - end -= start; - - message.assign(messageBody, start, end); - } - } - } - -// LL_DEBUGS("Voice") << " raw message = \n" << message << LL_ENDL; - - // strip formatting tags - { - std::string::size_type start; - std::string::size_type end; - - while((start = message.find('<')) != std::string::npos) - { - if((end = message.find('>', start + 1)) != std::string::npos) - { - // Strip out the tag - message.erase(start, (end + 1) - start); - } - else - { - // Avoid an infinite loop - break; - } - } - } - - // Decode ampersand-escaped chars - { - std::string::size_type mark = 0; - - // The text may contain text encoded with <, >, and & - mark = 0; - while((mark = message.find("<", mark)) != std::string::npos) - { - message.replace(mark, 4, "<"); - mark += 1; - } - - mark = 0; - while((mark = message.find(">", mark)) != std::string::npos) - { - message.replace(mark, 4, ">"); - mark += 1; - } - - mark = 0; - while((mark = message.find("&", mark)) != std::string::npos) - { - message.replace(mark, 5, "&"); - mark += 1; - } - } - - // strip leading/trailing whitespace (since we always seem to get a couple newlines) - LLStringUtil::trim(message); - -// LL_DEBUGS("Voice") << " stripped message = \n" << message << LL_ENDL; - - sessionStatePtr_t session(findSession(sessionHandle)); - if(session) - { - bool is_do_not_disturb = gAgent.isDoNotDisturb(); - bool is_muted = LLMuteList::getInstance()->isMuted(session->mCallerID, session->mName, LLMute::flagTextChat); - bool is_linden = LLMuteList::isLinden(session->mName); - LLChat chat; - - chat.mMuted = is_muted && !is_linden; - - if(!chat.mMuted) - { - chat.mFromID = session->mCallerID; - chat.mFromName = session->mName; - chat.mSourceType = CHAT_SOURCE_AGENT; - - if(is_do_not_disturb && !is_linden) - { - // TODO: Question: Return do not disturb mode response here? Or maybe when session is started instead? - } - - LL_DEBUGS("Voice") << "adding message, name " << session->mName << " session " << session->mIMSessionID << ", target " << session->mCallerID << LL_ENDL; - LLIMMgr::getInstance()->addMessage(session->mIMSessionID, - session->mCallerID, - session->mName.c_str(), - message.c_str(), - false, - LLStringUtil::null, // default arg - IM_NOTHING_SPECIAL, // default arg - 0, // default arg - LLUUID::null, // default arg - LLVector3::zero); // default arg - } - } - } -} - -void LLVivoxVoiceClient::sessionNotificationEvent(std::string &sessionHandle, std::string &uriString, std::string ¬ificationType) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - - if(session) - { - participantStatePtr_t participant(session->findParticipant(uriString)); - if(participant) - { - if (!stricmp(notificationType.c_str(), "Typing")) - { - // Other end started typing - // TODO: The proper way to add a typing notification seems to be LLIMMgr::processIMTypingStart(). - // It requires some info for the message, which we don't have here. - } - else if (!stricmp(notificationType.c_str(), "NotTyping")) - { - // Other end stopped typing - // TODO: The proper way to remove a typing notification seems to be LLIMMgr::processIMTypingStop(). - // It requires some info for the message, which we don't have here. - } - else - { - LL_DEBUGS("Voice") << "Unknown notification type " << notificationType << "for participant " << uriString << " in session " << session->mSIPURI << LL_ENDL; - } - } - else - { - LL_DEBUGS("Voice") << "Unknown participant " << uriString << " in session " << session->mSIPURI << LL_ENDL; - } - } - else - { - LL_DEBUGS("Voice") << "Unknown session handle " << sessionHandle << LL_ENDL; - } -} - -void LLVivoxVoiceClient::voiceServiceConnectionStateChangedEvent(int statusCode, std::string &statusString, std::string &build_id) -{ - // We don't generally need to process this. However, one occurence is when we first connect, and so it is the - // earliest opportunity to learn what we're connected to. - if (statusCode) - { - LL_WARNS("Voice") << "VoiceServiceConnectionStateChangedEvent statusCode: " << statusCode << - "statusString: " << statusString << LL_ENDL; - return; - } - if (build_id.empty()) - { - return; - } - mVoiceVersion.mBuildVersion = build_id; -} - -void LLVivoxVoiceClient::auxAudioPropertiesEvent(F32 energy) -{ - LL_DEBUGS("VoiceEnergy") << "got energy " << energy << LL_ENDL; - mTuningEnergy = energy; -} - -void LLVivoxVoiceClient::muteListChanged() -{ - // The user's mute list has been updated. Go through the current participant list and sync it with the mute list. - if(mAudioSession) - { - participantMap::iterator iter = mAudioSession->mParticipantsByURI.begin(); - - for(; iter != mAudioSession->mParticipantsByURI.end(); iter++) - { - participantStatePtr_t p(iter->second); - - // Check to see if this participant is on the mute list already - if(p->updateMuteState()) - mAudioSession->mVolumeDirty = true; - } - } -} - -///////////////////////////// -// Managing list of participants -LLVivoxVoiceClient::participantState::participantState(const std::string &uri) : - mURI(uri), - mPTT(false), - mIsSpeaking(false), - mIsModeratorMuted(false), - mLastSpokeTimestamp(0.f), - mPower(0.f), - mVolume(LLVoiceClient::VOLUME_DEFAULT), - mUserVolume(0), - mOnMuteList(false), - mVolumeSet(false), - mVolumeDirty(false), - mAvatarIDValid(false), - mIsSelf(false) -{ -} - -LLVivoxVoiceClient::participantStatePtr_t LLVivoxVoiceClient::sessionState::addParticipant(const std::string &uri) -{ - participantStatePtr_t result; - bool useAlternateURI = false; - - // Note: this is mostly the body of LLVivoxVoiceClient::sessionState::findParticipant(), but since we need to know if it - // matched the alternate SIP URI (so we can add it properly), we need to reproduce it here. - { - participantMap::iterator iter = mParticipantsByURI.find(uri); - - if(iter == mParticipantsByURI.end()) - { - if(!mAlternateSIPURI.empty() && (uri == mAlternateSIPURI)) - { - // This is a p2p session (probably with the SLIM client) with an alternate URI for the other participant. - // Use mSIPURI instead, since it will be properly encoded. - iter = mParticipantsByURI.find(mSIPURI); - useAlternateURI = true; - } - } - - if(iter != mParticipantsByURI.end()) - { - result = iter->second; - } - } - - if(!result) - { - // participant isn't already in one list or the other. - result = std::make_shared(useAlternateURI?mSIPURI:uri); - mParticipantsByURI.insert(participantMap::value_type(result->mURI, result)); - mParticipantsChanged = true; - - // Try to do a reverse transform on the URI to get the GUID back. - { - LLUUID id; - if(LLVivoxVoiceClient::getInstance()->IDFromName(result->mURI, id)) - { - result->mAvatarIDValid = true; - result->mAvatarID = id; - } - else - { - // Create a UUID by hashing the URI, but do NOT set mAvatarIDValid. - // This indicates that the ID will not be in the name cache. - result->mAvatarID.generate(uri); - } - } - - if(result->updateMuteState()) - { - mMuteDirty = true; - } - - mParticipantsByUUID.insert(participantUUIDMap::value_type(result->mAvatarID, result)); - - if (LLSpeakerVolumeStorage::getInstance()->getSpeakerVolume(result->mAvatarID, result->mVolume)) - { - result->mVolumeDirty = true; - mVolumeDirty = true; - } - - LL_DEBUGS("Voice") << "participant \"" << result->mURI << "\" added." << LL_ENDL; - } - - return result; -} - -bool LLVivoxVoiceClient::participantState::updateMuteState() -{ - bool result = false; - - bool isMuted = LLMuteList::getInstance()->isMuted(mAvatarID, LLMute::flagVoiceChat); - if(mOnMuteList != isMuted) - { - mOnMuteList = isMuted; - mVolumeDirty = true; - result = true; - } - return result; -} - -bool LLVivoxVoiceClient::participantState::isAvatar() -{ - return mAvatarIDValid; -} - -void LLVivoxVoiceClient::sessionState::removeParticipant(const LLVivoxVoiceClient::participantStatePtr_t &participant) -{ - if(participant) - { - participantMap::iterator iter = mParticipantsByURI.find(participant->mURI); - participantUUIDMap::iterator iter2 = mParticipantsByUUID.find(participant->mAvatarID); - - LL_DEBUGS("Voice") << "participant \"" << participant->mURI << "\" (" << participant->mAvatarID << ") removed." << LL_ENDL; - - if(iter == mParticipantsByURI.end()) - { - LL_WARNS("Voice") << "Internal error: participant " << participant->mURI << " not in URI map" << LL_ENDL; - } - else if(iter2 == mParticipantsByUUID.end()) - { - LL_WARNS("Voice") << "Internal error: participant ID " << participant->mAvatarID << " not in UUID map" << LL_ENDL; - } - else if(iter->second != iter2->second) - { - LL_WARNS("Voice") << "Internal error: participant mismatch!" << LL_ENDL; - } - else - { - mParticipantsByURI.erase(iter); - mParticipantsByUUID.erase(iter2); - - mParticipantsChanged = true; - } - } -} - -void LLVivoxVoiceClient::sessionState::removeAllParticipants() -{ - LL_DEBUGS("Voice") << "called" << LL_ENDL; - - while(!mParticipantsByURI.empty()) - { - removeParticipant(mParticipantsByURI.begin()->second); - } - - if(!mParticipantsByUUID.empty()) - { - LL_WARNS("Voice") << "Internal error: empty URI map, non-empty UUID map" << LL_ENDL; - } -} - -/*static*/ -void LLVivoxVoiceClient::sessionState::VerifySessions() -{ - std::set::iterator it = mSession.begin(); - while (it != mSession.end()) - { - if ((*it).expired()) - { - LL_WARNS("Voice") << "Expired session found! removing" << LL_ENDL; - it = mSession.erase(it); - } - else - ++it; - } -} - - -void LLVivoxVoiceClient::getParticipantList(std::set &participants) -{ - if(mProcessChannels && mAudioSession) - { - for(participantUUIDMap::iterator iter = mAudioSession->mParticipantsByUUID.begin(); - iter != mAudioSession->mParticipantsByUUID.end(); - iter++) - { - participants.insert(iter->first); - } - } -} - -bool LLVivoxVoiceClient::isParticipant(const LLUUID &speaker_id) -{ - if(mProcessChannels && mAudioSession) - { - return (mAudioSession->mParticipantsByUUID.find(speaker_id) != mAudioSession->mParticipantsByUUID.end()); - } - return false; -} - - -LLVivoxVoiceClient::participantStatePtr_t LLVivoxVoiceClient::sessionState::findParticipant(const std::string &uri) -{ - participantStatePtr_t result; - - participantMap::iterator iter = mParticipantsByURI.find(uri); - - if(iter == mParticipantsByURI.end()) - { - if(!mAlternateSIPURI.empty() && (uri == mAlternateSIPURI)) - { - // This is a p2p session (probably with the SLIM client) with an alternate URI for the other participant. - // Look up the other URI - iter = mParticipantsByURI.find(mSIPURI); - } - } - - if(iter != mParticipantsByURI.end()) - { - result = iter->second; - } - - return result; -} - -LLVivoxVoiceClient::participantStatePtr_t LLVivoxVoiceClient::sessionState::findParticipantByID(const LLUUID& id) -{ - participantStatePtr_t result; - participantUUIDMap::iterator iter = mParticipantsByUUID.find(id); - - if(iter != mParticipantsByUUID.end()) - { - result = iter->second; - } - - return result; -} - -LLVivoxVoiceClient::participantStatePtr_t LLVivoxVoiceClient::findParticipantByID(const LLUUID& id) -{ - participantStatePtr_t result; - - if(mAudioSession) - { - result = mAudioSession->findParticipantByID(id); - } - - return result; -} - - - -// Check for parcel boundary crossing -bool LLVivoxVoiceClient::checkParcelChanged(bool update) -{ - LLViewerRegion *region = gAgent.getRegion(); - LLParcel *parcel = LLViewerParcelMgr::getInstance()->getAgentParcel(); - - if(region && parcel) - { - S32 parcelLocalID = parcel->getLocalID(); - std::string regionName = region->getName(); - - // LL_DEBUGS("Voice") << "Region name = \"" << regionName << "\", parcel local ID = " << parcelLocalID << ", cap URI = \"" << capURI << "\"" << LL_ENDL; - - // The region name starts out empty and gets filled in later. - // Also, the cap gets filled in a short time after the region cross, but a little too late for our purposes. - // If either is empty, wait for the next time around. - if(!regionName.empty()) - { - if((parcelLocalID != mCurrentParcelLocalID) || (regionName != mCurrentRegionName)) - { - // We have changed parcels. Initiate a parcel channel lookup. - if (update) - { - mCurrentParcelLocalID = parcelLocalID; - mCurrentRegionName = regionName; - } - return true; - } - } - } - return false; -} - -bool LLVivoxVoiceClient::switchChannel( - std::string uri, - bool spatial, - bool no_reconnect, - bool is_p2p, - std::string hash) -{ - bool needsSwitch = !mIsInChannel; - - if (mIsInChannel) - { - if (mSessionTerminateRequested) - { - // If a terminate has been requested, we need to compare against where the URI we're already headed to. - if(mNextAudioSession) - { - if (mNextAudioSession->mSIPURI != uri) - { - needsSwitch = true; - } - } - else - { - // mNextAudioSession is null -- this probably means we're on our way back to spatial. - if(!uri.empty()) - { - // We do want to process a switch in this case. - needsSwitch = true; - } - } - } - else - { - // Otherwise, compare against the URI we're in now. - if(mAudioSession) - { - if(mAudioSession->mSIPURI != uri) - { - needsSwitch = true; - } - } - else - { - if(!uri.empty()) - { - // mAudioSession is null -- it's not clear what case would cause this. - // For now, log it as a warning and see if it ever crops up. - LL_WARNS("Voice") << "No current audio session... Forcing switch" << LL_ENDL; - needsSwitch = true; - } - } - } - } - - if(needsSwitch) - { - if(uri.empty()) - { - // Leave any channel we may be in - LL_DEBUGS("Voice") << "leaving channel" << LL_ENDL; - - sessionStatePtr_t oldSession = mNextAudioSession; - mNextAudioSession.reset(); - - // The old session may now need to be deleted. - reapSession(oldSession); - - // If voice was on, turn it off - if (LLVoiceClient::getInstance()->getUserPTTState()) - { - LLVoiceClient::getInstance()->setUserPTTState(false); - } - - notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED); - } - else - { - LL_DEBUGS("Voice") << "switching to channel " << uri << LL_ENDL; - - mNextAudioSession = addSession(uri); - mNextAudioSession->mHash = hash; - mNextAudioSession->mIsSpatial = spatial; - mNextAudioSession->mReconnect = !no_reconnect; - mNextAudioSession->mIsP2P = is_p2p; - } - - if (mIsInChannel) - { - // If we're already in a channel, or if we're joining one, terminate - // so we can rejoin with the new session data. - sessionTerminate(); - } - } - - return needsSwitch; -} - -void LLVivoxVoiceClient::joinSession(const sessionStatePtr_t &session) -{ - mNextAudioSession = session; - - if (mIsInChannel) - { - // If we're already in a channel, or if we're joining one, terminate - // so we can rejoin with the new session data. - sessionTerminate(); - } -} - -void LLVivoxVoiceClient::setNonSpatialChannel(const LLSD& channelInfo, bool notify_on_first_join, bool hangup_on_last_leave) -{ - switchChannel(channelInfo["channel_uri"].asString(), false, false, false, channelInfo["channel_credentials"].asString()); -} - -bool LLVivoxVoiceClient::setSpatialChannel(const LLSD& channelInfo) -{ - mSpatialSessionURI = channelInfo["channel_uri"].asString(); - mSpatialSessionCredentials = channelInfo["channel_credentials"].asString(); - if (!mProcessChannels) - { - // we're not even processing channels (another provider is) so - // save the credentials aside and exit - return false; - } - - LL_DEBUGS("Voice") << "got spatial channel uri: \"" << mSpatialSessionURI << "\"" << LL_ENDL; - - if((mIsInChannel && mAudioSession && !(mAudioSession->mIsSpatial)) || (mNextAudioSession && !(mNextAudioSession->mIsSpatial))) - { - // User is in a non-spatial chat or joining a non-spatial chat. Don't switch channels. - LL_INFOS("Voice") << "in non-spatial chat, not switching channels" << LL_ENDL; - return false; - } - else - { - return switchChannel(mSpatialSessionURI, true, false, false, mSpatialSessionCredentials); - } -} - -void LLVivoxVoiceClient::callUser(const LLUUID &uuid) -{ - std::string userURI = sipURIFromID(uuid); - mProcessChannels = true; - - switchChannel(userURI, false, true, true); -} - -void LLVivoxVoiceClient::hangup() { leaveChannel(); } - - -LLVoiceP2PIncomingCallInterfacePtr LLVivoxVoiceClient::getIncomingCallInterface(const LLSD &voice_call_info) -{ - return std::make_shared(voice_call_info); -} - -bool LLVivoxVoiceClient::answerInvite(const std::string &sessionHandle) -{ - // this is only ever used to answer incoming p2p call invites. - - sessionStatePtr_t session(findSession(sessionHandle)); - if (session) - { - session->mIsSpatial = false; - session->mReconnect = false; - session->mIsP2P = true; - mProcessChannels = true; - joinSession(session); - return true; - } - - return false; -} - -void LLVivoxVoiceClient::declineInvite(const std::string &sessionHandle) -{ - sessionStatePtr_t session(findSession(sessionHandle)); - if (session) - { - sessionMediaDisconnectSendMessage(session); - } -} - -bool LLVivoxVoiceClient::isVoiceWorking() const -{ - - //Added stateSessionTerminated state to avoid problems with call in parcels with disabled voice (EXT-4758) - // Condition with joining spatial num was added to take into account possible problems with connection to voice - // server(EXT-4313). See bug descriptions and comments for MAX_NORMAL_JOINING_SPATIAL_NUM for more info. - return (mSpatialJoiningNum < MAX_NORMAL_JOINING_SPATIAL_NUM) && mIsLoggedIn; -} - -// Returns true if the indicated participant in the current audio session is really an SL avatar. -// Currently this will be false only for PSTN callers into group chats, and PSTN p2p calls. -bool LLVivoxVoiceClient::isParticipantAvatar(const LLUUID &id) -{ - bool result = true; - sessionStatePtr_t session(findSession(id)); - - if(session) - { - // this is a p2p session with the indicated caller, or the session with the specified UUID. - if(session->mSynthesizedCallerID) - result = false; - } - else - { - // Didn't find a matching session -- check the current audio session for a matching participant - if(mAudioSession) - { - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - result = participant->isAvatar(); - } - } - } - - return result; -} - -// Returns true if calling back the session URI after the session has closed is possible. -// Currently this will be false only for PSTN P2P calls. -bool LLVivoxVoiceClient::isSessionCallBackPossible(const LLUUID &session_id) -{ - bool result = true; - sessionStatePtr_t session(findSession(session_id)); - - if(session != NULL) - { - result = session->isCallBackPossible(); - } - - return result; -} - -// Returns true if the session can accept text IM's. -// Currently this will be false only for PSTN P2P calls. -bool LLVivoxVoiceClient::isSessionTextIMPossible(const LLUUID &session_id) -{ - bool result = true; - sessionStatePtr_t session(findSession(session_id)); - - if(session != NULL) - { - result = session->isTextIMPossible(); - } - - return result; -} - -void LLVivoxVoiceClient::leaveNonSpatialChannel() -{ - LL_DEBUGS("Voice") << "Request to leave spacial channel." << LL_ENDL; - - // Make sure we don't rejoin the current session. - sessionStatePtr_t oldNextSession(mNextAudioSession); - mNextAudioSession.reset(); - - // Most likely this will still be the current session at this point, but check it anyway. - reapSession(oldNextSession); - - verifySessionState(); - - sessionTerminate(); -} - -void LLVivoxVoiceClient::processChannels(bool process) -{ - mCurrentParcelLocalID = -1; - mCurrentRegionName.clear(); - mProcessChannels = process; -} - -bool LLVivoxVoiceClient::isCurrentChannel(const LLSD &channelInfo) -{ - if (!mProcessChannels - || (channelInfo.has("voice_server_type") && channelInfo["voice_server_type"].asString() != VIVOX_VOICE_SERVER_TYPE) - || mSessionTerminateRequested) - { - return false; - } - // favor the next audio session, as that's the one we're bringing up. - sessionStatePtr_t session = mNextAudioSession; - if (!session) - { - session = mAudioSession; - } - if (session) - { - if (!channelInfo["session_handle"].asString().empty()) - { - return session->mHandle == channelInfo["session_handle"].asString(); - } - return channelInfo["channel_uri"].asString() == session->mSIPURI; - } - return false; -} - -bool LLVivoxVoiceClient::compareChannels(const LLSD& channelInfo1, const LLSD& channelInfo2) -{ - return (!channelInfo1.has("voice_server_type") || (channelInfo1["voice_server_type"] == VIVOX_VOICE_SERVER_TYPE)) && - (!channelInfo2.has("voice_server_type") || (channelInfo2["voice_server_type"] == VIVOX_VOICE_SERVER_TYPE)) && - (channelInfo1["channel_uri"] == channelInfo2["channel_uri"]); -} - -bool LLVivoxVoiceClient::inProximalChannel() -{ - bool result = false; - - if (mIsInChannel && !mSessionTerminateRequested) - { - result = inSpatialChannel(); - } - - return result; -} - -std::string LLVivoxVoiceClient::sipURIFromID(const LLUUID &id) const -{ - std::string result; - result = "sip:"; - result += nameFromID(id); - result += "@"; - result += mVoiceSIPURIHostName; - - return result; -} - -LLSD LLVivoxVoiceClient::getP2PChannelInfoTemplate(const LLUUID& id) const -{ - LLSD result; - result["channel_uri"] = sipURIFromID(id); - result["voice_server_type"] = VIVOX_VOICE_SERVER_TYPE; - return result; -} - -std::string LLVivoxVoiceClient::sipURIFromAvatar(LLVOAvatar *avatar) -{ - std::string result; - if(avatar) - { - result = "sip:"; - result += nameFromID(avatar->getID()); - result += "@"; - result += mVoiceSIPURIHostName; - } - - return result; -} - -std::string LLVivoxVoiceClient::nameFromID(const LLUUID &uuid) const -{ - std::string result; - - if (uuid.isNull()) { - //VIVOX, the uuid emtpy look for the mURIString and return that instead. - //result.assign(uuid.mURIStringName); - LLStringUtil::replaceChar(result, '_', ' '); - return result; - } - // Prepending this apparently prevents conflicts with reserved names inside the vivox code. - result = "x"; - - // Base64 encode and replace the pieces of base64 that are less compatible - // with e-mail local-parts. - // See RFC-4648 "Base 64 Encoding with URL and Filename Safe Alphabet" - result += LLBase64::encode(uuid.mData, UUID_BYTES); - LLStringUtil::replaceChar(result, '+', '-'); - LLStringUtil::replaceChar(result, '/', '_'); - - // If you need to transform a GUID to this form on the macOS command line, this will do so: - // echo -n x && (echo e669132a-6c43-4ee1-a78d-6c82fff59f32 |xxd -r -p |openssl base64|tr '/+' '_-') - - // The reverse transform can be done with: - // echo 'x5mkTKmxDTuGnjWyC__WfMg==' |cut -b 2- -|tr '_-' '/+' |openssl base64 -d|xxd -p - - return result; -} - -bool LLVivoxVoiceClient::IDFromName(const std::string inName, LLUUID &uuid) -{ - bool result = false; - - // SLIM SDK: The "name" may actually be a SIP URI such as: "sip:xFnPP04IpREWNkuw1cOXlhw==@bhr.vivox.com" - // If it is, convert to a bare name before doing the transform. - std::string name = nameFromsipURI(inName); - - // Doesn't look like a SIP URI, assume it's an actual name. - if(name.empty()) - name = inName; - - // This will only work if the name is of the proper form. - // As an example, the account name for Monroe Linden (UUID 1673cfd3-8229-4445-8d92-ec3570e5e587) is: - // "xFnPP04IpREWNkuw1cOXlhw==" - - if((name.size() == 25) && (name[0] == 'x') && (name[23] == '=') && (name[24] == '=')) - { - // The name appears to have the right form. - - // Reverse the transforms done by nameFromID - std::string temp = name; - LLStringUtil::replaceChar(temp, '-', '+'); - LLStringUtil::replaceChar(temp, '_', '/'); - - U8 rawuuid[UUID_BYTES + 1]; - int len = apr_base64_decode_binary(rawuuid, temp.c_str() + 1); - if(len == UUID_BYTES) - { - // The decode succeeded. Stuff the bits into the result's UUID - memcpy(uuid.mData, rawuuid, UUID_BYTES); - result = true; - } - } - - if(!result) - { - // VIVOX: not a standard account name, just copy the URI name mURIString field - // and hope for the best. bpj - uuid.setNull(); // VIVOX, set the uuid field to nulls - } - - return result; -} - -std::string LLVivoxVoiceClient::sipURIFromName(std::string &name) -{ - std::string result; - result = "sip:"; - result += name; - result += "@"; - result += mVoiceSIPURIHostName; - -// LLStringUtil::toLower(result); - - return result; -} - -std::string LLVivoxVoiceClient::nameFromsipURI(const std::string &uri) -{ - std::string result; - - std::string::size_type sipOffset, atOffset; - sipOffset = uri.find("sip:"); - atOffset = uri.find("@"); - if((sipOffset != std::string::npos) && (atOffset != std::string::npos)) - { - result = uri.substr(sipOffset + 4, atOffset - (sipOffset + 4)); - } - - return result; -} - -bool LLVivoxVoiceClient::inSpatialChannel(void) -{ - bool result = false; - - if(mAudioSession) - { - result = mAudioSession->mIsSpatial; - } - - return result; -} - - -LLSD LLVivoxVoiceClient::getAudioSessionChannelInfo() -{ - LLSD result; - - if (mAudioSession) - { - result = mAudioSession->getVoiceChannelInfo(); - } - - return result; -} - -std::string LLVivoxVoiceClient::getAudioSessionHandle() -{ - std::string result; - - if(mAudioSession) - result = mAudioSession->mHandle; - - return result; -} - - -///////////////////////////// -// Sending updates of current state - -void LLVivoxVoiceClient::enforceTether(void) -{ - LLVector3d tethered = mCameraRequestedPosition; - - // constrain 'tethered' to within 50m of mAvatarPosition. - { - F32 max_dist = 50.0f; - LLVector3d camera_offset = mCameraRequestedPosition - mAvatarPosition; - F32 camera_distance = (F32)camera_offset.magVec(); - if(camera_distance > max_dist) - { - tethered = mAvatarPosition + - (max_dist / camera_distance) * camera_offset; - } - } - - if(dist_vec_squared(mCameraPosition, tethered) > 0.01) - { - mCameraPosition = tethered; - mSpatialCoordsDirty = true; - } -} - -void LLVivoxVoiceClient::updatePosition(void) -{ - - LLViewerRegion *region = gAgent.getRegion(); - if(region && isAgentAvatarValid()) - { - LLMatrix3 rot; - LLVector3d pos; - LLQuaternion qrot; - - // TODO: If camera and avatar velocity are actually used by the voice system, we could compute them here... - // They're currently always set to zero. - - // Send the current camera position to the voice code - rot.setRows(LLViewerCamera::getInstance()->getAtAxis(), LLViewerCamera::getInstance()->getLeftAxis (), LLViewerCamera::getInstance()->getUpAxis()); - pos = gAgent.getRegion()->getPosGlobalFromRegion(LLViewerCamera::getInstance()->getOrigin()); - - LLVivoxVoiceClient::getInstance()->setCameraPosition( - pos, // position - LLVector3::zero, // velocity - rot); // rotation matrix - - // Send the current avatar position to the voice code - qrot = gAgentAvatarp->getRootJoint()->getWorldRotation(); - pos = gAgentAvatarp->getPositionGlobal(); - - // TODO: Can we get the head offset from outside the LLVOAvatar? - // pos += LLVector3d(mHeadOffset); - pos += LLVector3d(0.f, 0.f, 1.f); - - LLVivoxVoiceClient::getInstance()->setAvatarPosition( - pos, // position - LLVector3::zero, // velocity - qrot); // rotation matrix - } -} - -void LLVivoxVoiceClient::setCameraPosition(const LLVector3d &position, const LLVector3 &velocity, const LLMatrix3 &rot) -{ - mCameraRequestedPosition = position; - - if(mCameraVelocity != velocity) - { - mCameraVelocity = velocity; - mSpatialCoordsDirty = true; - } - - if(mCameraRot != rot) - { - mCameraRot = rot; - mSpatialCoordsDirty = true; - } -} - -void LLVivoxVoiceClient::setAvatarPosition(const LLVector3d &position, const LLVector3 &velocity, const LLQuaternion &rot) -{ - if(dist_vec_squared(mAvatarPosition, position) > 0.01) - { - mAvatarPosition = position; - mSpatialCoordsDirty = true; - } - - if(mAvatarVelocity != velocity) - { - mAvatarVelocity = velocity; - mSpatialCoordsDirty = true; - } - - // If the two rotations are not exactly equal test their dot product - // to get the cos of the angle between them. - // If it is too small, don't update. - F32 rot_cos_diff = llabs(dot(mAvatarRot, rot)); - if ((mAvatarRot != rot) && (rot_cos_diff < MINUSCULE_ANGLE_COS)) - { - mAvatarRot = rot; - mSpatialCoordsDirty = true; - } -} - -bool LLVivoxVoiceClient::channelFromRegion(LLViewerRegion *region, std::string &name) -{ - bool result = false; - - if(region) - { - name = region->getName(); - } - - if(!name.empty()) - result = true; - - return result; -} - -void LLVivoxVoiceClient::leaveChannel(void) -{ - if (mIsInChannel) - { - LL_DEBUGS("Voice") << "leaving channel for teleport/logout" << LL_ENDL; - mChannelName.clear(); - } - sessionTerminate(); -} - -void LLVivoxVoiceClient::setMuteMic(bool muted) -{ - if(mMuteMic != muted) - { - mMuteMic = muted; - mMuteMicDirty = true; - } -} - -void LLVivoxVoiceClient::setVoiceEnabled(bool enabled) -{ - LL_DEBUGS("Voice") - << "( " << (enabled ? "enabled" : "disabled") << " )" - << " was "<< (mVoiceEnabled ? "enabled" : "disabled") - << " coro "<< (mIsCoroutineActive ? "active" : "inactive") - << LL_ENDL; - - if (enabled != mVoiceEnabled) - { - // TODO: Refactor this so we don't call into LLVoiceChannel, but simply - // use the status observer - mVoiceEnabled = enabled; - LLVoiceClientStatusObserver::EStatusType status; - - if (enabled) - { - LL_DEBUGS("Voice") << "enabling" << LL_ENDL; - LLVoiceChannel::getCurrentVoiceChannel()->activate(); - status = LLVoiceClientStatusObserver::STATUS_VOICE_ENABLED; - - if (!mIsCoroutineActive) - { - LLCoros::instance().launch("LLVivoxVoiceClient::voiceControlCoro", - boost::bind(&LLVivoxVoiceClient::voiceControlCoro, LLVivoxVoiceClient::getInstance())); - } - else - { - LL_DEBUGS("Voice") << "coro should be active.. not launching" << LL_ENDL; - } - } - else - { - // Turning voice off looses your current channel -- this makes sure the UI isn't out of sync when you re-enable it. - LLVoiceChannel::getCurrentVoiceChannel()->deactivate(); - gAgent.setVoiceConnected(false); - status = LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED; - mCurrentParcelLocalID = -1; - mCurrentRegionName.clear(); - } - - notifyStatusObservers(status); - } - else - { - LL_DEBUGS("Voice") << " no-op" << LL_ENDL; - } -} - - -void LLVivoxVoiceClient::setEarLocation(S32 loc) -{ - if(mEarLocation != loc) - { - LL_DEBUGS("Voice") << "Setting mEarLocation to " << loc << LL_ENDL; - - mEarLocation = loc; - mSpatialCoordsDirty = true; - } -} - -void LLVivoxVoiceClient::setVoiceVolume(F32 volume) -{ - int scaled_volume = scale_speaker_volume(volume); - - if(scaled_volume != mSpeakerVolume) - { - int min_volume = scale_speaker_volume(0); - if((scaled_volume == min_volume) || (mSpeakerVolume == min_volume)) - { - mSpeakerMuteDirty = true; - } - - mSpeakerVolume = scaled_volume; - mSpeakerVolumeDirty = true; - } -} - -void LLVivoxVoiceClient::setMicGain(F32 volume) -{ - int scaled_volume = scale_mic_volume(volume); - - if(scaled_volume != mMicVolume) - { - mMicVolume = scaled_volume; - mMicVolumeDirty = true; - } -} - -///////////////////////////// -// Accessors for data related to nearby speakers - -std::string LLVivoxVoiceClient::getDisplayName(const LLUUID& id) -{ - std::string result; - if (mProcessChannels) - { - participantStatePtr_t participant(findParticipantByID(id)); - if (participant) - { - result = participant->mDisplayName; - } - } - - return result; -} - - - -bool LLVivoxVoiceClient::getIsSpeaking(const LLUUID& id) -{ - bool result = false; - if (mProcessChannels) - { - participantStatePtr_t participant(findParticipantByID(id)); - if (participant) - { - if (participant->mSpeakingTimeout.getElapsedTimeF32() > SPEAKING_TIMEOUT) - { - participant->mIsSpeaking = false; - } - result = participant->mIsSpeaking; - } - } - - return result; -} - -bool LLVivoxVoiceClient::getIsModeratorMuted(const LLUUID& id) -{ - bool result = false; - if (!mProcessChannels) - { - return false; - } - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - result = participant->mIsModeratorMuted; - } - - return result; -} - -F32 LLVivoxVoiceClient::getCurrentPower(const LLUUID& id) -{ - F32 result = 0; - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - result = participant->mPower; - } - - return result; -} - - - -bool LLVivoxVoiceClient::getUsingPTT(const LLUUID& id) -{ - bool result = false; - - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - // I'm not sure what the semantics of this should be. - // Does "using PTT" mean they're configured with a push-to-talk button? - // For now, we know there's no PTT mechanism in place, so nobody is using it. - } - - return result; -} - -// External accessors. -F32 LLVivoxVoiceClient::getUserVolume(const LLUUID& id) -{ - // Minimum volume will be returned for users with voice disabled - F32 result = LLVoiceClient::VOLUME_MIN; - - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - result = participant->mVolume; - - // Enable this when debugging voice slider issues. It's way to spammy even for debug-level logging. - // LL_DEBUGS("Voice") << "mVolume = " << result << " for " << id << LL_ENDL; - } - - return result; -} - -void LLVivoxVoiceClient::setUserVolume(const LLUUID& id, F32 volume) -{ - if(mAudioSession) - { - participantStatePtr_t participant(findParticipantByID(id)); - if (participant && !participant->mIsSelf) - { - if (!is_approx_equal(volume, LLVoiceClient::VOLUME_DEFAULT)) - { - // Store this volume setting for future sessions if it has been - // changed from the default - LLSpeakerVolumeStorage::getInstance()->storeSpeakerVolume(id, volume); - } - else - { - // Remove stored volume setting if it is returned to the default - LLSpeakerVolumeStorage::getInstance()->removeSpeakerVolume(id); - } - - participant->mVolume = llclamp(volume, LLVoiceClient::VOLUME_MIN, LLVoiceClient::VOLUME_MAX); - participant->mVolumeDirty = true; - mAudioSession->mVolumeDirty = true; - } - } -} - -std::string LLVivoxVoiceClient::getGroupID(const LLUUID& id) -{ - std::string result; - - participantStatePtr_t participant(findParticipantByID(id)); - if(participant) - { - result = participant->mGroupID; - } - - return result; -} - -void LLVivoxVoiceClient::recordingLoopStart(int seconds, int deltaFramesPerControlFrame) -{ -// LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Start)" << LL_ENDL; - - if(!mMainSessionGroupHandle.empty()) - { - std::ostringstream stream; - stream - << "" - << "" << mMainSessionGroupHandle << "" - << "Start" - << "" << deltaFramesPerControlFrame << "" - << "" << "" << "" - << "false" - << "" << seconds << "" - << "\n\n\n"; - - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::recordingLoopSave(const std::string& filename) -{ -// LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Flush)" << LL_ENDL; - - if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) - { - std::ostringstream stream; - stream - << "" - << "" << mMainSessionGroupHandle << "" - << "Flush" - << "" << filename << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::recordingStop() -{ -// LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Stop)" << LL_ENDL; - - if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) - { - std::ostringstream stream; - stream - << "" - << "" << mMainSessionGroupHandle << "" - << "Stop" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::filePlaybackStart(const std::string& filename) -{ -// LL_DEBUGS("Voice") << "sending SessionGroup.ControlPlayback (Start)" << LL_ENDL; - - if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) - { - std::ostringstream stream; - stream - << "" - << "" << mMainSessionGroupHandle << "" - << "Start" - << "" << filename << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::filePlaybackStop() -{ -// LL_DEBUGS("Voice") << "sending SessionGroup.ControlPlayback (Stop)" << LL_ENDL; - - if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) - { - std::ostringstream stream; - stream - << "" - << "" << mMainSessionGroupHandle << "" - << "Stop" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::filePlaybackSetPaused(bool paused) -{ - // TODO: Implement once Vivox gives me a sample -} - -void LLVivoxVoiceClient::filePlaybackSetMode(bool vox, float speed) -{ - // TODO: Implement once Vivox gives me a sample -} - -//------------------------------------------------------------------------ -std::set> LLVivoxVoiceClient::sessionState::mSession; - - -LLVivoxVoiceClient::sessionState::sessionState() : - mErrorStatusCode(0), - mMediaStreamState(streamStateUnknown), - mCreateInProgress(false), - mMediaConnectInProgress(false), - mVoiceInvitePending(false), - mTextInvitePending(false), - mSynthesizedCallerID(false), - mIsChannel(false), - mIsSpatial(false), - mIsP2P(false), - mIncoming(false), - mVoiceActive(false), - mReconnect(false), - mVolumeDirty(false), - mMuteDirty(false), - mParticipantsChanged(false) -{ -} - -LLSD LLVivoxVoiceClient::sessionState::getVoiceChannelInfo() -{ - LLSD result; - - result["voice_server_type"] = VIVOX_VOICE_SERVER_TYPE; - result["channel_credentials"] = mHash; - result["channel_uri"] = mSIPURI; - result["session_handle"] = mHandle; - - return result; -} - -/*static*/ -LLVivoxVoiceClient::sessionState::ptr_t LLVivoxVoiceClient::sessionState::createSession() -{ - sessionState::ptr_t ptr(new sessionState()); - - std::pair::iterator, bool> result = mSession.insert(ptr); - - if (result.second) - ptr->mMyIterator = result.first; - - return ptr; -} - -LLVivoxVoiceClient::sessionState::~sessionState() -{ - LL_INFOS("Voice") << "Destroying session handle=" << mHandle << " SIP=" << mSIPURI << LL_ENDL; - if (mMyIterator != mSession.end()) - mSession.erase(mMyIterator); - - removeAllParticipants(); -} - -bool LLVivoxVoiceClient::sessionState::isCallBackPossible() -{ - // This may change to be explicitly specified by vivox in the future... - // Currently, only PSTN P2P calls cannot be returned. - // Conveniently, this is also the only case where we synthesize a caller UUID. - return !mSynthesizedCallerID; -} - -bool LLVivoxVoiceClient::sessionState::isTextIMPossible() -{ - // This may change to be explicitly specified by vivox in the future... - return !mSynthesizedCallerID; -} - - -/*static*/ -LLVivoxVoiceClient::sessionState::ptr_t LLVivoxVoiceClient::sessionState::matchSessionByHandle(const std::string &handle) -{ - sessionStatePtr_t result; - - // *TODO: My kingdom for a lambda! - std::set::iterator it = std::find_if(mSession.begin(), mSession.end(), boost::bind(testByHandle, _1, handle)); - - if (it != mSession.end()) - result = (*it).lock(); - - return result; -} - -/*static*/ -LLVivoxVoiceClient::sessionState::ptr_t LLVivoxVoiceClient::sessionState::matchCreatingSessionByURI(const std::string &uri) -{ - sessionStatePtr_t result; - - // *TODO: My kingdom for a lambda! - std::set::iterator it = std::find_if(mSession.begin(), mSession.end(), boost::bind(testByCreatingURI, _1, uri)); - - if (it != mSession.end()) - result = (*it).lock(); - - return result; -} - -/*static*/ -LLVivoxVoiceClient::sessionState::ptr_t LLVivoxVoiceClient::sessionState::matchSessionByURI(const std::string &uri) -{ - sessionStatePtr_t result; - - // *TODO: My kingdom for a lambda! - std::set::iterator it = std::find_if(mSession.begin(), mSession.end(), boost::bind(testBySIPOrAlterateURI, _1, uri)); - - if (it != mSession.end()) - result = (*it).lock(); - - return result; -} - -/*static*/ -LLVivoxVoiceClient::sessionState::ptr_t LLVivoxVoiceClient::sessionState::matchSessionByParticipant(const LLUUID &participant_id) -{ - sessionStatePtr_t result; - - // *TODO: My kingdom for a lambda! - std::set::iterator it = std::find_if(mSession.begin(), mSession.end(), boost::bind(testByCallerId, _1, participant_id)); - - if (it != mSession.end()) - result = (*it).lock(); - - return result; -} - -void LLVivoxVoiceClient::sessionState::for_each(sessionFunc_t func) -{ - std::for_each(mSession.begin(), mSession.end(), boost::bind(for_eachPredicate, _1, func)); -} - -// simple test predicates. -// *TODO: These should be made into lambdas when we can pull the trigger on newer C++ features. -bool LLVivoxVoiceClient::sessionState::testByHandle(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string handle) -{ - ptr_t aLock(a.lock()); - - return aLock ? aLock->mHandle == handle : false; -} - -bool LLVivoxVoiceClient::sessionState::testByCreatingURI(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string uri) -{ - ptr_t aLock(a.lock()); - - return aLock ? (aLock->mCreateInProgress && (aLock->mSIPURI == uri)) : false; -} - -bool LLVivoxVoiceClient::sessionState::testBySIPOrAlterateURI(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string uri) -{ - ptr_t aLock(a.lock()); - - return aLock ? ((aLock->mSIPURI == uri) || (aLock->mAlternateSIPURI == uri)) : false; -} - - -bool LLVivoxVoiceClient::sessionState::testByCallerId(const LLVivoxVoiceClient::sessionState::wptr_t &a, LLUUID participantId) -{ - ptr_t aLock(a.lock()); - - return aLock ? ((aLock->mCallerID == participantId) || (aLock->mIMSessionID == participantId)) : false; -} - -/*static*/ -void LLVivoxVoiceClient::sessionState::for_eachPredicate(const LLVivoxVoiceClient::sessionState::wptr_t &a, sessionFunc_t func) -{ - ptr_t aLock(a.lock()); - - if (aLock) - func(aLock); - else - { - LL_WARNS("Voice") << "Stale handle in session map!" << LL_ENDL; - } -} - - - -LLVivoxVoiceClient::sessionStatePtr_t LLVivoxVoiceClient::findSession(const std::string &handle) -{ - sessionStatePtr_t result; - sessionMap::iterator iter = mSessionsByHandle.find(handle); - if(iter != mSessionsByHandle.end()) - { - result = iter->second; - } - - return result; -} - -LLVivoxVoiceClient::sessionStatePtr_t LLVivoxVoiceClient::findSessionBeingCreatedByURI(const std::string &uri) -{ - sessionStatePtr_t result = sessionState::matchCreatingSessionByURI(uri); - - return result; -} - -LLVivoxVoiceClient::sessionStatePtr_t LLVivoxVoiceClient::findSession(const LLUUID &participant_id) -{ - sessionStatePtr_t result = sessionState::matchSessionByParticipant(participant_id); - - return result; -} - -LLVivoxVoiceClient::sessionStatePtr_t LLVivoxVoiceClient::addSession(const std::string &uri, const std::string &handle) -{ - sessionStatePtr_t result; - - if(handle.empty()) - { - // No handle supplied. - // Check whether there's already a session with this URI - result = sessionState::matchSessionByURI(uri); - } - else // (!handle.empty()) - { - // Check for an existing session with this handle - sessionMap::iterator iter = mSessionsByHandle.find(handle); - - if(iter != mSessionsByHandle.end()) - { - result = iter->second; - } - } - - if(!result) - { - // No existing session found. - - LL_DEBUGS("Voice") << "adding new session: handle \"" << handle << "\" URI " << uri << LL_ENDL; - result = sessionState::createSession(); - result->mSIPURI = uri; - result->mHandle = handle; - - if (LLVoiceClient::instance().getVoiceEffectEnabled()) - { - result->mVoiceFontID = LLVoiceClient::instance().getVoiceEffectDefault(); - } - - if(!result->mHandle.empty()) - { - // *TODO: Rider: This concerns me. There is a path (via switchChannel) where - // we do not track the session. In theory this means that we could end up with - // a mAuidoSession that does not match the session tracked in mSessionsByHandle - mSessionsByHandle.insert(sessionMap::value_type(result->mHandle, result)); - } - } - else - { - // Found an existing session - - if(uri != result->mSIPURI) - { - // TODO: Should this be an internal error? - LL_DEBUGS("Voice") << "changing uri from " << result->mSIPURI << " to " << uri << LL_ENDL; - setSessionURI(result, uri); - } - - if(handle != result->mHandle) - { - if(handle.empty()) - { - // There's at least one race condition where where addSession was clearing an existing session handle, which caused things to break. - LL_DEBUGS("Voice") << "NOT clearing handle " << result->mHandle << LL_ENDL; - } - else - { - // TODO: Should this be an internal error? - LL_DEBUGS("Voice") << "changing handle from " << result->mHandle << " to " << handle << LL_ENDL; - setSessionHandle(result, handle); - } - } - - LL_DEBUGS("Voice") << "returning existing session: handle " << handle << " URI " << uri << LL_ENDL; - } - - verifySessionState(); - - return result; -} - -void LLVivoxVoiceClient::clearSessionHandle(const sessionStatePtr_t &session) -{ - if (session) - { - if (!session->mHandle.empty()) - { - sessionMap::iterator iter = mSessionsByHandle.find(session->mHandle); - if (iter != mSessionsByHandle.end()) - { - mSessionsByHandle.erase(iter); - } - } - else - { - LL_WARNS("Voice") << "Session has empty handle!" << LL_ENDL; - } - } - else - { - LL_WARNS("Voice") << "Attempt to clear NULL session!" << LL_ENDL; - } - -} - -void LLVivoxVoiceClient::setSessionHandle(const sessionStatePtr_t &session, const std::string &handle) -{ - // Have to remove the session from the handle-indexed map before changing the handle, or things will break badly. - - if(!session->mHandle.empty()) - { - // Remove session from the map if it should have been there. - sessionMap::iterator iter = mSessionsByHandle.find(session->mHandle); - if(iter != mSessionsByHandle.end()) - { - if(iter->second != session) - { - LL_WARNS("Voice") << "Internal error: session mismatch! Session may have been duplicated. Removing version in map." << LL_ENDL; - } - - mSessionsByHandle.erase(iter); - } - else - { - LL_WARNS("Voice") << "Attempt to remove session with handle " << session->mHandle << " not found in map!" << LL_ENDL; - } - } - - session->mHandle = handle; - - if(!handle.empty()) - { - mSessionsByHandle.insert(sessionMap::value_type(session->mHandle, session)); - } - - verifySessionState(); -} - -void LLVivoxVoiceClient::setSessionURI(const sessionStatePtr_t &session, const std::string &uri) -{ - // There used to be a map of session URIs to sessions, which made this complex.... - session->mSIPURI = uri; - - verifySessionState(); -} - -void LLVivoxVoiceClient::deleteSession(const sessionStatePtr_t &session) -{ - // Remove the session from the handle map - if(!session->mHandle.empty()) - { - sessionMap::iterator iter = mSessionsByHandle.find(session->mHandle); - if(iter != mSessionsByHandle.end()) - { - if(iter->second != session) - { - LL_WARNS("Voice") << "Internal error: session mismatch, removing session in map." << LL_ENDL; - } - mSessionsByHandle.erase(iter); - } - } - - // At this point, the session should be unhooked from all lists and all state should be consistent. - verifySessionState(); - - // If this is the current audio session, clean up the pointer which will soon be dangling. - if(mAudioSession == session) - { - mAudioSession.reset(); - } - - // ditto for the next audio session - if(mNextAudioSession == session) - { - mNextAudioSession.reset(); - } - -} - -void LLVivoxVoiceClient::deleteAllSessions() -{ - LL_DEBUGS("Voice") << LL_ENDL; - - while (!mSessionsByHandle.empty()) - { - const sessionStatePtr_t session = mSessionsByHandle.begin()->second; - deleteSession(session); - } - -} - -void LLVivoxVoiceClient::verifySessionState(void) -{ - LL_DEBUGS("Voice") << "Sessions in handle map=" << mSessionsByHandle.size() << LL_ENDL; - sessionState::VerifySessions(); -} - -void LLVivoxVoiceClient::addObserver(LLVoiceClientParticipantObserver* observer) -{ - mParticipantObservers.insert(observer); -} - -void LLVivoxVoiceClient::removeObserver(LLVoiceClientParticipantObserver* observer) -{ - mParticipantObservers.erase(observer); -} - -void LLVivoxVoiceClient::notifyParticipantObservers() -{ - for (observer_set_t::iterator it = mParticipantObservers.begin(); - it != mParticipantObservers.end(); - ) - { - LLVoiceClientParticipantObserver* observer = *it; - observer->onParticipantsChanged(); - // In case onParticipantsChanged() deleted an entry. - it = mParticipantObservers.upper_bound(observer); - } -} - -void LLVivoxVoiceClient::addObserver(LLVoiceClientStatusObserver* observer) -{ - mStatusObservers.insert(observer); -} - -void LLVivoxVoiceClient::removeObserver(LLVoiceClientStatusObserver* observer) -{ - mStatusObservers.erase(observer); -} - -void LLVivoxVoiceClient::notifyStatusObservers(LLVoiceClientStatusObserver::EStatusType status) -{ - LL_DEBUGS("Voice") << "( " << LLVoiceClientStatusObserver::status2string(status) << " )" - << " mAudioSession=" << mAudioSession - << LL_ENDL; - - if(mAudioSession) - { - if(status == LLVoiceClientStatusObserver::ERROR_UNKNOWN) - { - switch(mAudioSession->mErrorStatusCode) - { - case 20713: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_FULL; break; - case 20714: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_LOCKED; break; - case 20715: - //invalid channel, we may be using a set of poorly cached - //info - status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; - break; - case 1009: - //invalid username and password - status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; - break; - } - - // Reset the error code to make sure it won't be reused later by accident. - mAudioSession->mErrorStatusCode = 0; - } - else if(status == LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL) - { - switch(mAudioSession->mErrorStatusCode) - { - case HTTP_NOT_FOUND: // NOT_FOUND - // *TODO: Should this be 503? - case 480: // TEMPORARILY_UNAVAILABLE - case HTTP_REQUEST_TIME_OUT: // REQUEST_TIMEOUT - // call failed because other user was not available - // treat this as an error case - status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; - - // Reset the error code to make sure it won't be reused later by accident. - mAudioSession->mErrorStatusCode = 0; - break; - } - } - } - - LLSD channel_info = getAudioSessionChannelInfo(); - LL_DEBUGS("Voice") - << " " << LLVoiceClientStatusObserver::status2string(status) - << ", session channelInfo " << channel_info - << ", proximal is " << inSpatialChannel() - << LL_ENDL; - - if (!mProcessChannels) - { - // we're not processing...another voice module is. - // so nobody wants to hear from us. - return; - } - for (status_observer_set_t::iterator it = mStatusObservers.begin(); - it != mStatusObservers.end(); - ) - { - LLVoiceClientStatusObserver* observer = *it; - observer->onChange(status, channel_info, inSpatialChannel()); - // In case onError() deleted an entry. - it = mStatusObservers.upper_bound(observer); - } - - // skipped to avoid speak button blinking - if ( status != LLVoiceClientStatusObserver::STATUS_JOINING - && status != LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL - && status != LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED) - { - bool voice_status = LLVoiceClient::getInstance()->voiceEnabled() && LLVoiceClient::getInstance()->isVoiceWorking(); - - LL_WARNS("Voice") << "Setting voice connected " << (voice_status ? "True" : "False") << LL_ENDL; - gAgent.setVoiceConnected(voice_status); - - if (voice_status) - { - LLAppViewer::instance()->postToMainCoro([=]() { LLFirstUse::speak(true); }); - } - } -} - -void LLVivoxVoiceClient::addObserver(LLFriendObserver* observer) -{ - mFriendObservers.insert(observer); -} - -void LLVivoxVoiceClient::removeObserver(LLFriendObserver* observer) -{ - mFriendObservers.erase(observer); -} - -void LLVivoxVoiceClient::notifyFriendObservers() -{ - for (friend_observer_set_t::iterator it = mFriendObservers.begin(); - it != mFriendObservers.end(); - ) - { - LLFriendObserver* observer = *it; - it++; - // The only friend-related thing we notify on is online/offline transitions. - observer->changed(LLFriendObserver::ONLINE); - } -} - -void LLVivoxVoiceClient::lookupName(const LLUUID &id) -{ - if (mAvatarNameCacheConnection.connected()) - { - mAvatarNameCacheConnection.disconnect(); - } - mAvatarNameCacheConnection = LLAvatarNameCache::get(id, boost::bind(&LLVivoxVoiceClient::onAvatarNameCache, this, _1, _2)); -} - -void LLVivoxVoiceClient::onAvatarNameCache(const LLUUID& agent_id, - const LLAvatarName& av_name) -{ - mAvatarNameCacheConnection.disconnect(); - std::string display_name = av_name.getDisplayName(); - avatarNameResolved(agent_id, display_name); -} - -void LLVivoxVoiceClient::predAvatarNameResolution(const LLVivoxVoiceClient::sessionStatePtr_t &session, LLUUID id, std::string name) -{ - participantStatePtr_t participant(session->findParticipantByID(id)); - if (participant) - { - // Found -- fill in the name - participant->mAccountName = name; - // and post a "participants updated" message to listeners later. - session->mParticipantsChanged = true; - } - - // Check whether this is a p2p session whose caller name just resolved - if (session->mCallerID == id) - { - // this session's "caller ID" just resolved. Fill in the name. - session->mName = name; - if (session->mTextInvitePending) - { - session->mTextInvitePending = false; - - // We don't need to call LLIMMgr::getInstance()->addP2PSession() here. The first incoming message will create the panel. - } - if (session->mVoiceInvitePending) - { - session->mVoiceInvitePending = false; - LLIMMgr::getInstance()->inviteToSession( - session->mIMSessionID, - session->mName, - session->mCallerID, - session->mName, - IM_SESSION_P2P_INVITE, - LLIMMgr::INVITATION_TYPE_VOICE, - session->getVoiceChannelInfo()); - } - } -} - -void LLVivoxVoiceClient::avatarNameResolved(const LLUUID &id, const std::string &name) -{ - sessionState::for_each(boost::bind(predAvatarNameResolution, _1, id, name)); -} - -bool LLVivoxVoiceClient::setVoiceEffect(const LLUUID& id) -{ - if (!mAudioSession) - { - return false; - } - - if (!id.isNull()) - { - if (mVoiceFontMap.empty()) - { - LL_DEBUGS("Voice") << "Voice fonts not available." << LL_ENDL; - return false; - } - else if (mVoiceFontMap.find(id) == mVoiceFontMap.end()) - { - LL_DEBUGS("Voice") << "Invalid voice font " << id << LL_ENDL; - return false; - } - } - - // *TODO: Check for expired fonts? - mAudioSession->mVoiceFontID = id; - - // *TODO: Separate voice font defaults for spatial chat and IM? - gSavedPerAccountSettings.setString("VoiceEffectDefault", id.asString()); - - sessionSetVoiceFontSendMessage(mAudioSession); - notifyVoiceFontObservers(); - - return true; -} - -const LLUUID LLVivoxVoiceClient::getVoiceEffect() -{ - return mAudioSession ? mAudioSession->mVoiceFontID : LLUUID::null; -} - -LLSD LLVivoxVoiceClient::getVoiceEffectProperties(const LLUUID& id) -{ - LLSD sd; - - voice_font_map_t::iterator iter = mVoiceFontMap.find(id); - if (iter != mVoiceFontMap.end()) - { - sd["template_only"] = false; - } - else - { - // Voice effect is not in the voice font map, see if there is a template - iter = mVoiceFontTemplateMap.find(id); - if (iter == mVoiceFontTemplateMap.end()) - { - LL_WARNS("Voice") << "Voice effect " << id << "not found." << LL_ENDL; - return sd; - } - sd["template_only"] = true; - } - - voiceFontEntry *font = iter->second; - sd["name"] = font->mName; - sd["expiry_date"] = font->mExpirationDate; - sd["is_new"] = font->mIsNew; - - return sd; -} - -LLVivoxVoiceClient::voiceFontEntry::voiceFontEntry(LLUUID& id) : - mID(id), - mFontIndex(0), - mFontType(VOICE_FONT_TYPE_NONE), - mFontStatus(VOICE_FONT_STATUS_NONE), - mIsNew(false) -{ - mExpiryTimer.stop(); -} - -LLVivoxVoiceClient::voiceFontEntry::~voiceFontEntry() -{ -} - -void LLVivoxVoiceClient::refreshVoiceEffectLists(bool clear_lists) -{ - if (clear_lists) - { - mVoiceFontsReceived = false; - deleteAllVoiceFonts(); - deleteVoiceFontTemplates(); - } - - accountGetSessionFontsSendMessage(); - accountGetTemplateFontsSendMessage(); -} - -const voice_effect_list_t& LLVivoxVoiceClient::getVoiceEffectList() const -{ - return mVoiceFontList; -} - -const voice_effect_list_t& LLVivoxVoiceClient::getVoiceEffectTemplateList() const -{ - return mVoiceFontTemplateList; -} - -void LLVivoxVoiceClient::addVoiceFont(const S32 font_index, - const std::string &name, - const std::string &description, - const LLDate &expiration_date, - bool has_expired, - const S32 font_type, - const S32 font_status, - const bool template_font) -{ - // Vivox SessionFontIDs are not guaranteed to remain the same between - // sessions or grids so use a UUID for the name. - - // If received name is not a UUID, fudge one by hashing the name and type. - LLUUID font_id; - if (LLUUID::validate(name)) - { - font_id = LLUUID(name); - } - else - { - font_id.generate(STRINGIZE(font_type << ":" << name)); - } - - voiceFontEntry *font = NULL; - - voice_font_map_t& font_map = template_font ? mVoiceFontTemplateMap : mVoiceFontMap; - voice_effect_list_t& font_list = template_font ? mVoiceFontTemplateList : mVoiceFontList; - - // Check whether we've seen this font before. - voice_font_map_t::iterator iter = font_map.find(font_id); - bool new_font = (iter == font_map.end()); - - // Override the has_expired flag if we have passed the expiration_date as a double check. - if (expiration_date.secondsSinceEpoch() < (LLDate::now().secondsSinceEpoch() + VOICE_FONT_EXPIRY_INTERVAL)) - { - has_expired = true; - } - - if (has_expired) - { - LL_DEBUGS("VoiceFont") << "Expired " << (template_font ? "Template " : "") - << expiration_date.asString() << " " << font_id - << " (" << font_index << ") " << name << LL_ENDL; - - // Remove existing session fonts that have expired since we last saw them. - if (!new_font && !template_font) - { - deleteVoiceFont(font_id); - } - return; - } - - if (new_font) - { - // If it is a new font create a new entry. - font = new voiceFontEntry(font_id); - } - else - { - // Not a new font, update the existing entry - font = iter->second; - } - - if (font) - { - font->mFontIndex = font_index; - // Use the description for the human readable name if available, as the - // "name" may be a UUID. - font->mName = description.empty() ? name : description; - font->mFontType = font_type; - font->mFontStatus = font_status; - - // If the font is new or the expiration date has changed the expiry timers need updating. - if (!template_font && (new_font || font->mExpirationDate != expiration_date)) - { - font->mExpirationDate = expiration_date; - - // Set the expiry timer to trigger a notification when the voice font can no longer be used. - font->mExpiryTimer.start(); - font->mExpiryTimer.setExpiryAt(expiration_date.secondsSinceEpoch() - VOICE_FONT_EXPIRY_INTERVAL); - - // Only flag new session fonts after the first time we have fetched the list. - if (mVoiceFontsReceived) - { - font->mIsNew = true; - mVoiceFontsNew = true; - } - } - - LL_DEBUGS("VoiceFont") << (template_font ? "Template " : "") - << font->mExpirationDate.asString() << " " << font->mID - << " (" << font->mFontIndex << ") " << name << LL_ENDL; - - if (new_font) - { - font_map.insert(voice_font_map_t::value_type(font->mID, font)); - font_list.insert(voice_effect_list_t::value_type(font->mName, font->mID)); - } - - mVoiceFontListDirty = true; - - // Debugging stuff - - if (font_type < VOICE_FONT_TYPE_NONE || font_type >= VOICE_FONT_TYPE_UNKNOWN) - { - LL_WARNS("VoiceFont") << "Unknown voice font type: " << font_type << LL_ENDL; - } - if (font_status < VOICE_FONT_STATUS_NONE || font_status >= VOICE_FONT_STATUS_UNKNOWN) - { - LL_WARNS("VoiceFont") << "Unknown voice font status: " << font_status << LL_ENDL; - } - } -} - -void LLVivoxVoiceClient::expireVoiceFonts() -{ - // *TODO: If we are selling voice fonts in packs, there are probably - // going to be a number of fonts with the same expiration time, so would - // be more efficient to just keep a list of expiration times rather - // than checking each font individually. - - bool have_expired = false; - bool expired_in_use = false; - - LLUUID current_effect = LLVoiceClient::instance().getVoiceEffectDefault(); - - voice_font_map_t::iterator iter; - for (iter = mVoiceFontMap.begin(); iter != mVoiceFontMap.end(); ++iter) - { - voiceFontEntry* voice_font = iter->second; - LLFrameTimer& expiry_timer = voice_font->mExpiryTimer; - - // Check for expired voice fonts - if (expiry_timer.getStarted() && expiry_timer.hasExpired()) - { - // Check whether it is the active voice font - if (voice_font->mID == current_effect) - { - // Reset to no voice effect. - setVoiceEffect(LLUUID::null); - expired_in_use = true; - } - - LL_DEBUGS("Voice") << "Voice Font " << voice_font->mName << " has expired." << LL_ENDL; - deleteVoiceFont(voice_font->mID); - have_expired = true; - } - } - - LLSD args; - args["URL"] = LLTrans::getString("voice_morphing_url"); - args["PREMIUM_URL"] = LLTrans::getString("premium_voice_morphing_url"); - - // Give a notification if any voice fonts have expired. - if (have_expired) - { - if (expired_in_use) - { - LLNotificationsUtil::add("VoiceEffectsExpiredInUse", args); - } - else - { - LLNotificationsUtil::add("VoiceEffectsExpired", args); - } - - // Refresh voice font lists in the UI. - notifyVoiceFontObservers(); - } -} - -void LLVivoxVoiceClient::deleteVoiceFont(const LLUUID& id) -{ - // Remove the entry from the voice font list. - voice_effect_list_t::iterator list_iter = mVoiceFontList.begin(); - while (list_iter != mVoiceFontList.end()) - { - if (list_iter->second == id) - { - LL_DEBUGS("VoiceFont") << "Removing " << id << " from the voice font list." << LL_ENDL; - list_iter = mVoiceFontList.erase(list_iter); - mVoiceFontListDirty = true; - } - else - { - ++list_iter; - } - } - - // Find the entry in the voice font map and erase its data. - voice_font_map_t::iterator map_iter = mVoiceFontMap.find(id); - if (map_iter != mVoiceFontMap.end()) - { - delete map_iter->second; - } - - // Remove the entry from the voice font map. - mVoiceFontMap.erase(map_iter); -} - -void LLVivoxVoiceClient::deleteAllVoiceFonts() -{ - mVoiceFontList.clear(); - - voice_font_map_t::iterator iter; - for (iter = mVoiceFontMap.begin(); iter != mVoiceFontMap.end(); ++iter) - { - delete iter->second; - } - mVoiceFontMap.clear(); -} - -void LLVivoxVoiceClient::deleteVoiceFontTemplates() -{ - mVoiceFontTemplateList.clear(); - - voice_font_map_t::iterator iter; - for (iter = mVoiceFontTemplateMap.begin(); iter != mVoiceFontTemplateMap.end(); ++iter) - { - delete iter->second; - } - mVoiceFontTemplateMap.clear(); -} - -S32 LLVivoxVoiceClient::getVoiceFontIndex(const LLUUID& id) const -{ - S32 result = 0; - if (!id.isNull()) - { - voice_font_map_t::const_iterator it = mVoiceFontMap.find(id); - if (it != mVoiceFontMap.end()) - { - result = it->second->mFontIndex; - } - else - { - LL_WARNS("VoiceFont") << "Selected voice font " << id << " is not available." << LL_ENDL; - } - } - return result; -} - -S32 LLVivoxVoiceClient::getVoiceFontTemplateIndex(const LLUUID& id) const -{ - S32 result = 0; - if (!id.isNull()) - { - voice_font_map_t::const_iterator it = mVoiceFontTemplateMap.find(id); - if (it != mVoiceFontTemplateMap.end()) - { - result = it->second->mFontIndex; - } - else - { - LL_WARNS("VoiceFont") << "Selected voice font template " << id << " is not available." << LL_ENDL; - } - } - return result; -} - -void LLVivoxVoiceClient::accountGetSessionFontsSendMessage() -{ - if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("VoiceFont") << "Requesting voice font list." << LL_ENDL; - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::accountGetTemplateFontsSendMessage() -{ - if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("VoiceFont") << "Requesting voice font template list." << LL_ENDL; - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::sessionSetVoiceFontSendMessage(const sessionStatePtr_t &session) -{ - S32 font_index = getVoiceFontIndex(session->mVoiceFontID); - LL_DEBUGS("VoiceFont") << "Requesting voice font: " << session->mVoiceFontID << " (" << font_index << "), session handle: " << session->mHandle << LL_ENDL; - - std::ostringstream stream; - - stream - << "" - << "" << session->mHandle << "" - << "" << font_index << "" - << "\n\n\n"; - - writeString(stream.str()); -} - -void LLVivoxVoiceClient::accountGetSessionFontsResponse(int statusCode, const std::string &statusString) -{ - if (mIsWaitingForFonts) - { - // *TODO: We seem to get multiple events of this type. Should figure a way to advance only after - // receiving the last one. - LLSD result(LLSDMap("voice_fonts", LLSD::Boolean(true))); - - mVivoxPump.post(result); - } - notifyVoiceFontObservers(); - mVoiceFontsReceived = true; -} - -void LLVivoxVoiceClient::accountGetTemplateFontsResponse(int statusCode, const std::string &statusString) -{ - // Voice font list entries were updated via addVoiceFont() during parsing. - notifyVoiceFontObservers(); -} -void LLVivoxVoiceClient::addObserver(LLVoiceEffectObserver* observer) -{ - mVoiceFontObservers.insert(observer); -} - -void LLVivoxVoiceClient::removeObserver(LLVoiceEffectObserver* observer) -{ - mVoiceFontObservers.erase(observer); -} - -// method checks the item in VoiceMorphing menu for appropriate current voice font -bool LLVivoxVoiceClient::onCheckVoiceEffect(const std::string& voice_effect_name) -{ - LLVoiceEffectInterface * effect_interfacep = LLVoiceClient::instance().getVoiceEffectInterface(); - if (NULL != effect_interfacep) - { - const LLUUID& currect_voice_effect_id = effect_interfacep->getVoiceEffect(); - - if (currect_voice_effect_id.isNull()) - { - if (voice_effect_name == "NoVoiceMorphing") - { - return true; - } - } - else - { - const LLSD& voice_effect_props = effect_interfacep->getVoiceEffectProperties(currect_voice_effect_id); - if (voice_effect_props["name"].asString() == voice_effect_name) - { - return true; - } - } - } - - return false; -} - -// method changes voice font for selected VoiceMorphing menu item -void LLVivoxVoiceClient::onClickVoiceEffect(const std::string& voice_effect_name) -{ - LLVoiceEffectInterface * effect_interfacep = LLVoiceClient::instance().getVoiceEffectInterface(); - if (NULL != effect_interfacep) - { - if (voice_effect_name == "NoVoiceMorphing") - { - effect_interfacep->setVoiceEffect(LLUUID()); - return; - } - const voice_effect_list_t& effect_list = effect_interfacep->getVoiceEffectList(); - if (!effect_list.empty()) - { - for (voice_effect_list_t::const_iterator it = effect_list.begin(); it != effect_list.end(); ++it) - { - if (voice_effect_name == it->first) - { - effect_interfacep->setVoiceEffect(it->second); - return; - } - } - } - } -} - -// it updates VoiceMorphing menu items in accordance with purchased properties -void LLVivoxVoiceClient::updateVoiceMorphingMenu() -{ - if (mVoiceFontListDirty) - { - LLVoiceEffectInterface * effect_interfacep = LLVoiceClient::instance().getVoiceEffectInterface(); - if (effect_interfacep) - { - const voice_effect_list_t& effect_list = effect_interfacep->getVoiceEffectList(); - if (!effect_list.empty()) - { - LLMenuGL * voice_morphing_menup = gMenuBarView->findChildMenuByName("VoiceMorphing", true); - - if (NULL != voice_morphing_menup) - { - S32 items = voice_morphing_menup->getItemCount(); - if (items > 0) - { - voice_morphing_menup->erase(1, items - 3, false); - - S32 pos = 1; - for (voice_effect_list_t::const_iterator it = effect_list.begin(); it != effect_list.end(); ++it) - { - LLMenuItemCheckGL::Params p; - p.name = it->first; - p.label = it->first; - p.on_check.function(boost::bind(&LLVivoxVoiceClient::onCheckVoiceEffect, this, it->first)); - p.on_click.function(boost::bind(&LLVivoxVoiceClient::onClickVoiceEffect, this, it->first)); - LLMenuItemCheckGL * voice_effect_itemp = LLUICtrlFactory::create(p); - voice_morphing_menup->insert(pos++, voice_effect_itemp, false); - } - - voice_morphing_menup->needsArrange(); - } - } - } - } - } -} -void LLVivoxVoiceClient::notifyVoiceFontObservers() -{ - LL_DEBUGS("VoiceFont") << "Notifying voice effect observers. Lists changed: " << mVoiceFontListDirty << LL_ENDL; - - updateVoiceMorphingMenu(); - - for (voice_font_observer_set_t::iterator it = mVoiceFontObservers.begin(); - it != mVoiceFontObservers.end();) - { - LLVoiceEffectObserver* observer = *it; - observer->onVoiceEffectChanged(mVoiceFontListDirty); - // In case onVoiceEffectChanged() deleted an entry. - it = mVoiceFontObservers.upper_bound(observer); - } - mVoiceFontListDirty = false; - - // If new Voice Fonts have been added notify the user. - if (mVoiceFontsNew) - { - if (mVoiceFontsReceived) - { - LLNotificationsUtil::add("VoiceEffectsNew"); - } - mVoiceFontsNew = false; - } -} - -void LLVivoxVoiceClient::enablePreviewBuffer(bool enable) -{ - LLSD result; - mCaptureBufferMode = enable; - - if (enable) - result["recplay"] = "start"; - else - result["recplay"] = "quit"; - - mVivoxPump.post(result); - - if(mCaptureBufferMode && mIsInChannel) - { - LL_DEBUGS("Voice") << "no channel" << LL_ENDL; - sessionTerminate(); - } -} - -void LLVivoxVoiceClient::recordPreviewBuffer() -{ - if (!mCaptureBufferMode) - { - LL_DEBUGS("Voice") << "Not in voice effect preview mode, cannot start recording." << LL_ENDL; - mCaptureBufferRecording = false; - return; - } - - mCaptureBufferRecording = true; - - LLSD result(LLSDMap("recplay", "record")); - mVivoxPump.post(result); -} - -void LLVivoxVoiceClient::playPreviewBuffer(const LLUUID& effect_id) -{ - if (!mCaptureBufferMode) - { - LL_DEBUGS("Voice") << "Not in voice effect preview mode, no buffer to play." << LL_ENDL; - mCaptureBufferRecording = false; - return; - } - - if (!mCaptureBufferRecorded) - { - // Can't play until we have something recorded! - mCaptureBufferPlaying = false; - return; - } - - mPreviewVoiceFont = effect_id; - mCaptureBufferPlaying = true; - - LLSD result(LLSDMap("recplay", "playback")); - mVivoxPump.post(result); -} - -void LLVivoxVoiceClient::stopPreviewBuffer() -{ - mCaptureBufferRecording = false; - mCaptureBufferPlaying = false; - - LLSD result(LLSDMap("recplay", "quit")); - mVivoxPump.post(result); -} - -bool LLVivoxVoiceClient::isPreviewRecording() -{ - return (mCaptureBufferMode && mCaptureBufferRecording); -} - -bool LLVivoxVoiceClient::isPreviewPlaying() -{ - return (mCaptureBufferMode && mCaptureBufferPlaying); -} - -void LLVivoxVoiceClient::captureBufferRecordStartSendMessage() -{ if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("Voice") << "Starting audio capture to buffer." << LL_ENDL; - - // Start capture - stream - << "" - << "" - << "\n\n\n"; - - // Unmute the mic - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "false" - << "\n\n\n"; - - // Dirty the mute mic state so that it will get reset when we finishing previewing - mMuteMicDirty = true; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::captureBufferRecordStopSendMessage() -{ - if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("Voice") << "Stopping audio capture to buffer." << LL_ENDL; - - // Mute the mic. Mic mute state was dirtied at recording start, so will be reset when finished previewing. - stream << "" - << "" << LLVivoxSecurity::getInstance()->connectorHandle() << "" - << "true" - << "\n\n\n"; - - // Stop capture - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::captureBufferPlayStartSendMessage(const LLUUID& voice_font_id) -{ - if(mAccountLoggedIn) - { - // Track how may play requests are sent, so we know how many stop events to - // expect before play actually stops. - ++mPlayRequestCount; - - std::ostringstream stream; - - LL_DEBUGS("Voice") << "Starting audio buffer playback." << LL_ENDL; - - S32 font_index = getVoiceFontTemplateIndex(voice_font_id); - LL_DEBUGS("Voice") << "With voice font: " << voice_font_id << " (" << font_index << ")" << LL_ENDL; - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" << font_index << "" - << "" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -void LLVivoxVoiceClient::captureBufferPlayStopSendMessage() -{ - if(mAccountLoggedIn) - { - std::ostringstream stream; - - LL_DEBUGS("Voice") << "Stopping audio buffer playback." << LL_ENDL; - - stream - << "" - << "" << LLVivoxSecurity::getInstance()->accountHandle() << "" - << "" - << "\n\n\n"; - - writeString(stream.str()); - } -} - -LLVivoxProtocolParser::LLVivoxProtocolParser() -{ - parser = XML_ParserCreate(NULL); - - reset(); -} - -void LLVivoxProtocolParser::reset() -{ - responseDepth = 0; - ignoringTags = false; - accumulateText = false; - energy = 0.f; - hasText = false; - hasAudio = false; - hasVideo = false; - terminated = false; - ignoreDepth = 0; - isChannel = false; - incoming = false; - enabled = false; - isEvent = false; - isLocallyMuted = false; - isModeratorMuted = false; - isSpeaking = false; - participantType = 0; - returnCode = -1; - state = 0; - statusCode = 0; - volume = 0; - textBuffer.clear(); - alias.clear(); - numberOfAliases = 0; - applicationString.clear(); -} - -//virtual -LLVivoxProtocolParser::~LLVivoxProtocolParser() -{ - if (parser) - XML_ParserFree(parser); -} - -static LLTrace::BlockTimerStatHandle FTM_VIVOX_PROCESS("Vivox Process"); - -// virtual -LLIOPipe::EStatus LLVivoxProtocolParser::process_impl( - const LLChannelDescriptors& channels, - buffer_ptr_t& buffer, - bool& eos, - LLSD& context, - LLPumpIO* pump) -{ - LL_RECORD_BLOCK_TIME(FTM_VIVOX_PROCESS); - LLBufferStream istr(channels, buffer.get()); - std::ostringstream ostr; - while (istr.good()) - { - char buf[1024]; - istr.read(buf, sizeof(buf)); - mInput.append(buf, istr.gcount()); - } - - // Look for input delimiter(s) in the input buffer. If one is found, send the message to the xml parser. - size_t start = 0; - size_t delim; - while((delim = mInput.find("\n\n\n", start)) != std::string::npos) - { - - // Reset internal state of the LLVivoxProtocolParser (no effect on the expat parser) - reset(); - - XML_ParserReset(parser, NULL); - XML_SetElementHandler(parser, ExpatStartTag, ExpatEndTag); - XML_SetCharacterDataHandler(parser, ExpatCharHandler); - XML_SetUserData(parser, this); - XML_Parse(parser, mInput.data() + start, static_cast(delim - start), false); - - - LL_DEBUGS("VivoxProtocolParser") << "parsing: " << mInput.substr(start, delim - start) << LL_ENDL; - start = delim + 3; - } - - if(start != 0) - mInput = mInput.substr(start); - - LL_DEBUGS("VivoxProtocolParser") << "at end, mInput is: " << mInput << LL_ENDL; - - if(!LLVivoxVoiceClient::sConnected) - { - // If voice has been disabled, we just want to close the socket. This does so. - LL_INFOS("Voice") << "returning STATUS_STOP" << LL_ENDL; - return STATUS_STOP; - } - - return STATUS_OK; -} - -void XMLCALL LLVivoxProtocolParser::ExpatStartTag(void *data, const char *el, const char **attr) -{ - if (data) - { - LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; - object->StartTag(el, attr); - } -} - -// -------------------------------------------------------------------------------- - -void XMLCALL LLVivoxProtocolParser::ExpatEndTag(void *data, const char *el) -{ - if (data) - { - LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; - object->EndTag(el); - } -} - -// -------------------------------------------------------------------------------- - -void XMLCALL LLVivoxProtocolParser::ExpatCharHandler(void *data, const XML_Char *s, int len) -{ - if (data) - { - LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; - object->CharData(s, len); - } -} - -// -------------------------------------------------------------------------------- - - -void LLVivoxProtocolParser::StartTag(const char *tag, const char **attr) -{ - // Reset the text accumulator. We shouldn't have strings that are inturrupted by new tags - textBuffer.clear(); - // only accumulate text if we're not ignoring tags. - accumulateText = !ignoringTags; - - if (responseDepth == 0) - { - isEvent = !stricmp("Event", tag); - - if (!stricmp("Response", tag) || isEvent) - { - // Grab the attributes - while (*attr) - { - const char *key = *attr++; - const char *value = *attr++; - - if (!stricmp("requestId", key)) - { - requestId = value; - } - else if (!stricmp("action", key)) - { - actionString = value; - } - else if (!stricmp("type", key)) - { - eventTypeString = value; - } - } - } - LL_DEBUGS("VivoxProtocolParser") << tag << " (" << responseDepth << ")" << LL_ENDL; - } - else - { - if (ignoringTags) - { - LL_DEBUGS("VivoxProtocolParser") << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; - } - else - { - LL_DEBUGS("VivoxProtocolParser") << tag << " (" << responseDepth << ")" << LL_ENDL; - - // Ignore the InputXml stuff so we don't get confused - if (!stricmp("InputXml", tag)) - { - ignoringTags = true; - ignoreDepth = responseDepth; - accumulateText = false; - - LL_DEBUGS("VivoxProtocolParser") << "starting ignore, ignoreDepth is " << ignoreDepth << LL_ENDL; - } - else if (!stricmp("CaptureDevices", tag)) - { - LLVivoxVoiceClient::getInstance()->clearCaptureDevices(); - } - else if (!stricmp("RenderDevices", tag)) - { - LLVivoxVoiceClient::getInstance()->clearRenderDevices(); - } - else if (!stricmp("CaptureDevice", tag)) - { - deviceString.clear(); - } - else if (!stricmp("RenderDevice", tag)) - { - deviceString.clear(); - } - else if (!stricmp("SessionFont", tag)) - { - id = 0; - nameString.clear(); - descriptionString.clear(); - expirationDate = LLDate(); - hasExpired = false; - fontType = 0; - fontStatus = 0; - } - else if (!stricmp("TemplateFont", tag)) - { - id = 0; - nameString.clear(); - descriptionString.clear(); - expirationDate = LLDate(); - hasExpired = false; - fontType = 0; - fontStatus = 0; - } - else if (!stricmp("MediaCompletionType", tag)) - { - mediaCompletionType.clear(); - } - } - } - responseDepth++; -} - -// -------------------------------------------------------------------------------- - -void LLVivoxProtocolParser::EndTag(const char *tag) -{ - const std::string& string = textBuffer; - - responseDepth--; - - if (ignoringTags) - { - if (ignoreDepth == responseDepth) - { - LL_DEBUGS("VivoxProtocolParser") << "end of ignore" << LL_ENDL; - ignoringTags = false; - } - else - { - LL_DEBUGS("VivoxProtocolParser") << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; - } - } - - if (!ignoringTags) - { - LL_DEBUGS("VivoxProtocolParser") << "processing tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; - - // Closing a tag. Finalize the text we've accumulated and reset - if (!stricmp("ReturnCode", tag)) - returnCode = strtol(string.c_str(), NULL, 10); - else if (!stricmp("SessionHandle", tag)) - sessionHandle = string; - else if (!stricmp("SessionGroupHandle", tag)) - sessionGroupHandle = string; - else if (!stricmp("StatusCode", tag)) - statusCode = strtol(string.c_str(), NULL, 10); - else if (!stricmp("StatusString", tag)) - statusString = string; - else if (!stricmp("ParticipantURI", tag)) - uriString = string; - else if (!stricmp("Volume", tag)) - volume = strtol(string.c_str(), NULL, 10); - else if (!stricmp("Energy", tag)) - energy = (F32)strtod(string.c_str(), NULL); - else if (!stricmp("IsModeratorMuted", tag)) - isModeratorMuted = !stricmp(string.c_str(), "true"); - else if (!stricmp("IsSpeaking", tag)) - isSpeaking = !stricmp(string.c_str(), "true"); - else if (!stricmp("Alias", tag)) - alias = string; - else if (!stricmp("NumberOfAliases", tag)) - numberOfAliases = strtol(string.c_str(), NULL, 10); - else if (!stricmp("Application", tag)) - applicationString = string; - else if (!stricmp("ConnectorHandle", tag)) - connectorHandle = string; - else if (!stricmp("VersionID", tag)) - versionID = string; - else if (!stricmp("Version", tag)) - mBuildID = string; - else if (!stricmp("AccountHandle", tag)) - accountHandle = string; - else if (!stricmp("State", tag)) - state = strtol(string.c_str(), NULL, 10); - else if (!stricmp("URI", tag)) - uriString = string; - else if (!stricmp("IsChannel", tag)) - isChannel = !stricmp(string.c_str(), "true"); - else if (!stricmp("Incoming", tag)) - incoming = !stricmp(string.c_str(), "true"); - else if (!stricmp("Enabled", tag)) - enabled = !stricmp(string.c_str(), "true"); - else if (!stricmp("Name", tag)) - nameString = string; - else if (!stricmp("AudioMedia", tag)) - audioMediaString = string; - else if (!stricmp("ChannelName", tag)) - nameString = string; - else if (!stricmp("DisplayName", tag)) - displayNameString = string; - else if (!stricmp("Device", tag)) - deviceString = string; - else if (!stricmp("AccountName", tag)) - nameString = string; - else if (!stricmp("ParticipantType", tag)) - participantType = strtol(string.c_str(), NULL, 10); - else if (!stricmp("IsLocallyMuted", tag)) - isLocallyMuted = !stricmp(string.c_str(), "true"); - else if (!stricmp("MicEnergy", tag)) - energy = (F32)strtod(string.c_str(), NULL); - else if (!stricmp("ChannelName", tag)) - nameString = string; - else if (!stricmp("ChannelURI", tag)) - uriString = string; - else if (!stricmp("BuddyURI", tag)) - uriString = string; - else if (!stricmp("Presence", tag)) - statusString = string; - else if (!stricmp("CaptureDevices", tag)) - { - LLVivoxVoiceClient::getInstance()->setDevicesListUpdated(true); - } - else if (!stricmp("RenderDevices", tag)) - { - LLVivoxVoiceClient::getInstance()->setDevicesListUpdated(true); - } - else if (!stricmp("CaptureDevice", tag)) - { - LLVivoxVoiceClient::getInstance()->addCaptureDevice(LLVoiceDevice(displayNameString, deviceString)); - } - else if (!stricmp("RenderDevice", tag)) - { - LLVivoxVoiceClient::getInstance()->addRenderDevice(LLVoiceDevice(displayNameString, deviceString)); - } - else if (!stricmp("BlockMask", tag)) - blockMask = string; - else if (!stricmp("PresenceOnly", tag)) - presenceOnly = string; - else if (!stricmp("AutoAcceptMask", tag)) - autoAcceptMask = string; - else if (!stricmp("AutoAddAsBuddy", tag)) - autoAddAsBuddy = string; - else if (!stricmp("MessageHeader", tag)) - messageHeader = string; - else if (!stricmp("MessageBody", tag)) - messageBody = string; - else if (!stricmp("NotificationType", tag)) - notificationType = string; - else if (!stricmp("HasText", tag)) - hasText = !stricmp(string.c_str(), "true"); - else if (!stricmp("HasAudio", tag)) - hasAudio = !stricmp(string.c_str(), "true"); - else if (!stricmp("HasVideo", tag)) - hasVideo = !stricmp(string.c_str(), "true"); - else if (!stricmp("Terminated", tag)) - terminated = !stricmp(string.c_str(), "true"); - else if (!stricmp("SubscriptionHandle", tag)) - subscriptionHandle = string; - else if (!stricmp("SubscriptionType", tag)) - subscriptionType = string; - else if (!stricmp("SessionFont", tag)) - { - LLVivoxVoiceClient::getInstance()->addVoiceFont(id, nameString, descriptionString, expirationDate, hasExpired, fontType, fontStatus, false); - } - else if (!stricmp("TemplateFont", tag)) - { - LLVivoxVoiceClient::getInstance()->addVoiceFont(id, nameString, descriptionString, expirationDate, hasExpired, fontType, fontStatus, true); - } - else if (!stricmp("ID", tag)) - { - id = strtol(string.c_str(), NULL, 10); - } - else if (!stricmp("Description", tag)) - { - descriptionString = string; - } - else if (!stricmp("ExpirationDate", tag)) - { - expirationDate = expiryTimeStampToLLDate(string); - } - else if (!stricmp("Expired", tag)) - { - hasExpired = !stricmp(string.c_str(), "1"); - } - else if (!stricmp("Type", tag)) - { - fontType = strtol(string.c_str(), NULL, 10); - } - else if (!stricmp("Status", tag)) - { - fontStatus = strtol(string.c_str(), NULL, 10); - } - else if (!stricmp("MediaCompletionType", tag)) - { - mediaCompletionType = string;; - } - - textBuffer.clear(); - accumulateText= false; - - if (responseDepth == 0) - { - // We finished all of the XML, process the data - processResponse(tag); - } - } -} - -// -------------------------------------------------------------------------------- - -void LLVivoxProtocolParser::CharData(const char *buffer, int length) -{ - /* - This method is called for anything that isn't a tag, which can be text you - want that lies between tags, and a lot of stuff you don't want like file formatting - (tabs, spaces, CR/LF, etc). - - Only copy text if we are in accumulate mode... - */ - if (accumulateText) - textBuffer.append(buffer, length); -} - -// -------------------------------------------------------------------------------- - -LLDate LLVivoxProtocolParser::expiryTimeStampToLLDate(const std::string& vivox_ts) -{ - // *HACK: Vivox reports the time incorrectly. LLDate also only parses a - // subset of valid ISO 8601 dates (only handles Z, not offsets). - // So just use the date portion and fix the time here. - std::string time_stamp = vivox_ts.substr(0, 10); - time_stamp += VOICE_FONT_EXPIRY_TIME; - - LL_DEBUGS("VivoxProtocolParser") << "Vivox timestamp " << vivox_ts << " modified to: " << time_stamp << LL_ENDL; - - return LLDate(time_stamp); -} - -// -------------------------------------------------------------------------------- - -void LLVivoxProtocolParser::processResponse(std::string tag) -{ - LL_DEBUGS("VivoxProtocolParser") << tag << LL_ENDL; - - // SLIM SDK: the SDK now returns a statusCode of "200" (OK) for success. This is a change vs. previous SDKs. - // According to Mike S., "The actual API convention is that responses with return codes of 0 are successful, regardless of the status code returned", - // so I believe this will give correct behavior. - - if(returnCode == 0) - statusCode = 0; - - if (isEvent) - { - const char *eventTypeCstr = eventTypeString.c_str(); - LL_DEBUGS("LowVoice") << eventTypeCstr << LL_ENDL; - - if (!stricmp(eventTypeCstr, "ParticipantUpdatedEvent")) - { - // These happen so often that logging them is pretty useless. - LL_DEBUGS("LowVoice") << "Updated Params: " << sessionHandle << ", " << sessionGroupHandle << ", " << uriString << ", " << alias << ", " << isModeratorMuted << ", " << isSpeaking << ", " << volume << ", " << energy << LL_ENDL; - LLVivoxVoiceClient::getInstance()->participantUpdatedEvent(sessionHandle, sessionGroupHandle, uriString, alias, isModeratorMuted, isSpeaking, volume, energy); - } - else if (!stricmp(eventTypeCstr, "AccountLoginStateChangeEvent")) - { - LLVivoxVoiceClient::getInstance()->accountLoginStateChangeEvent(accountHandle, statusCode, statusString, state); - } - else if (!stricmp(eventTypeCstr, "SessionAddedEvent")) - { - /* - - c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 - c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 - sip:confctl-1408789@bhr.vivox.com - true - false - - - */ - LLVivoxVoiceClient::getInstance()->sessionAddedEvent(uriString, alias, sessionHandle, sessionGroupHandle, isChannel, incoming, nameString, applicationString); - } - else if (!stricmp(eventTypeCstr, "SessionRemovedEvent")) - { - LLVivoxVoiceClient::getInstance()->sessionRemovedEvent(sessionHandle, sessionGroupHandle); - } - else if (!stricmp(eventTypeCstr, "SessionGroupUpdatedEvent")) - { - //nothng useful to process for this event, but we should not WARN that we have received it. - } - else if (!stricmp(eventTypeCstr, "SessionGroupAddedEvent")) - { - LLVivoxVoiceClient::getInstance()->sessionGroupAddedEvent(sessionGroupHandle); - } - else if (!stricmp(eventTypeCstr, "MediaStreamUpdatedEvent")) - { - /* - - c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 - c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 - 200 - OK - 2 - false - - */ - LLVivoxVoiceClient::getInstance()->mediaStreamUpdatedEvent(sessionHandle, sessionGroupHandle, statusCode, statusString, state, incoming); - } - else if (!stricmp(eventTypeCstr, "MediaCompletionEvent")) - { - /* - - - AuxBufferAudioCapture - - */ - LLVivoxVoiceClient::getInstance()->mediaCompletionEvent(sessionGroupHandle, mediaCompletionType); - } - else if (!stricmp(eventTypeCstr, "ParticipantAddedEvent")) - { - /* - - c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4 - c1_m1000xFnPP04IpREWNkuw1cOXlhw==4 - sip:xI5auBZ60SJWIk606-1JGRQ==@bhr.vivox.com - xI5auBZ60SJWIk606-1JGRQ== - - 0 - - */ - LL_DEBUGS("LowVoice") << "Added Params: " << sessionHandle << ", " << sessionGroupHandle << ", " << uriString << ", " << alias << ", " << nameString << ", " << displayNameString << ", " << participantType << LL_ENDL; - LLVivoxVoiceClient::getInstance()->participantAddedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString, displayNameString, participantType); - } - else if (!stricmp(eventTypeCstr, "ParticipantRemovedEvent")) - { - /* - - c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4 - c1_m1000xFnPP04IpREWNkuw1cOXlhw==4 - sip:xtx7YNV-3SGiG7rA1fo5Ndw==@bhr.vivox.com - xtx7YNV-3SGiG7rA1fo5Ndw== - - */ - LL_DEBUGS("LowVoice") << "Removed params:" << sessionHandle << ", " << sessionGroupHandle << ", " << uriString << ", " << alias << ", " << nameString << LL_ENDL; - - LLVivoxVoiceClient::getInstance()->participantRemovedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString); - } - else if (!stricmp(eventTypeCstr, "AuxAudioPropertiesEvent")) - { - // These are really spammy in tuning mode - LLVivoxVoiceClient::getInstance()->auxAudioPropertiesEvent(energy); - } - else if (!stricmp(eventTypeCstr, "MessageEvent")) - { - //TODO: This probably is not received any more, it was used to support SLim clients - LLVivoxVoiceClient::getInstance()->messageEvent(sessionHandle, uriString, alias, messageHeader, messageBody, applicationString); - } - else if (!stricmp(eventTypeCstr, "SessionNotificationEvent")) - { - //TODO: This probably is not received any more, it was used to support SLim clients - LLVivoxVoiceClient::getInstance()->sessionNotificationEvent(sessionHandle, uriString, notificationType); - } - else if (!stricmp(eventTypeCstr, "SessionUpdatedEvent")) - { - /* - - c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 - c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 - sip:confctl-9@bhd.vivox.com - 0 - 50 - 1 - 0 - 000 - 0 - - */ - // We don't need to process this, but we also shouldn't warn on it, since that confuses people. - } - else if (!stricmp(eventTypeCstr, "SessionGroupRemovedEvent")) - { - // We don't need to process this, but we also shouldn't warn on it, since that confuses people. - } - else if (!stricmp(eventTypeCstr, "VoiceServiceConnectionStateChangedEvent")) - { - LLVivoxVoiceClient::getInstance()->voiceServiceConnectionStateChangedEvent(statusCode, statusString, mBuildID); - } - else if (!stricmp(eventTypeCstr, "AudioDeviceHotSwapEvent")) - { - /* - - RenderDeviceChanged< / EventType> - - Speakers(Turtle Beach P11 Headset)< / Device> - Speakers(Turtle Beach P11 Headset)< / DisplayName> - SpecificDevice< / Type> - < / RelevantDevice> - < / Event> - */ - // an audio device was removed or added, fetch and update the local list of audio devices. - LLVivoxVoiceClient::getInstance()->getCaptureDevicesSendMessage(); - LLVivoxVoiceClient::getInstance()->getRenderDevicesSendMessage(); - } - else - { - LL_WARNS("VivoxProtocolParser") << "Unknown event type " << eventTypeString << LL_ENDL; - } - } - else - { - const char *actionCstr = actionString.c_str(); - LL_DEBUGS("LowVoice") << actionCstr << LL_ENDL; - - if (!stricmp(actionCstr, "Session.Set3DPosition.1")) - { - // We don't need to process these - } - else if (!stricmp(actionCstr, "Connector.Create.1")) - { - LLVivoxVoiceClient::getInstance()->connectorCreateResponse(statusCode, statusString, connectorHandle, versionID); - } - else if (!stricmp(actionCstr, "Account.Login.1")) - { - LLVivoxVoiceClient::getInstance()->loginResponse(statusCode, statusString, accountHandle, numberOfAliases); - } - else if (!stricmp(actionCstr, "Session.Create.1")) - { - LLVivoxVoiceClient::getInstance()->sessionCreateResponse(requestId, statusCode, statusString, sessionHandle); - } - else if (!stricmp(actionCstr, "SessionGroup.AddSession.1")) - { - LLVivoxVoiceClient::getInstance()->sessionGroupAddSessionResponse(requestId, statusCode, statusString, sessionHandle); - } - else if (!stricmp(actionCstr, "Session.Connect.1")) - { - LLVivoxVoiceClient::getInstance()->sessionConnectResponse(requestId, statusCode, statusString); - } - else if (!stricmp(actionCstr, "Account.Logout.1")) - { - LLVivoxVoiceClient::getInstance()->logoutResponse(statusCode, statusString); - } - else if (!stricmp(actionCstr, "Connector.InitiateShutdown.1")) - { - LLVivoxVoiceClient::getInstance()->connectorShutdownResponse(statusCode, statusString); - } - else if (!stricmp(actionCstr, "Account.GetSessionFonts.1")) - { - LLVivoxVoiceClient::getInstance()->accountGetSessionFontsResponse(statusCode, statusString); - } - else if (!stricmp(actionCstr, "Account.GetTemplateFonts.1")) - { - LLVivoxVoiceClient::getInstance()->accountGetTemplateFontsResponse(statusCode, statusString); - } - else if (!stricmp(actionCstr, "Aux.SetVadProperties.1")) - { - // both values of statusCode (old and more recent) indicate valid requests - if (statusCode != 0 && statusCode != 200) - { - LL_WARNS("Voice") << "Aux.SetVadProperties.1 request failed: " - << "statusCode: " << statusCode - << " and " - << "statusString: " << statusString - << LL_ENDL; - } - } - /* - else if (!stricmp(actionCstr, "Account.ChannelGetList.1")) - { - LLVoiceClient::getInstance()->channelGetListResponse(statusCode, statusString); - } - else if (!stricmp(actionCstr, "Connector.AccountCreate.1")) - { - - } - else if (!stricmp(actionCstr, "Connector.MuteLocalMic.1")) - { - - } - else if (!stricmp(actionCstr, "Connector.MuteLocalSpeaker.1")) - { - - } - else if (!stricmp(actionCstr, "Connector.SetLocalMicVolume.1")) - { - - } - else if (!stricmp(actionCstr, "Connector.SetLocalSpeakerVolume.1")) - { - - } - else if (!stricmp(actionCstr, "Session.ListenerSetPosition.1")) - { - - } - else if (!stricmp(actionCstr, "Session.SpeakerSetPosition.1")) - { - - } - else if (!stricmp(actionCstr, "Session.AudioSourceSetPosition.1")) - { - - } - else if (!stricmp(actionCstr, "Session.GetChannelParticipants.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelCreate.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelUpdate.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelDelete.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelCreateAndInvite.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelFolderCreate.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelFolderUpdate.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelFolderDelete.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelAddModerator.1")) - { - - } - else if (!stricmp(actionCstr, "Account.ChannelDeleteModerator.1")) - { - - } - */ - } -} - -LLVivoxSecurity::LLVivoxSecurity() -{ - // This size is an arbitrary choice; Vivox does not care - // Use a multiple of three so that there is no '=' padding in the base64 (purely an esthetic choice) - #define VIVOX_TOKEN_BYTES 9 - U8 random_value[VIVOX_TOKEN_BYTES]; - - for (int b = 0; b < VIVOX_TOKEN_BYTES; b++) - { - random_value[b] = ll_rand() & 0xff; - } - mConnectorHandle = LLBase64::encode(random_value, VIVOX_TOKEN_BYTES); - - for (int b = 0; b < VIVOX_TOKEN_BYTES; b++) - { - random_value[b] = ll_rand() & 0xff; - } - mAccountHandle = LLBase64::encode(random_value, VIVOX_TOKEN_BYTES); -} - -LLVivoxSecurity::~LLVivoxSecurity() -{ -} - -bool LLVivoxVoiceP2PIncomingCall::answerInvite() { return LLVivoxVoiceClient::getInstance()->answerInvite(mCallInfo["session_handle"]); } - -void LLVivoxVoiceP2PIncomingCall::declineInvite() { LLVivoxVoiceClient::getInstance()->declineInvite(mCallInfo["session_handle"]); } diff --git a/indra/newview/llvoicevivox.h b/indra/newview/llvoicevivox.h deleted file mode 100644 index 3167705528..0000000000 --- a/indra/newview/llvoicevivox.h +++ /dev/null @@ -1,1093 +0,0 @@ -/** - * @file llvoicevivox.h - * @brief Declaration of LLDiamondwareVoiceClient class which is the interface to the voice client process. - * - * $LicenseInfo:firstyear=2001&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ -#ifndef LL_VOICE_VIVOX_H -#define LL_VOICE_VIVOX_H - -class LLVOAvatar; -class LLVivoxProtocolParser; - -#include "lliopipe.h" -#include "llpumpio.h" -#include "llchainio.h" -#include "lliosocket.h" -#include "v3math.h" -#include "llframetimer.h" -#include "llviewerregion.h" -#include "llcallingcard.h" // for LLFriendObserver -#include "lleventcoro.h" -#include "llcoros.h" -#include - -#ifdef LL_USESYSTEMLIBS -# include "expat.h" -#else -# include "expat/expat.h" -#endif -#include "llvoiceclient.h" - -class LLAvatarName; -class LLVivoxVoiceClientMuteListObserver; - -extern const std::string VIVOX_VOICE_SERVER_TYPE; - -class LLVivoxVoiceP2PIncomingCall : public LLVoiceP2PIncomingCallInterface -{ - public: - LLVivoxVoiceP2PIncomingCall(const LLSD& call_info) : mCallInfo(call_info) {} - ~LLVivoxVoiceP2PIncomingCall() override {} - - bool answerInvite() override; - void declineInvite() override; - - protected: - LLSD mCallInfo; -}; - -class LLVivoxVoiceClient : public LLSingleton, - virtual public LLVoiceModuleInterface, - virtual public LLVoiceEffectInterface, - virtual public LLVoiceP2POutgoingCallInterface -{ - LLSINGLETON(LLVivoxVoiceClient); - LOG_CLASS(LLVivoxVoiceClient); - virtual ~LLVivoxVoiceClient(); - -public: - /// @name LLVoiceModuleInterface virtual implementations - /// @see LLVoiceModuleInterface - //@{ - void init(LLPumpIO *pump) override; // Call this once at application startup (creates connector) - void terminate() override; // Call this to clean up during shutdown - - const LLVoiceVersionInfo& getVersion() override; - - void updateSettings() override; // call after loading settings and whenever they change - - // Returns true if vivox has successfully logged in and is not in error state - bool isVoiceWorking() const override; - - void setHidden(bool hidden) override; // virtual - - ///////////////////// - /// @name Tuning - //@{ - void tuningStart() override; - void tuningStop() override; - bool inTuningMode() override; - - void tuningSetMicVolume(float volume) override; - void tuningSetSpeakerVolume(float volume) override; - float tuningGetEnergy(void) override; - - //@} - - ///////////////////// - /// @name Devices - //@{ - // This returns true when it's safe to bring up the "device settings" dialog in the prefs. - // i.e. when the daemon is running and connected, and the device lists are populated. - bool deviceSettingsAvailable() override; - bool deviceSettingsUpdated() override; //return if the list has been updated and never fetched, only to be called from the voicepanel. - - // Requery the vivox daemon for the current list of input/output devices. - // If you pass true for clearCurrentList, deviceSettingsAvailable() will be false until the query has completed - // (use this if you want to know when it's done). - // If you pass false, you'll have no way to know when the query finishes, but the device lists will not appear empty in the interim. - void refreshDeviceLists(bool clearCurrentList = true) override; - - void setCaptureDevice(const std::string& name) override; - void setRenderDevice(const std::string& name) override; - - LLVoiceDeviceList& getCaptureDevices() override; - LLVoiceDeviceList& getRenderDevices() override; - //@} - - void getParticipantList(std::set &participants) override; - bool isParticipant(const LLUUID& speaker_id) override; - - // Send a text message to the specified user, initiating the session if necessary. - // virtual bool sendTextMessage(const LLUUID& participant_id, const std::string& message) const {return false;}; - - // Returns true if calling back the session URI after the session has closed is possible. - // Currently this will be false only for PSTN P2P calls. - // NOTE: this will return true if the session can't be found. - bool isSessionCallBackPossible(const LLUUID &session_id) override; - - // Returns true if the session can accepte text IM's. - // Currently this will be false only for PSTN P2P calls. - // NOTE: this will return true if the session can't be found. - bool isSessionTextIMPossible(const LLUUID &session_id) override; - - //////////////////////////// - /// @name Channel stuff - //@{ - // returns true iff the user is currently in a proximal (local spatial) channel. - // Note that gestures should only fire if this returns true. - bool inProximalChannel() override; - - void setNonSpatialChannel(const LLSD& channelInfo, - bool notify_on_first_join, - bool hangup_on_last_leave) override; - - bool setSpatialChannel(const LLSD& channelInfo) override; - - void leaveNonSpatialChannel() override; - - void processChannels(bool process) override; - - void leaveChannel(void); - - bool isCurrentChannel(const LLSD &channelInfo) override; - bool compareChannels(const LLSD &channelInfo1, const LLSD &channelInfo2) override; - - //@} - - - ////////////////////////// - /// @name LLVoiceP2POutgoingCallInterface - //@{ - // start a voice channel with the specified user - void callUser(const LLUUID &uuid) override; - void hangup() override; - - //@} - - LLVoiceP2POutgoingCallInterface *getOutgoingCallInterface() override { return this; } - - LLVoiceP2PIncomingCallInterfacePtr getIncomingCallInterface(const LLSD &voice_call_info) override; - - bool answerInvite(const std::string &sessionHandle); - void declineInvite(const std::string &sessionHandle); - - ///////////////////////// - /// @name Volume/gain - //@{ - void setVoiceVolume(F32 volume) override; - void setMicGain(F32 volume) override; - //@} - - ///////////////////////// - /// @name enable disable voice and features - //@{ - void setVoiceEnabled(bool enabled) override; - void setMuteMic(bool muted) override; // Set the mute state of the local mic. - //@} - - ////////////////////////// - /// @name nearby speaker accessors - //@{ - std::string getDisplayName(const LLUUID& id) override; - bool isParticipantAvatar(const LLUUID &id) override; - bool getIsSpeaking(const LLUUID& id) override; - bool getIsModeratorMuted(const LLUUID& id) override; - F32 getCurrentPower(const LLUUID& id) override; // "power" is related to "amplitude" in a defined way. I'm just not sure what the formula is... - F32 getUserVolume(const LLUUID& id) override; - void setUserVolume(const LLUUID& id, F32 volume) override; // set's volume for specified agent, from 0-1 (where .5 is nominal) - //@} - - // authorize the user - void userAuthorized(const std::string& user_id, - const LLUUID &agentID) override; - - ////////////////////////////// - /// @name Status notification - //@{ - void addObserver(LLVoiceClientStatusObserver* observer) override; - void removeObserver(LLVoiceClientStatusObserver* observer) override; - void addObserver(LLFriendObserver* observer) override; - void removeObserver(LLFriendObserver* observer) override; - void addObserver(LLVoiceClientParticipantObserver* observer) override; - void removeObserver(LLVoiceClientParticipantObserver* observer) override; - //@} - - std::string sipURIFromID(const LLUUID &id) const override; - LLSD getP2PChannelInfoTemplate(const LLUUID& id) const override; - //@} - - /// @name LLVoiceEffectInterface virtual implementations - /// @see LLVoiceEffectInterface - //@{ - - ////////////////////////// - /// @name Accessors - //@{ - bool setVoiceEffect(const LLUUID& id) override; - const LLUUID getVoiceEffect() override; - LLSD getVoiceEffectProperties(const LLUUID& id) override; - - void refreshVoiceEffectLists(bool clear_lists) override; - const voice_effect_list_t& getVoiceEffectList() const override; - const voice_effect_list_t& getVoiceEffectTemplateList() const override; - //@} - - ////////////////////////////// - /// @name Status notification - //@{ - void addObserver(LLVoiceEffectObserver* observer) override; - void removeObserver(LLVoiceEffectObserver* observer) override; - //@} - - ////////////////////////////// - /// @name Effect preview buffer - //@{ - void enablePreviewBuffer(bool enable) override; - void recordPreviewBuffer() override; - void playPreviewBuffer(const LLUUID& effect_id = LLUUID::null) override; - void stopPreviewBuffer() override; - - bool isPreviewRecording() override; - bool isPreviewPlaying() override; - //@} - - //@} - - bool onCheckVoiceEffect(const std::string& voice_effect_name); - void onClickVoiceEffect(const std::string& voice_effect_name); - -protected: - ////////////////////// - // Vivox Specific definitions - - friend class LLVivoxVoiceClientMuteListObserver; - friend class LLVivoxVoiceClientFriendsObserver; - - - enum streamState - { - streamStateUnknown = 0, - streamStateIdle = 1, - streamStateConnected = 2, - streamStateRinging = 3, - streamStateConnecting = 6, // same as Vivox session_media_connecting enum - streamStateDisconnecting = 7, //Same as Vivox session_media_disconnecting enum - }; - - struct participantState - { - public: - participantState(const std::string &uri); - - bool updateMuteState(); // true if mute state has changed - bool isAvatar(); - - std::string mURI; - LLUUID mAvatarID; - std::string mAccountName; - std::string mDisplayName; - LLFrameTimer mSpeakingTimeout; - F32 mLastSpokeTimestamp; - F32 mPower; - F32 mVolume; - std::string mGroupID; - int mUserVolume; - bool mPTT; - bool mIsSpeaking; - bool mIsModeratorMuted; - bool mOnMuteList; // true if this avatar is on the user's mute list (and should be muted) - bool mVolumeSet; // true if incoming volume messages should not change the volume - bool mVolumeDirty; // true if this participant needs a volume command sent (either mOnMuteList or mUserVolume has changed) - bool mAvatarIDValid; - bool mIsSelf; - }; - typedef std::shared_ptr participantStatePtr_t; - typedef std::weak_ptr participantStateWptr_t; - - typedef std::map participantMap; - typedef std::map participantUUIDMap; - - struct sessionState - { - public: - typedef std::shared_ptr ptr_t; - typedef std::weak_ptr wptr_t; - - typedef std::function sessionFunc_t; - - static ptr_t createSession(); - ~sessionState(); - - LLSD getVoiceChannelInfo(); - - participantStatePtr_t addParticipant(const std::string &uri); - void removeParticipant(const participantStatePtr_t &participant); - void removeAllParticipants(); - - participantStatePtr_t findParticipant(const std::string &uri); - participantStatePtr_t findParticipantByID(const LLUUID& id); - - static ptr_t matchSessionByHandle(const std::string &handle); - static ptr_t matchCreatingSessionByURI(const std::string &uri); - static ptr_t matchSessionByURI(const std::string &uri); - static ptr_t matchSessionByParticipant(const LLUUID &participant_id); - - bool isCallBackPossible(); - bool isTextIMPossible(); - bool isSpatial() { return mIsSpatial; } - - static void for_each(sessionFunc_t func); - - std::string mHandle; - std::string mGroupHandle; - std::string mSIPURI; - std::string mAlias; - std::string mName; - std::string mAlternateSIPURI; - std::string mHash; // Channel password - std::string mErrorStatusString; - std::queue mTextMsgQueue; - - LLUUID mIMSessionID; - LLUUID mCallerID; - int mErrorStatusCode; - int mMediaStreamState; - bool mCreateInProgress; // True if a Session.Create has been sent for this session and no response has been received yet. - bool mMediaConnectInProgress; // True if a Session.MediaConnect has been sent for this session and no response has been received yet. - bool mVoiceInvitePending; // True if a voice invite is pending for this session (usually waiting on a name lookup) - bool mTextInvitePending; // True if a text invite is pending for this session (usually waiting on a name lookup) - bool mSynthesizedCallerID; // True if the caller ID is a hash of the SIP URI -- this means we shouldn't do a name lookup. - bool mIsChannel; // True for both group and spatial channels (false for p2p, PSTN) - bool mIsSpatial; // True for spatial channels - bool mIsP2P; - bool mIncoming; - bool mVoiceActive; - bool mReconnect; // Whether we should try to reconnect to this session if it's dropped - - // Set to true when the volume/mute state of someone in the participant list changes. - // The code will have to walk the list to find the changed participant(s). - bool mVolumeDirty; - bool mMuteDirty; - - bool mParticipantsChanged; - participantMap mParticipantsByURI; - participantUUIDMap mParticipantsByUUID; - - LLUUID mVoiceFontID; - - static void VerifySessions(); - - private: - sessionState(); - - static std::set> mSession; // canonical list of outstanding sessions. - std::set::iterator mMyIterator; // used for delete - - static void for_eachPredicate(const wptr_t &a, sessionFunc_t func); - - static bool testByHandle(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string handle); - static bool testByCreatingURI(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string uri); - static bool testBySIPOrAlterateURI(const LLVivoxVoiceClient::sessionState::wptr_t &a, std::string uri); - static bool testByCallerId(const LLVivoxVoiceClient::sessionState::wptr_t &a, LLUUID participantId); - - }; - typedef std::shared_ptr sessionStatePtr_t; - - typedef std::map sessionMap; - - /////////////////////////////////////////////////////// - // Private Member Functions - ////////////////////////////////////////////////////// - - - - ////////////////////////////// - /// @name TVC/Server management and communication - //@{ - // Call this if the connection to the daemon terminates unexpectedly. It will attempt to reset everything and relaunch. - void daemonDied(); - - // Call this if we're just giving up on voice (can't provision an account, etc.). It will clean up and go away. - void giveUp(); - - // write to the tvc - bool writeString(const std::string &str); - - void connectorCreate(); - void connectorShutdown(); - void closeSocket(void); - -// void requestVoiceAccountProvision(S32 retries = 3); - void setLoginInfo( - const std::string& account_name, - const std::string& password, - const std::string& voice_sip_uri_hostname, - const std::string& voice_account_server_uri); - void loginSendMessage(); - void logout(); - void logoutSendMessage(); - - - //@} - - //------------------------------------ - // tuning - - void tuningRenderStartSendMessage(const std::string& name, bool loop); - void tuningRenderStopSendMessage(); - - void tuningCaptureStartSendMessage(int duration); - void tuningCaptureStopSendMessage(); - - //---------------------------------- - // devices - void clearCaptureDevices(); - void addCaptureDevice(const LLVoiceDevice& device); - void clearRenderDevices(); - void setDevicesListUpdated(bool state); - void addRenderDevice(const LLVoiceDevice& device); - void buildSetAudioDevices(std::ostringstream &stream); - - void getCaptureDevicesSendMessage(); - void getRenderDevicesSendMessage(); - - // local audio updates, mic mute, speaker mute, mic volume and speaker volumes - void sendLocalAudioUpdates(); - - ///////////////////////////// - // Response/Event handlers - void connectorCreateResponse(int statusCode, std::string &statusString, std::string &connectorHandle, std::string &versionID); - void loginResponse(int statusCode, std::string &statusString, std::string &accountHandle, int numberOfAliases); - void sessionCreateResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle); - void sessionGroupAddSessionResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle); - void sessionConnectResponse(std::string &requestId, int statusCode, std::string &statusString); - void logoutResponse(int statusCode, std::string &statusString); - void connectorShutdownResponse(int statusCode, std::string &statusString); - - void accountLoginStateChangeEvent(std::string &accountHandle, int statusCode, std::string &statusString, int state); - void mediaCompletionEvent(std::string &sessionGroupHandle, std::string &mediaCompletionType); - void mediaStreamUpdatedEvent(std::string &sessionHandle, std::string &sessionGroupHandle, int statusCode, std::string &statusString, int state, bool incoming); - void sessionAddedEvent(std::string &uriString, std::string &alias, std::string &sessionHandle, std::string &sessionGroupHandle, bool isChannel, bool incoming, std::string &nameString, std::string &applicationString); - void sessionGroupAddedEvent(std::string &sessionGroupHandle); - void sessionRemovedEvent(std::string &sessionHandle, std::string &sessionGroupHandle); - void participantAddedEvent(std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, std::string &nameString, std::string &displayNameString, int participantType); - void participantRemovedEvent(std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, std::string &nameString); - void participantUpdatedEvent(std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, bool isModeratorMuted, bool isSpeaking, int volume, F32 energy); - void voiceServiceConnectionStateChangedEvent(int statusCode, std::string &statusString, std::string &build_id); - void auxAudioPropertiesEvent(F32 energy); - void messageEvent(std::string &sessionHandle, std::string &uriString, std::string &alias, std::string &messageHeader, std::string &messageBody, std::string &applicationString); - void sessionNotificationEvent(std::string &sessionHandle, std::string &uriString, std::string ¬ificationType); - - void muteListChanged(); - - ///////////////////////////// - // VAD changes - // disable auto-VAD and configure VAD parameters explicitly - void setupVADParams(unsigned int vad_auto, unsigned int vad_hangover, unsigned int vad_noise_floor, unsigned int vad_sensitivity); - void onVADSettingsChange(); - - ///////////////////////////// - // Sending updates of current state - void updatePosition(void); - void setCameraPosition(const LLVector3d &position, const LLVector3 &velocity, const LLMatrix3 &rot); - void setAvatarPosition(const LLVector3d &position, const LLVector3 &velocity, const LLQuaternion &rot); - bool channelFromRegion(LLViewerRegion *region, std::string &name); - - void setEarLocation(S32 loc); - - - ///////////////////////////// - // Accessors for data related to nearby speakers - - // MBW -- XXX -- Not sure how to get this data out of the TVC - bool getUsingPTT(const LLUUID& id); - std::string getGroupID(const LLUUID& id); // group ID if the user is in group chat (empty string if not applicable) - - ///////////////////////////// - // Recording controls - void recordingLoopStart(int seconds = 3600, int deltaFramesPerControlFrame = 200); - void recordingLoopSave(const std::string& filename); - void recordingStop(); - - // Playback controls - void filePlaybackStart(const std::string& filename); - void filePlaybackStop(); - void filePlaybackSetPaused(bool paused); - void filePlaybackSetMode(bool vox = false, float speed = 1.0f); - - participantStatePtr_t findParticipantByID(const LLUUID& id); - - -#if 0 - //////////////////////////////////////// - // voice sessions. - typedef std::set sessionSet; - - typedef sessionSet::iterator sessionIterator; - sessionIterator sessionsBegin(void); - sessionIterator sessionsEnd(void); -#endif - - sessionStatePtr_t findSession(const std::string &handle); - sessionStatePtr_t findSessionBeingCreatedByURI(const std::string &uri); - sessionStatePtr_t findSession(const LLUUID &participant_id); - - sessionStatePtr_t addSession(const std::string &uri, const std::string &handle = std::string()); - void clearSessionHandle(const sessionStatePtr_t &session); - void setSessionHandle(const sessionStatePtr_t &session, const std::string &handle); - void setSessionURI(const sessionStatePtr_t &session, const std::string &uri); - void deleteSession(const sessionStatePtr_t &session); - void deleteAllSessions(void); - - void verifySessionState(void); - - void joinedAudioSession(const sessionStatePtr_t &session); - void leftAudioSession(const sessionStatePtr_t &session); - - // This is called in several places where the session _may_ need to be deleted. - // It contains logic for whether to delete the session or keep it around. - void reapSession(const sessionStatePtr_t &session); - - // Returns true if the session seems to indicate we've moved to a region on a different voice server - bool sessionNeedsRelog(const sessionStatePtr_t &session); - - - ////////////////////////////////////// - // buddy list stuff, needed for SLIM later - struct buddyListEntry - { - buddyListEntry(const std::string &uri); - std::string mURI; - std::string mDisplayName; - LLUUID mUUID; - bool mOnlineSL; - bool mOnlineSLim; - bool mCanSeeMeOnline; - bool mHasBlockListEntry; - bool mHasAutoAcceptListEntry; - bool mNameResolved; - bool mInSLFriends; - bool mInVivoxBuddies; - }; - - typedef std::map buddyListMap; - - ///////////////////////////// - // session control messages - - void accountListBlockRulesSendMessage(); - void accountListAutoAcceptRulesSendMessage(); - - void sessionGroupCreateSendMessage(); - void sessionCreateSendMessage(const sessionStatePtr_t &session, bool startAudio = true, bool startText = false); - void sessionGroupAddSessionSendMessage(const sessionStatePtr_t &session, bool startAudio = true, bool startText = false); - void sessionMediaConnectSendMessage(const sessionStatePtr_t &session); // just joins the audio session - void sessionTextConnectSendMessage(const sessionStatePtr_t &session); // just joins the text session - void sessionTerminateSendMessage(const sessionStatePtr_t &session); - void sessionGroupTerminateSendMessage(const sessionStatePtr_t &session); - void sessionMediaDisconnectSendMessage(const sessionStatePtr_t &session); - // void sessionTextDisconnectSendMessage(sessionState *session); - - - - // Pokes the state machine to leave the audio session next time around. - void sessionTerminate(); - - // Pokes the state machine to shut down the connector and restart it. - void requestRelog(); - - // Does the actual work to get out of the audio session - void leaveAudioSession(); - - friend class LLVivoxVoiceClientCapResponder; - - - void lookupName(const LLUUID &id); - void onAvatarNameCache(const LLUUID& id, const LLAvatarName& av_name); - void avatarNameResolved(const LLUUID &id, const std::string &name); - static void predAvatarNameResolution(const LLVivoxVoiceClient::sessionStatePtr_t &session, LLUUID id, std::string name); - - boost::signals2::connection mAvatarNameCacheConnection; - - ///////////////////////////// - // Voice fonts - - void addVoiceFont(const S32 id, - const std::string &name, - const std::string &description, - const LLDate &expiration_date, - bool has_expired, - const S32 font_type, - const S32 font_status, - const bool template_font = false); - void accountGetSessionFontsResponse(int statusCode, const std::string &statusString); - void accountGetTemplateFontsResponse(int statusCode, const std::string &statusString); - -private: - - LLVoiceVersionInfo mVoiceVersion; - - // Coroutine support methods - //--- - void voiceControlCoro(); - void voiceControlStateMachine(S32 &coro_state); - - bool endAndDisconnectSession(); - - bool callbackEndDaemon(const LLSD& data); - bool startAndLaunchDaemon(); - bool provisionVoiceAccount(); - bool establishVoiceConnection(); - bool breakVoiceConnection(bool wait); - bool loginToVivox(); - void logoutOfVivox(bool wait); - bool retrieveVoiceFonts(); - - bool requestParcelVoiceInfo(); - - bool addAndJoinSession(const sessionStatePtr_t &nextSession); - bool terminateAudioSession(bool wait); - - bool waitForChannel(); - bool runSession(const sessionStatePtr_t &session); - - void recordingAndPlaybackMode(); - int voiceRecordBuffer(); - int voicePlaybackBuffer(); - - bool performMicTuning(); - //--- - /// Clean up objects created during a voice session. - void cleanUp(); - - bool mSessionTerminateRequested; - bool mRelogRequested; - // Number of times (in a row) "stateJoiningSession" case for spatial channel is reached in stateMachine(). - // The larger it is the greater is possibility there is a problem with connection to voice server. - // Introduced while fixing EXT-4313. - int mSpatialJoiningNum; - - static void idle(void *user_data); - - LLHost mDaemonHost; - LLSocket::ptr_t mSocket; - - // We should kill the voice daemon in case of connection alert - bool mTerminateDaemon; - - friend class LLVivoxProtocolParser; - - std::string mAccountName; - std::string mAccountPassword; - std::string mAccountDisplayName; - - bool mTuningMode; - float mTuningEnergy; - std::string mTuningAudioFile; - int mTuningMicVolume; - bool mTuningMicVolumeDirty; - int mTuningSpeakerVolume; - bool mTuningSpeakerVolumeDirty; - bool mDevicesListUpdated; // set to true when the device list has been updated - // and false when the panelvoicedevicesettings has queried for an update status. - - std::string mSpatialSessionURI; - std::string mSpatialSessionCredentials; - - std::string mMainSessionGroupHandle; // handle of the "main" session group. - - std::string mChannelName; // Name of the channel to be looked up - sessionStatePtr_t mAudioSession; // Session state for the current audio session - - sessionStatePtr_t mNextAudioSession; // Session state for the audio session we're trying to join - - S32 mCurrentParcelLocalID; // Used to detect parcel boundary crossings - std::string mCurrentRegionName; // Used to detect parcel boundary crossings - - bool mConnectorEstablished; // set by "Create Connector" response - bool mAccountLoggedIn; // set by login message - int mNumberOfAliases; - U32 mCommandCookie; - - std::string mVoiceAccountServerURI; - std::string mVoiceSIPURIHostName; - - int mLoginRetryCount; - - sessionMap mSessionsByHandle; // Active sessions, indexed by session handle. Sessions which are being initiated may not be in this map. -#if 0 - sessionSet mSessions; // All sessions, not indexed. This is the canonical session list. -#endif - - bool mBuddyListMapPopulated; - bool mBlockRulesListReceived; - bool mAutoAcceptRulesListReceived; - buddyListMap mBuddyListMap; - - LLVoiceDeviceList mCaptureDevices; - LLVoiceDeviceList mRenderDevices; - - std::string mCaptureDevice; - std::string mRenderDevice; - bool mCaptureDeviceDirty; - bool mRenderDeviceDirty; - - bool mIsInitialized; - bool mShutdownComplete; - - bool checkParcelChanged(bool update = false); - bool switchChannel(std::string uri = std::string(), bool spatial = true, bool no_reconnect = false, bool is_p2p = false, std::string hash = ""); - void joinSession(const sessionStatePtr_t &session); - - std::string nameFromID(const LLUUID &id) const; - bool IDFromName(const std::string name, LLUUID &uuid); - std::string sipURIFromAvatar(LLVOAvatar *avatar); - std::string sipURIFromName(std::string &name); - - // Returns the name portion of the SIP URI if the string looks vaguely like a SIP URI, or an empty string if not. - std::string nameFromsipURI(const std::string &uri); - - bool inSpatialChannel(void); - LLSD getAudioSessionChannelInfo(); - std::string getAudioSessionHandle(); - - void sendPositionAndVolumeUpdate(void); - - void sendCaptureAndRenderDevices(); - void buildSetCaptureDevice(std::ostringstream &stream); - void buildSetRenderDevice(std::ostringstream &stream); - - - void sendFriendsListUpdates(); - -#if 0 - // start a text IM session with the specified user - // This will be asynchronous, the session may be established at a future time. - sessionStatePtr_t startUserIMSession(const LLUUID& uuid); -#endif - - void enforceTether(void); - - bool mSpatialCoordsDirty; - - LLVector3d mCameraPosition; - LLVector3d mCameraRequestedPosition; - LLVector3 mCameraVelocity; - LLMatrix3 mCameraRot; - - LLVector3d mAvatarPosition; - LLVector3 mAvatarVelocity; - LLQuaternion mAvatarRot; - - bool mMuteMic; - bool mMuteMicDirty; - bool mHidden; //Set to true during teleport to hide the agent's position. - - // Set to true when the friends list is known to have changed. - bool mFriendsListDirty; - - enum - { - earLocCamera = 0, // ear at camera - earLocAvatar, // ear at avatar - earLocMixed // ear at avatar location/camera direction - }; - - S32 mEarLocation; - - bool mSpeakerVolumeDirty; - bool mSpeakerMuteDirty; - int mSpeakerVolume; - - int mMicVolume; - bool mMicVolumeDirty; - - bool mVoiceEnabled; - bool mProcessChannels; - bool mWriteInProgress; - std::string mWriteString; - size_t mWriteOffset; - - typedef std::set observer_set_t; - observer_set_t mParticipantObservers; - - void notifyParticipantObservers(); - - typedef std::set status_observer_set_t; - status_observer_set_t mStatusObservers; - - void notifyStatusObservers(LLVoiceClientStatusObserver::EStatusType status); - - typedef std::set friend_observer_set_t; - friend_observer_set_t mFriendObservers; - void notifyFriendObservers(); - - // Voice Fonts - - void expireVoiceFonts(); - void deleteVoiceFont(const LLUUID& id); - void deleteAllVoiceFonts(); - void deleteVoiceFontTemplates(); - - S32 getVoiceFontIndex(const LLUUID& id) const; - S32 getVoiceFontTemplateIndex(const LLUUID& id) const; - - void accountGetSessionFontsSendMessage(); - void accountGetTemplateFontsSendMessage(); - void sessionSetVoiceFontSendMessage(const sessionStatePtr_t &session); - - void updateVoiceMorphingMenu(); - void notifyVoiceFontObservers(); - - typedef enum e_voice_font_type - { - VOICE_FONT_TYPE_NONE = 0, - VOICE_FONT_TYPE_ROOT = 1, - VOICE_FONT_TYPE_USER = 2, - VOICE_FONT_TYPE_UNKNOWN - } EVoiceFontType; - - typedef enum e_voice_font_status - { - VOICE_FONT_STATUS_NONE = 0, - VOICE_FONT_STATUS_FREE = 1, - VOICE_FONT_STATUS_NOT_FREE = 2, - VOICE_FONT_STATUS_UNKNOWN - } EVoiceFontStatus; - - struct voiceFontEntry - { - voiceFontEntry(LLUUID& id); - ~voiceFontEntry(); - - LLUUID mID; - S32 mFontIndex; - std::string mName; - LLDate mExpirationDate; - S32 mFontType; - S32 mFontStatus; - bool mIsNew; - - LLFrameTimer mExpiryTimer; - }; - - bool mVoiceFontsReceived; - bool mVoiceFontsNew; - bool mVoiceFontListDirty; - voice_effect_list_t mVoiceFontList; - voice_effect_list_t mVoiceFontTemplateList; - - typedef std::map voice_font_map_t; - voice_font_map_t mVoiceFontMap; - voice_font_map_t mVoiceFontTemplateMap; - - typedef std::set voice_font_observer_set_t; - voice_font_observer_set_t mVoiceFontObservers; - - LLFrameTimer mVoiceFontExpiryTimer; - - - // Audio capture buffer - - void captureBufferRecordStartSendMessage(); - void captureBufferRecordStopSendMessage(); - void captureBufferPlayStartSendMessage(const LLUUID& voice_font_id = LLUUID::null); - void captureBufferPlayStopSendMessage(); - - bool mCaptureBufferMode; // Disconnected from voice channels while using the capture buffer. - bool mCaptureBufferRecording; // A voice sample is being captured. - bool mCaptureBufferRecorded; // A voice sample is captured in the buffer ready to play. - bool mCaptureBufferPlaying; // A voice sample is being played. - - LLTimer mCaptureTimer; - LLUUID mPreviewVoiceFont; - LLUUID mPreviewVoiceFontLast; - S32 mPlayRequestCount; - bool mIsInTuningMode; - bool mIsInChannel; - bool mIsJoiningSession; - bool mIsWaitingForFonts; - bool mIsLoggingIn; - bool mIsLoggedIn; - bool mIsProcessingChannels; - bool mIsCoroutineActive; - - // This variables can last longer than vivox in coroutines so we need them as static - static bool sShuttingDown; - static bool sConnected; - static LLPumpIO* sPump; - - LLEventMailDrop mVivoxPump; -}; - - -/** - * @class LLVivoxProtocolParser - * @brief This class helps construct new LLIOPipe specializations - * @see LLIOPipe - * - * THOROUGH_DESCRIPTION - */ -class LLVivoxProtocolParser : public LLIOPipe -{ - LOG_CLASS(LLVivoxProtocolParser); -public: - LLVivoxProtocolParser(); - virtual ~LLVivoxProtocolParser(); - -protected: - /* @name LLIOPipe virtual implementations - */ - //@{ - /** - * @brief Process the data in buffer - */ - virtual EStatus process_impl( - const LLChannelDescriptors& channels, - buffer_ptr_t& buffer, - bool& eos, - LLSD& context, - LLPumpIO* pump); - //@} - - std::string mInput; - - // Expat control members - XML_Parser parser; - int responseDepth; - bool ignoringTags; - bool isEvent; - int ignoreDepth; - - // Members for processing responses. The values are transient and only valid within a call to processResponse(). - int returnCode; - int statusCode; - std::string statusString; - std::string requestId; - std::string actionString; - std::string connectorHandle; - std::string versionID; - std::string mBuildID; - std::string accountHandle; - std::string sessionHandle; - std::string sessionGroupHandle; - std::string alias; - std::string applicationString; - - // Members for processing events. The values are transient and only valid within a call to processResponse(). - std::string eventTypeString; - int state; - std::string uriString; - bool isChannel; - bool incoming; - bool enabled; - std::string nameString; - std::string audioMediaString; - std::string deviceString; - std::string displayNameString; - int participantType; - bool isLocallyMuted; - bool isModeratorMuted; - bool isSpeaking; - int volume; - F32 energy; - std::string messageHeader; - std::string messageBody; - std::string notificationType; - bool hasText; - bool hasAudio; - bool hasVideo; - bool terminated; - std::string blockMask; - std::string presenceOnly; - std::string autoAcceptMask; - std::string autoAddAsBuddy; - int numberOfAliases; - std::string subscriptionHandle; - std::string subscriptionType; - S32 id; - std::string descriptionString; - LLDate expirationDate; - bool hasExpired; - S32 fontType; - S32 fontStatus; - std::string mediaCompletionType; - - // Members for processing text between tags - std::string textBuffer; - bool accumulateText; - - void reset(); - - void processResponse(std::string tag); - - static void XMLCALL ExpatStartTag(void *data, const char *el, const char **attr); - static void XMLCALL ExpatEndTag(void *data, const char *el); - static void XMLCALL ExpatCharHandler(void *data, const XML_Char *s, int len); - - void StartTag(const char *tag, const char **attr); - void EndTag(const char *tag); - void CharData(const char *buffer, int length); - LLDate expiryTimeStampToLLDate(const std::string& vivox_ts); - -}; - -class LLVivoxSecurity : public LLSingleton -{ - LLSINGLETON(LLVivoxSecurity); - virtual ~LLVivoxSecurity(); - - public: - std::string connectorHandle() { return mConnectorHandle; }; - std::string accountHandle() { return mAccountHandle; }; - - private: - std::string mConnectorHandle; - std::string mAccountHandle; -}; - -class LLVoiceVivoxStats : public LLSingleton -{ - LLSINGLETON(LLVoiceVivoxStats); - LOG_CLASS(LLVoiceVivoxStats); - virtual ~LLVoiceVivoxStats(); - - private: - F64SecondsImplicit mStartTime; - - U32 mConnectCycles; - - F64 mConnectTime; - U32 mConnectAttempts; - - F64 mProvisionTime; - U32 mProvisionAttempts; - - F64 mEstablishTime; - U32 mEstablishAttempts; - - public: - - void reset(); - void connectionAttemptStart(); - void connectionAttemptEnd(bool success); - void provisionAttemptStart(); - void provisionAttemptEnd(bool success); - void establishAttemptStart(); - void establishAttemptEnd(bool success); - LLSD read(); -}; - -#endif //LL_VIVOX_VOICE_CLIENT_H - diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 333046adce..59e634b130 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -2342,12 +2342,6 @@ void LLWebRTCVoiceClient::avatarNameResolved(const LLUUID &id, const std::string sessionState::for_each(boost::bind(predAvatarNameResolution, _1, id, name)); } -// Leftover from vivox PTSN -std::string LLWebRTCVoiceClient::sipURIFromID(const LLUUID& id) const -{ - return id.asString(); -} - LLSD LLWebRTCVoiceClient::getP2PChannelInfoTemplate(const LLUUID& id) const { return LLSD(); diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h index 8efbd1778f..be384f0de0 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -87,7 +87,6 @@ public: // Returns true if WebRTC has successfully logged in and is not in error state bool isVoiceWorking() const override; - std::string sipURIFromID(const LLUUID &id) const override; LLSD getP2PChannelInfoTemplate(const LLUUID& id) const override; void setHidden(bool hidden) override; // virtual diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index c33f4f140a..8e59c26316 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -576,14 +576,6 @@ class Windows_x86_64_Manifest(ViewerManifest): self.path_optional("vcruntime140_1.dll") self.path_optional("vcruntime140_threads.dll") - # SLVoice executable - with self.prefix(src=os.path.join(pkgdir, 'bin', 'release')): - self.path("SLVoice.exe") - - # Vivox libraries - self.path("vivoxsdk_x64.dll") - self.path("ortp_x64.dll") - # BugSplat if self.args.get('bugsplat'): self.path("BsSndRpt64.exe") @@ -1139,16 +1131,6 @@ class Darwin_x86_64_Manifest(ViewerManifest): # Need to get the llcommon dll from any of the build directories as well. libfile_parent = self.get_dst_prefix() dylibs=[] - # SLVoice executable - with self.prefix(src=os.path.join(pkgdir, 'bin', 'release')): - self.path("SLVoice") - - # Vivox libraries - for libfile in ( - 'libortp.dylib', - 'libvivoxsdk.dylib', - ): - self.path2basename(relpkgdir, libfile) # Discord social SDK if self.args['discord'] == 'ON': @@ -1475,7 +1457,6 @@ class Linux_i686_Manifest(LinuxManifest): self.path("libdirect-1.4.so.5*") self.path("libalut.so*") self.path("libopenal.so*") - self.path("libopenal.so", "libvivoxoal.so.1") # vivox's sdk expects this soname # KLUDGE: As of 2012-04-11, the 'fontconfig' package installs # libfontconfig.so.1.4.4, along with symlinks libfontconfig.so.1 # and libfontconfig.so. Before we added support for library-file @@ -1505,15 +1486,6 @@ class Linux_i686_Manifest(LinuxManifest): print("tcmalloc files not found, skipping") pass - # Vivox runtimes - with self.prefix(src=relpkgdir, dst="bin"): - self.path("SLVoice") - with self.prefix(src=relpkgdir, dst="lib"): - self.path("libortp.so") - self.path("libsndfile.so.1") - #self.path("libvivoxoal.so.1") # no - we'll re-use the viewer's own OpenAL lib - self.path("libvivoxsdk.so") - self.strip_binaries() -- cgit v1.3 From 0aa4373e355c03d79d1815f9c65fbceb60ead415 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 12 May 2026 14:48:02 -0700 Subject: Remove Vivox Licenses --- indra/newview/licenses-linux.txt | 177 -------------------------------------- indra/newview/licenses-mac.txt | 178 --------------------------------------- indra/newview/licenses-win32.txt | 178 --------------------------------------- 3 files changed, 533 deletions(-) (limited to 'indra') diff --git a/indra/newview/licenses-linux.txt b/indra/newview/licenses-linux.txt index e53ba94a36..a199ed410c 100644 --- a/indra/newview/licenses-linux.txt +++ b/indra/newview/licenses-linux.txt @@ -632,180 +632,3 @@ supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. -================= -Vivox SDK License -================= - -RSA Data Security, Inc. MD5 Message-Digest Algorithm - -Audio coding: Polycom(R) Siren14TM (ITU-T Rec. G.722.1 Annex C) - -Open Source Software Licensing -Each open source software component utilized by this product is subject to its own copyright and licensing terms, as listed below. - - -************************************************************* -************************************************************* - -/** - * OpenAL cross platform audio library - * Copyright (C) 1999-2000 by authors. - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * Or go to http://www.gnu.org/copyleft/lgpl.html - */ - -************************************************************* -************************************************************* -RTP code under Lesser General Public License - -/* - The oRTP library is an RTP (Realtime Transport Protocol - rfc3550) stack. - Copyright (C) 2001 Simon MORLAT simon.morlat@linphone.org - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -************************************************************ -************************************************************* - -/* - * The Vovida Software License, Version 1.0 - * - * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. The names "VOCAL", "Vovida Open Communication Application Library", - * and "Vovida Open Communication Application Library (VOCAL)" must - * not be used to endorse or promote products derived from this - * software without prior written permission. For written - * permission, please contact vocal@vovida.org. - * - * 4. Products derived from this software may not be called "VOCAL", nor - * may "VOCAL" appear in their name, without prior written - * permission of Vovida Networks, Inc. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND - * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA - * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES - * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * - * - * This software consists of voluntary contributions made by Vovida - * Networks, Inc. and many individuals on behalf of Vovida Networks, - * Inc. For more information on Vovida Networks, Inc., please see - * - * - */ -************************************************************* -************************************************************* - -Internet Software Consortium code - -/* This is from the BIND 4.9.4 release, modified to compile by itself */ -/* Copyright (c) 1996 by Internet Software Consortium. - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS - * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE - * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL - * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR - * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS - * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS - * SOFTWARE. - */ - -************************************************************* - -************************************************************* - -************************************************************ - -http://tinyxpath.sourceforge.net/ - -TinyXPath is covered by the zlib license : - - www.sourceforge.net/projects/tinyxpath - Copyright (c) 2002-2006 Yves Berquin (yvesb@users.sourceforge.net) - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any - damages arising from the use of this software. - - Permission is granted to anyone to use this software for any - purpose, including commercial applications, and to alter it and - redistribute it freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must - not claim that you wrote the original software. If you use this - software in a product, an acknowledgment in the product documentation - would be appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and - must not be misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source distribution. - - -************************************************************ -************************************************************ - -THE FREE SOFTWARE FOUNDATION - -Any customer may request the source code for all open source portions of this product which are covered by the Free Software Foundation's General Public License (GPL), for a period of three years from purchase. Please contact the vendor from whom you obtained this product for instructions. A fee equivalent to the cost of making the code available may be charged. Alternatively, customers may choose to download desired GPL components directly from their original vendors. Specifically, this product contains the following GPL-licensed components: - - -From Vivox: - - Assorted software components. To request source, contact Vivox at: - Vivox, Inc. - Attn: customer support - 40 Speen Street Suite 402 - Framingham, MA 01701 diff --git a/indra/newview/licenses-mac.txt b/indra/newview/licenses-mac.txt index 29b5a919bd..0bd9ebc5f1 100644 --- a/indra/newview/licenses-mac.txt +++ b/indra/newview/licenses-mac.txt @@ -516,184 +516,6 @@ supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. -================= -Vivox SDK License -================= - -RSA Data Security, Inc. MD5 Message-Digest Algorithm - -Audio coding: Polycom(R) Siren14TM (ITU-T Rec. G.722.1 Annex C) - -Open Source Software Licensing -Each open source software component utilized by this product is subject to its own copyright and licensing terms, as listed below. - - -************************************************************* -************************************************************* - -/** - * OpenAL cross platform audio library - * Copyright (C) 1999-2000 by authors. - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * Or go to http://www.gnu.org/copyleft/lgpl.html - */ - -************************************************************* -************************************************************* -RTP code under Lesser General Public License - -/* - The oRTP library is an RTP (Realtime Transport Protocol - rfc3550) stack. - Copyright (C) 2001 Simon MORLAT simon.morlat@linphone.org - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -************************************************************ -************************************************************* - -/* - * The Vovida Software License, Version 1.0 - * - * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. The names "VOCAL", "Vovida Open Communication Application Library", - * and "Vovida Open Communication Application Library (VOCAL)" must - * not be used to endorse or promote products derived from this - * software without prior written permission. For written - * permission, please contact vocal@vovida.org. - * - * 4. Products derived from this software may not be called "VOCAL", nor - * may "VOCAL" appear in their name, without prior written - * permission of Vovida Networks, Inc. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND - * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA - * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES - * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * - * - * This software consists of voluntary contributions made by Vovida - * Networks, Inc. and many individuals on behalf of Vovida Networks, - * Inc. For more information on Vovida Networks, Inc., please see - * - * - */ -************************************************************* -************************************************************* - -Internet Software Consortium code - -/* This is from the BIND 4.9.4 release, modified to compile by itself */ -/* Copyright (c) 1996 by Internet Software Consortium. - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS - * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE - * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL - * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR - * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS - * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS - * SOFTWARE. - */ - -************************************************************* - -************************************************************* - -************************************************************ - -http://tinyxpath.sourceforge.net/ - -TinyXPath is covered by the zlib license : - - www.sourceforge.net/projects/tinyxpath - Copyright (c) 2002-2006 Yves Berquin (yvesb@users.sourceforge.net) - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any - damages arising from the use of this software. - - Permission is granted to anyone to use this software for any - purpose, including commercial applications, and to alter it and - redistribute it freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must - not claim that you wrote the original software. If you use this - software in a product, an acknowledgment in the product documentation - would be appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and - must not be misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source distribution. - - -************************************************************ -************************************************************ - -THE FREE SOFTWARE FOUNDATION - -Any customer may request the source code for all open source portions of this product which are covered by the Free Software Foundation's General Public License (GPL), for a period of three years from purchase. Please contact the vendor from whom you obtained this product for instructions. A fee equivalent to the cost of making the code available may be charged. Alternatively, customers may choose to download desired GPL components directly from their original vendors. Specifically, this product contains the following GPL-licensed components: - - -From Vivox: - - Assorted software components. To request source, contact Vivox at: - Vivox, Inc. - Attn: customer support - 40 Speen Street Suite 402 - Framingham, MA 01701 - ============= meshoptimizer diff --git a/indra/newview/licenses-win32.txt b/indra/newview/licenses-win32.txt index eddc9a4475..b56f1bf2eb 100644 --- a/indra/newview/licenses-win32.txt +++ b/indra/newview/licenses-win32.txt @@ -558,184 +558,6 @@ supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. -================= -Vivox SDK License -================= - -RSA Data Security, Inc. MD5 Message-Digest Algorithm - -Audio coding: Polycom(R) Siren14TM (ITU-T Rec. G.722.1 Annex C) - -Open Source Software Licensing -Each open source software component utilized by this product is subject to its own copyright and licensing terms, as listed below. - - -************************************************************* -************************************************************* - -/** - * OpenAL cross platform audio library - * Copyright (C) 1999-2000 by authors. - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * Or go to http://www.gnu.org/copyleft/lgpl.html - */ - -************************************************************* -************************************************************* -RTP code under Lesser General Public License - -/* - The oRTP library is an RTP (Realtime Transport Protocol - rfc3550) stack. - Copyright (C) 2001 Simon MORLAT simon.morlat@linphone.org - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -************************************************************ -************************************************************* - -/* - * The Vovida Software License, Version 1.0 - * - * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. The names "VOCAL", "Vovida Open Communication Application Library", - * and "Vovida Open Communication Application Library (VOCAL)" must - * not be used to endorse or promote products derived from this - * software without prior written permission. For written - * permission, please contact vocal@vovida.org. - * - * 4. Products derived from this software may not be called "VOCAL", nor - * may "VOCAL" appear in their name, without prior written - * permission of Vovida Networks, Inc. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND - * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA - * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES - * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * - * - * This software consists of voluntary contributions made by Vovida - * Networks, Inc. and many individuals on behalf of Vovida Networks, - * Inc. For more information on Vovida Networks, Inc., please see - * - * - */ -************************************************************* -************************************************************* - -Internet Software Consortium code - -/* This is from the BIND 4.9.4 release, modified to compile by itself */ -/* Copyright (c) 1996 by Internet Software Consortium. - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS - * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE - * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL - * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR - * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS - * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS - * SOFTWARE. - */ - -************************************************************* - -************************************************************* - -************************************************************ - -http://tinyxpath.sourceforge.net/ - -TinyXPath is covered by the zlib license : - - www.sourceforge.net/projects/tinyxpath - Copyright (c) 2002-2006 Yves Berquin (yvesb@users.sourceforge.net) - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any - damages arising from the use of this software. - - Permission is granted to anyone to use this software for any - purpose, including commercial applications, and to alter it and - redistribute it freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must - not claim that you wrote the original software. If you use this - software in a product, an acknowledgment in the product documentation - would be appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and - must not be misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source distribution. - - -************************************************************ -************************************************************ - -THE FREE SOFTWARE FOUNDATION - -Any customer may request the source code for all open source portions of this product which are covered by the Free Software Foundation's General Public License (GPL), for a period of three years from purchase. Please contact the vendor from whom you obtained this product for instructions. A fee equivalent to the cost of making the code available may be charged. Alternatively, customers may choose to download desired GPL components directly from their original vendors. Specifically, this product contains the following GPL-licensed components: - - -From Vivox: - - Assorted software components. To request source, contact Vivox at: - Vivox, Inc. - Attn: customer support - 40 Speen Street Suite 402 - Framingham, MA 01701 - ======================== google-perftools license -- cgit v1.3 From 82db4943e011fc65e9a0b87990abb49b898a7782 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 12 May 2026 20:23:44 -0400 Subject: Rework texture streaming and tracking. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a big one: - Reworks the discard signal almost entirely. Now has a normalized 0..1 discard signal: distance x size x channel exponent, floored by staleness and background app state. Shaped by VRAM pressure. - Textures can now scale down to the smallest GPU mip (1×1), independent of the codec's encoded mip count. - Terrain texture LOD now works. Useful for 2K textures and PBR on terrain. Based upon camera distance to nearest terrain patch. - New texture quality setting. Low/Medium/High/Ultra - Caps texture resolution on Low to 1024, and otherwise shifts the discard signal around. Makes distance based texture LOD work a lot more predictably. - We now track last bind state for textures, and discard accordingly. We progressively discard based upon last bind time. - Avatar textures get a residency boost to stay loaded in VRAM longer under pressure. --- indra/llrender/llimagegl.cpp | 84 ++++++++-- indra/llrender/llimagegl.h | 16 +- indra/llrender/llrender.cpp | 14 ++ indra/newview/app_settings/settings.xml | 144 +++++++++++++++-- indra/newview/llface.cpp | 30 +++- indra/newview/llface.h | 7 +- indra/newview/llsurface.cpp | 14 +- indra/newview/llsurface.h | 6 + indra/newview/lltexturefetch.cpp | 12 +- indra/newview/lltexturefetch.h | 3 +- indra/newview/llviewercontrol.cpp | 21 ++- indra/newview/llviewertexture.cpp | 276 +++++++++++++++++++++++--------- indra/newview/llviewertexture.h | 50 +++++- indra/newview/llviewertexturelist.cpp | 176 ++++++++++++++------ indra/newview/llviewertexturelist.h | 5 + indra/newview/llvlcomposition.cpp | 18 +-- 16 files changed, 702 insertions(+), 174 deletions(-) (limited to 'indra') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ff..d79d13dc8b 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -66,8 +66,8 @@ static LLMutex sTexMemMutex; static std::unordered_map sTextureAllocs; static U64 sTextureBytes = 0; -// track a texture alloc on the currently bound texture. -// asserts that no currently tracked alloc exists +// Per-mip upload paths call this once per level; only free_tex_image +// removes a texture's accounting entirely. void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count) { U32 texUnit = gGL.getCurrentTexUnitIndex(); @@ -80,15 +80,46 @@ void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 sTexMemMutex.lock(); - // it is a precondition that no existing allocation exists for this texture - llassert(sTextureAllocs.find(texName) == sTextureAllocs.end()); - - sTextureAllocs[texName] = size; + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += size; + } + else + { + sTextureAllocs[texName] = size; + } sTextureBytes += size; sTexMemMutex.unlock(); } +// Add mip 1..N bytes to existing accounting. Use after glGenerateMipmap. +void LLImageGLMemory::account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat) +{ + U64 extra = 0; + U32 w = base_width; + U32 h = base_height; + while (w > 1 || h > 1) + { + w = w > 1 ? w >> 1 : 1; + h = h > 1 ? h >> 1 : 1; + extra += LLImageGL::dataFormatBytes(intformat, w, h); + } + + U32 texUnit = gGL.getCurrentTexUnitIndex(); + U32 texName = gGL.getTexUnit(texUnit)->getCurrTexture(); + + sTexMemMutex.lock(); + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += extra; + sTextureBytes += extra; + } + sTexMemMutex.unlock(); +} + // track texture free on given texName void LLImageGLMemory::free_tex_image(U32 texName) { @@ -838,7 +869,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 mMipLevels = wpo2(llmax(w, h)); //use legacy mipmap generation mode (note: making this condional can cause rendering issues) - // -- but making it not conditional triggers deprecation warnings when core profile is enabled + // - but making it not conditional triggers deprecation warnings when core profile is enabled // (some rendering issues while core profile is enabled are acceptable at this point in time) if (!LLRender::sGLCoreProfile) { @@ -864,6 +895,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 { LL_PROFILE_GPU_ZONE("generate mip map"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(w, h, mFormatInternal); } stop_glerror(); } @@ -1461,7 +1493,12 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt LL_PROFILE_ZONE_NUM(width); LL_PROFILE_ZONE_NUM(height); - free_cur_tex_image(); + // Release prior accounting only on the base mip; per-mip iteration + // accumulates the rest via the additive alloc_tex_image. + if (miplevel == 0) + { + free_cur_tex_image(); + } const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) { @@ -2039,6 +2076,28 @@ S32 LLImageGL::getWidth(S32 discard_level) const return width; } +// static +S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height) +{ + if (width <= 0 || height <= 0) + { + return 0; + } + // max(w,h) - min() caps short on rectangular textures + // (1024x512 reaches 1x1 at discard 10, not 9). + return (S32)floorf(log2f((F32)llmax(width, height))); +} + +void LLImageGL::stampBound() const +{ + // Skip the store on same-frame re-binds - bindFast is per-draw and + // would dirty this cache line per bind per texture otherwise. + if (mLastBindTime != sLastFrameTime) + { + mLastBindTime = sLastFrameTime; + } +} + S64 LLImageGL::getBytes(S32 discard_level) const { if (discard_level < 0) @@ -2468,7 +2527,12 @@ bool LLImageGL::scaleDown(S32 desired_discard) return false; } - desired_discard = llmin(desired_discard, mMaxDiscardLevel); + // GL pyramid reaches 1x1 regardless of codec levels; + // mMaxDiscardLevel is hardcapped at MAX_DISCARD_LEVEL. + S32 dim_max_discard = (mWidth > 0 && mHeight > 0) + ? dimDerivedMaxDiscard(mWidth, mHeight) + : (S32)mMaxDiscardLevel; + desired_discard = llmin(desired_discard, dim_max_discard); if (desired_discard <= mCurrentDiscardLevel) { @@ -2501,6 +2565,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); gGL.getTexUnit(0)->bind(this); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); } } @@ -2546,6 +2611,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) { LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); } gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e..a02c320738 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -51,6 +51,11 @@ class LLWindow; namespace LLImageGLMemory { void alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count); + + // Add mip 1..N bytes to existing accounting. Call after glGenerateMipmap + // when only the base mip was accounted; without this the bytes counter + // undercounts mipmap-generated textures by ~25%. + void account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat); void free_tex_image(U32 texName); void free_tex_images(U32 count, const U32* texNames); void free_cur_tex_image(); @@ -151,6 +156,15 @@ public: S32 getDiscardLevel() const { return mCurrentDiscardLevel; } S32 getMaxDiscardLevel() const { return mMaxDiscardLevel; } + // floor(log2(max(w, h))) - deepest GL pyramid level (down to 1x1). + // Returns 0 for non-positive inputs. + static S32 dimDerivedMaxDiscard(S32 width, S32 height); + + // Record the wall-clock bind time - every bind path that touches a + // streaming-managed texture must call this, or the staleness signal + // sees the texture as never-bound and ramps it toward eviction. + void stampBound() const; + // override the current discard level // should only be used for local textures where you know exactly what you're doing void setDiscardLevel(S32 level) { mCurrentDiscardLevel = level; } @@ -224,7 +238,7 @@ public: public: // Various GL/Rendering options S64Bytes mTextureMemory; - mutable F32 mLastBindTime; // last time this was bound, by discard level + mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives streaming staleness private: U32 createPickMask(S32 pWidth, S32 pHeight); diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 1a3a499b20..696a0d145f 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -197,6 +197,9 @@ void LLTexUnit::bindFast(LLTexture* texture) glActiveTexture(GL_TEXTURE0 + mIndex); gGL.mCurrTextureUnitIndex = mIndex; mCurrTexture = gl_tex->getTexName(); + // bindFast bypasses updateBindStats(); stamp directly so the staleness + // signal sees per-frame use of batched textures. + gl_tex->stampBound(); if (!mCurrTexture) { LL_PROFILE_ZONE_NAMED("MISSING TEXTURE"); @@ -249,11 +252,17 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind) setTextureFilteringOption(gl_tex->mFilterOption); } } + else + { + // Already current - still being used, keep it fresh. + gl_tex->stampBound(); + } } else { //if deleted, will re-generate it immediately texture->forceImmediateUpdate() ; + gl_tex->stampBound(); gl_tex->forceUpdateBindStats() ; return texture->bindDefaultImage(mIndex); @@ -325,6 +334,11 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 stop_glerror(); } } + else + { + // Already current - still being used, keep it fresh. + texture->stampBound(); + } stop_glerror(); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 9de8892c5e..0f1b704dc9 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -9221,7 +9221,7 @@ RenderReflectionDetail Comment - DEPRECATED -- use RenderTransparentWater and RenderReflectionProbeDetail -- Detail of reflection render pass. + DEPRECATED - use RenderTransparentWater and RenderReflectionProbeDetail - Detail of reflection render pass. Persist 1 Type @@ -11819,30 +11819,152 @@ TextureChannelPriority Comment - Per-channel texture streaming aggressiveness. X=normals, Y=diffuse, Z=specular/metallic, W=emissive. 1.0=baseline, higher=more aggressive downrez. + Per-channel exponent on the combined discard factor. X=normals, Y=diffuse, Z=spec, W=emissive. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. Persist 1 Type Vector4 Value - 5 - 7.5 - 20 - 7.5 + 1.0 + 0.75 + 0.5 + 0.75 - TextureCameraBoost + TextureMaxDiscardOverride Comment - Amount to boost resolution of textures that are important to the camera. + When non-zero, overrides the per-texture codec-derived max discard cap. 0 = use codec-reported levels. Higher lets the streaming math push past the codec ceiling; scaleDown handles the GL side. Persist + 1 + Type + S32 + Value 0 + + TextureMemoryPressureRampRate + + Comment + Feedback rate (per second) for the VRAM-pressure factor (0..1). Higher = faster convergence on the budget; lower = gentler. + Persist + 1 Type F32 Value - 8.0 + 3.0 + + TextureMemoryPressureBackoffStart + + Comment + Fraction of the VRAM target at which the pressure ramp starts (0..1). Lower = earlier headroom-building; 1.0 disables backoff (ramp only above target). + Persist + 1 + Type + F32 + Value + 0.85 + + TextureTerrainDistanceFloor + + Comment + Minimum distance factor for BOOST_TERRAIN textures. Keeps combined > 0 so VRAM pressure can evict terrain. Lower = higher idle quality, less pressure response. + Persist + 1 + Type + F32 + Value + 0.01 + + TextureTerrainCoverageFraction + + Comment + Synthetic on-screen coverage fraction for BOOST_TERRAIN textures (no faces are registered). Higher = higher idle quality, less pressure response. + Persist + 1 + Type + F32 + Value + 0.99 + + TextureAgentAvatarBoost + + Comment + Quality boost (0..1) for textures on the agent's avatar (rigged mesh / animated objects). Lower = higher quality. Preference, not exemption - pressure can still evict. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureBackgroundFactorRatePerSec + + Comment + Per-second ramp rate of the background-window discard floor (0..1). Snaps to 0 in foreground. Default 0.011 ~ 90s to saturate. + Persist + 1 + Type + F32 + Value + 0.011 + + TextureDistanceDiscardPower + + Comment + Exponent on the distance factor (face_distance / draw_distance). 1.0 = linear; lower = textures hit max discard sooner with distance. Default 0.5 = sqrt. + Persist + 1 + Type + F32 + Value + 0.5 + TextureSizeDiscardPower + + Comment + Exponent on the on-screen size factor (1 - coverage). 1.0 = linear; lower = small-on-screen textures attenuate sooner. + Persist + 1 + Type + F32 + Value + 1.0 + + TextureStalenessIntervalSeconds + + Comment + Seconds per staleness step. Per-interval increment is 1/max_discard so any texture saturates after interval * max_discard seconds idle. + Persist + 1 + Type + F32 + Value + 5.0 + + TextureBindDecaySeconds + + Comment + Grace seconds after a bind during which the staleness factor stays at 0. Prevents intermittently-bound textures from ramping. 0 disables the grace period. + Persist + 1 + Type + F32 + Value + 5.0 + + TextureFetchPressureScale + + Comment + Pending-fetch divisor for the bias floor: bias floor = 1 + clamp(pending / scale, 0, 3). Lower = bias rises sooner under load floods. + Persist + 1 + Type + F32 + Value + 1000.0 + + TextureDecodeDisabled Comment @@ -16383,7 +16505,7 @@ EmulateCoreCount Comment - For debugging -- number of cores to restrict the main process to, or 0 for no limit. Requires restart. + For debugging - number of cores to restrict the main process to, or 0 for no limit. Requires restart. Persist 1 Type @@ -16537,7 +16659,7 @@ MultiModeDoubleClickFolder Comment - Sets the action for Double-click on folder in multi-folder view (0 - expands and collapses folder, 1 - opens a new window, 2 – stays in current floater but switches to SFV) + Sets the action for Double-click on folder in multi-folder view (0 - expands and collapses folder, 1 - opens a new window, 2 - stays in current floater but switches to SFV) Persist 1 Type diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index 018d4c4bba..e34bea63ef 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -1577,7 +1577,7 @@ bool LLFace::getGeometryVolume(const LLVolume& volume, skin = mSkinInfo; } - //TODO -- cache this (check profile marker above)? + //TODO - cache this (check profile marker above)? glm::mat4 m = glm::make_mat4((F32*)skin->mBindShapeMatrix.getF32ptr()); m = glm::transpose(glm::inverse(m)); mat_normal.loadu(glm::value_ptr(m)); @@ -2265,7 +2265,18 @@ F32 LLFace::getTextureVirtualSize() face_area = mPixelArea / llclamp(texel_area, 0.015625f, 128.f); } - face_area = LLFace::adjustPixelArea(mImportanceToCamera, face_area); + // Diffuse source area as the dim-aware hint for adjustPixelArea. + S32 source_area = 0; + if (mTexture[LLRender::DIFFUSE_MAP].notNull()) + { + S32 sw = mTexture[LLRender::DIFFUSE_MAP]->getFullWidth(); + S32 sh = mTexture[LLRender::DIFFUSE_MAP]->getFullHeight(); + if (sw > 0 && sh > 0) + { + source_area = sw * sh; + } + } + face_area = LLFace::adjustPixelArea(mImportanceToCamera, face_area, source_area); if(face_area > LLViewerTexture::sMinLargeImageSize) //if is large image, shrink face_area by considering the partial overlapping. { if(mImportanceToCamera > LEAST_IMPORTANCE_FOR_LARGE_IMAGE && mTexture[LLRender::DIFFUSE_MAP].notNull() && mTexture[LLRender::DIFFUSE_MAP]->isLargeImage()) @@ -2389,6 +2400,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) F32 dist = lookAt.getLength3().getF32(); dist = llmax(dist-size.getLength3().getF32(), 0.001f); + mDistanceToCamera = dist; lookAt.normalize3fast() ; @@ -2513,8 +2525,16 @@ F32 LLFace::calcImportanceToCamera(F32 cos_angle_to_view_dir, F32 dist) } //static -F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area) +F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area, S32 source_area) { + // Dim-aware floor: source_area/256 lowers the "large but unimportant" + // clamp proportionally so smaller sources can be pushed past discard 4. + F32 large_floor = (F32)LLViewerTexture::sMinLargeImageSize; + if (source_area > 0) + { + large_floor = llmin(large_floor, (F32)source_area / 256.f); + } + if(pixel_area > LLViewerTexture::sMaxSmallImageSize) { if(importance < LEAST_IMPORTANCE) //if the face is not important, do not load hi-res. @@ -2522,11 +2542,11 @@ F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area) static const F32 MAX_LEAST_IMPORTANCE_IMAGE_SIZE = 128.0f * 128.0f ; pixel_area = llmin(pixel_area * 0.5f, MAX_LEAST_IMPORTANCE_IMAGE_SIZE) ; } - else if(pixel_area > LLViewerTexture::sMinLargeImageSize) //if is large image, shrink face_area by considering the partial overlapping. + else if(pixel_area > large_floor) //if is large image, shrink face_area by considering the partial overlapping. { if(importance < LEAST_IMPORTANCE_FOR_LARGE_IMAGE)//if the face is not important, do not load hi-res. { - pixel_area = (F32)LLViewerTexture::sMinLargeImageSize ; + pixel_area = large_floor ; } } } diff --git a/indra/newview/llface.h b/indra/newview/llface.h index 6e9d23c3a2..71ba3d0f2f 100644 --- a/indra/newview/llface.h +++ b/indra/newview/llface.h @@ -242,7 +242,9 @@ private: bool calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) ; public: static F32 calcImportanceToCamera(F32 to_view_dir, F32 dist); - static F32 adjustPixelArea(F32 importance, F32 pixel_area) ; + // source_area > 0 lowers the "large but unimportant" floor for + // moderate sources; 0 keeps the legacy sMinLargeImageSize floor. + static F32 adjustPixelArea(F32 importance, F32 pixel_area, S32 source_area = 0) ; public: @@ -251,6 +253,9 @@ public: LLVector2 mTexExtents[2]; F32 mDistance; + // Camera-to-face distance, cached by calcPixelArea; read by the + // streaming math's distance signal. + F32 mDistanceToCamera = 0.f; F32 mLastUpdateTime; F32 mLastSkinTime; F32 mLastMoveTime; diff --git a/indra/newview/llsurface.cpp b/indra/newview/llsurface.cpp index 64359b6cbe..fe77d585c8 100644 --- a/indra/newview/llsurface.cpp +++ b/indra/newview/llsurface.cpp @@ -57,6 +57,8 @@ namespace LLColor4U MAX_WATER_COLOR(0, 48, 96, 240); S32 LLSurface::sTextureSize = 256; +F32 LLSurface::sNearestVisiblePatchDistance = FLT_MAX; +U32 LLSurface::sNearestVisiblePatchFrame = 0; // ---------------- LLSurface:: Public Members --------------- @@ -122,7 +124,7 @@ LLSurface::~LLSurface() else if (poolp->mReferences.empty()) { gPipeline.removePool(poolp); - // Don't enable this until we blitz the draw pool for it as well. -- djs + // Don't enable this until we blitz the draw pool for it as well. - djs mSTexturep = nullptr; } else @@ -583,6 +585,13 @@ void LLSurface::updatePatchVisibilities(LLAgent &agent) LLSurfacePatch *patchp; + // Reset the cross-region accumulator at the start of each frame. + if (sNearestVisiblePatchFrame != gFrameCount) + { + sNearestVisiblePatchDistance = FLT_MAX; + sNearestVisiblePatchFrame = gFrameCount; + } + mVisiblePatchCount = 0; for (S32 i=0; iupdateCameraDistanceRegion(pos_region); + sNearestVisiblePatchDistance = llmin(sNearestVisiblePatchDistance, patchp->getDistance()); } } } @@ -961,7 +971,7 @@ std::ostream& operator<<(std::ostream &s, const LLSurface &S) void LLSurface::createPatchData() { // Assumes mGridsPerEdge, mGridsPerPatchEdge, and mPatchesPerEdge have been properly set - // TODO -- check for create() called when surface is not empty + // TODO - check for create() called when surface is not empty S32 i, j; LLSurfacePatch *patchp; diff --git a/indra/newview/llsurface.h b/indra/newview/llsurface.h index a599019ca5..4bdb90b102 100644 --- a/indra/newview/llsurface.h +++ b/indra/newview/llsurface.h @@ -163,6 +163,12 @@ public: F32 mDetailTextureScale; // Number of times to repeat detail texture across this surface + // Closest visible patch distance across all surfaces this frame + // (meters), or FLT_MAX if none visible. Drives BOOST_TERRAIN + // streaming - terrain has no faces registered with its texture. + static F32 sNearestVisiblePatchDistance; + static U32 sNearestVisiblePatchFrame; + private: void createSTexture(); void initTextures(); diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp index 51ade60827..574c200eb4 100644 --- a/indra/newview/lltexturefetch.cpp +++ b/indra/newview/lltexturefetch.cpp @@ -542,6 +542,7 @@ private: S32 mRequestedDiscard; S32 mLoadedDiscard; S32 mDecodedDiscard; + S32 mCodecLevels = 0; LLFrameTimer mRequestedDeltaTimer; LLFrameTimer mFetchDeltaTimer; LLTimer mCacheReadTimer; @@ -1843,6 +1844,10 @@ bool LLTextureFetchWorker::doWork(S32 param) else { llassert_always(mRawImage.notNull()); + if (mFormattedImage.notNull()) + { + mCodecLevels = (S32)mFormattedImage->getLevels(); + } LL_DEBUGS(LOG_TXT) << mID << ": Decoded. Discard: " << mDecodedDiscard << " Raw Image: " << llformat("%dx%d",mRawImage->getWidth(),mRawImage->getHeight()) << LL_ENDL; setState(WRITE_TO_CACHE); @@ -2774,10 +2779,12 @@ LLTextureFetchWorker* LLTextureFetch::getWorker(const LLUUID& id) // Threads: T* bool LLTextureFetch::getRequestFinished(const LLUUID& id, S32& discard_level, S32& worker_state, LLPointer& raw, LLPointer& aux, - LLCore::HttpStatus& last_http_get_status) + LLCore::HttpStatus& last_http_get_status, + S32& codec_levels) { LL_PROFILE_ZONE_SCOPED; bool res = false; + codec_levels = 0; LLTextureFetchWorker* worker = getWorker(id); if (worker) { @@ -2809,6 +2816,9 @@ bool LLTextureFetch::getRequestFinished(const LLUUID& id, S32& discard_level, S3 discard_level = worker->mDecodedDiscard; raw = worker->mRawImage; aux = worker->mAuxImage; + // Cached on the worker so the value survives mFormattedImage + // clears (cache-retry, decode-abort, write-to-cache complete). + codec_levels = worker->mCodecLevels; decode_time = worker->mDecodeTime; fetch_time = worker->mFetchTime; diff --git a/indra/newview/lltexturefetch.h b/indra/newview/lltexturefetch.h index 851d6c11a0..d75e16ab7c 100644 --- a/indra/newview/lltexturefetch.h +++ b/indra/newview/lltexturefetch.h @@ -106,7 +106,8 @@ public: // keep in mind that if fetcher isn't done, it still might need original raw image bool getRequestFinished(const LLUUID& id, S32& discard_level, S32& worker_state, LLPointer& raw, LLPointer& aux, - LLCore::HttpStatus& last_http_get_status); + LLCore::HttpStatus& last_http_get_status, + S32& codec_levels); // Threads: T* bool updateRequestPriority(const LLUUID& id, F32 priority); diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index d4d07f24ea..3a6e4e2e2d 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -114,32 +114,37 @@ static bool handleRenderAvatarMouselookChanged(const LLSD& newvalue) static bool handleRenderTextureQualityChanged(const LLSD& newvalue) { - // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives max-resolution and per-channel - // streaming aggressiveness. Channel order is X=normals, Y=diffuse, - // Z=specular/metallic, W=emissive (matches TextureChannelPriority). + // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives max-resolution and the + // per-channel TextureChannelPriority + TextureDistanceDiscardPower + // exponents. Channel order: X=normals, Y=diffuse, Z=spec, W=emissive. U32 quality = (U32)newvalue.asInteger(); U32 max_res = 2048; - LLVector4 channel_priority(5.f, 7.5f, 20.f, 7.5f); + LLVector4 channel_priority(1.f, 0.75f, 0.5f, 0.75f); + F32 distance_power = 0.5f; switch (quality) { case 0: // Low max_res = 1024; - channel_priority.setVec(20.f, 30.f, 80.f, 30.f); + channel_priority.setVec(0.5f, 0.75f, 0.1f, 0.5f); + distance_power = 0.15f; break; case 1: // Medium - channel_priority.setVec(10.f, 15.f, 40.f, 15.f); + channel_priority.setVec(0.75f, 0.75f, 0.3f, 0.75f); + distance_power = 0.25f; break; case 2: // High - // defaults above + // channel defaults above (1, 0.75, 0.5, 0.75) + distance_power = 0.35f; break; case 3: // Ultra default: - if (quality > 3) quality = 3; channel_priority.setVec(1.f, 1.f, 1.f, 1.f); + distance_power = 0.5f; break; } gSavedSettings.setU32("RenderMaxTextureResolution", max_res); gSavedSettings.setVector4("TextureChannelPriority", channel_priority); + gSavedSettings.setF32("TextureDistanceDiscardPower", distance_power); return true; } diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 0f23596c9a..79439b4d58 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -87,6 +87,8 @@ S32 LLViewerTexture::sRawCount = 0; S32 LLViewerTexture::sAuxCount = 0; LLFrameTimer LLViewerTexture::sEvaluationTimer; F32 LLViewerTexture::sDesiredDiscardBias = 0.f; +F32 LLViewerTexture::sBackgroundFactor = 0.f; +F32 LLViewerTexture::sMemoryPressureFactor = 0.f; U32 LLViewerTexture::sBiasTexturesUpdated = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size @@ -97,12 +99,12 @@ constexpr S32 DEFAULT_ICON_DIMENSIONS = 32; constexpr S32 DEFAULT_THUMBNAIL_DIMENSIONS = 256; U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256. U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA; -bool LLViewerTexture::sFreezeImageUpdates = false; F32 LLViewerTexture::sCurrentTime = 0.0f; constexpr F32 MEMORY_CHECK_WAIT_TIME = 1.0f; constexpr F32 MIN_VRAM_BUDGET = 768.f; F32 LLViewerTexture::sFreeVRAMMegabytes = MIN_VRAM_BUDGET; +F32 LLViewerTexture::sWindowPixelArea = 1.f; LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF; @@ -120,7 +122,7 @@ LLLoadedCallbackEntry::LLLoadedCallbackEntry(loaded_callback_func cb, LLViewerFetchedTexture* target, bool pause) : mCallback(cb), - mLastUsedDiscard(MAX_DISCARD_LEVEL+1), + mLastUsedDiscard(S32_MAX), mDesiredDiscard(discard_level), mNeedsImageRaw(need_imageraw), mUserData(userdata), @@ -485,6 +487,13 @@ void LLViewerTexture::updateClass() LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; sCurrentTime = gFrameTimeSeconds; + if (gViewerWindow) + { + F32 w = (F32)gViewerWindow->getWindowWidthRaw(); + F32 h = (F32)gViewerWindow->getWindowHeightRaw(); + sWindowPixelArea = llmax(w * h, 1.f); + } + LLTexturePipelineTester* tester = (LLTexturePipelineTester*)LLMetricPerformanceTesterBasic::getTester(sTesterName); if (tester) { @@ -523,6 +532,28 @@ void LLViewerTexture::updateClass() F32 over_pct = (used - target) / target; + // VRAM-pressure feedback loop with progressive backoff. Ramp starts at + // backoff_start x target, not at the budget cliff. Proportional in both + // directions: ramp = (over-1)(1-factor), decay = factor; converges at + // factor = 1 - 1/over_at_backoff_target. + { + static LLCachedControl backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f); + F32 backoff_target = target * llclamp((F32)backoff_start, 0.05f, 1.f); + F32 over = used / llmax(backoff_target, 1.f); + static LLCachedControl pressure_ramp_rate(gSavedSettings, "TextureMemoryPressureRampRate", 3.0f); + F32 dt = gFrameIntervalSeconds; + if (over > 1.f) + { + sMemoryPressureFactor += + (over - 1.f) * (1.f - sMemoryPressureFactor) * (F32)pressure_ramp_rate * dt; + } + else + { + sMemoryPressureFactor -= sMemoryPressureFactor * (F32)pressure_ramp_rate * dt; + } + sMemoryPressureFactor = llclampf(sMemoryPressureFactor); + } + bool is_sys_low = isSystemMemoryLow(); bool is_low = is_sys_low || over_pct > 0.f; @@ -569,6 +600,10 @@ void LLViewerTexture::updateClass() // don't execute above until the slam to 1.5 has a chance to take effect sEvaluationTimer.reset(); + // Don't decay bias while downscale is still draining - those bytes + // are about to free and the loop would oscillate. + bool eviction_in_flight = !gTextureList.mDownScaleQueue.empty(); + // lower discard bias over time when at least 10% of budget is free constexpr F32 FREE_PERCENTAGE_TRESHOLD = -0.1f; constexpr U32 FREE_SYS_MEM_TRESHOLD = 100; @@ -576,7 +611,8 @@ void LLViewerTexture::updateClass() const S32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory() + FREE_SYS_MEM_TRESHOLD); if (sDesiredDiscardBias > 1.f && over_pct < FREE_PERCENTAGE_TRESHOLD - && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY) + && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY + && !eviction_in_flight) { static LLCachedControl high_mem_discard_decrement(gSavedSettings, "RenderHighMemMinDiscardDecrement", .1f); @@ -622,6 +658,33 @@ void LLViewerTexture::updateClass() } } + // Background-window ramp: 0 -> 1 at rate per second while backgrounded, + // snaps to 0 in foreground. Default 0.011 ~ 90s to saturate. + { + static LLCachedControl bg_factor_rate(gSavedSettings, "TextureBackgroundFactorRatePerSec", 0.011f); + if (in_background) + { + sBackgroundFactor += (F32)bg_factor_rate * gFrameIntervalSeconds; + sBackgroundFactor = llclampf(sBackgroundFactor); + } + else + { + sBackgroundFactor = 0.f; + } + } + + // Fetch-queue depth as a one-way bias floor (decay path still drops + // bias when the queue drains). Pushes bias up before VRAM overflows + // during teleport/scene-change floods. + if (LLTextureFetch* fetcher = LLAppViewer::getTextureFetch()) + { + S32 pending = fetcher->getNumRequests(); + static LLCachedControl fetch_pressure_scale(gSavedSettings, "TextureFetchPressureScale", 1000.f); + F32 scale = llmax((F32)fetch_pressure_scale, 1.f); + F32 fetch_pressure = llclamp((F32)pending / scale, 0.f, 3.f); + sDesiredDiscardBias = llmax(sDesiredDiscardBias, 1.f + fetch_pressure); + } + sDesiredDiscardBias = llclamp(sDesiredDiscardBias, 1.f, 4.f); if (last_texture_update_count_bias < sDesiredDiscardBias) { @@ -638,8 +701,6 @@ void LLViewerTexture::updateClass() // a problem. last_texture_update_count_bias = sDesiredDiscardBias; } - - LLViewerTexture::sFreezeImageUpdates = false; } //static @@ -1156,8 +1217,10 @@ void LLViewerFetchedTexture::init(bool firstinit) mRequestedDownloadPriority = 0.f; mFullyLoaded = false; mCanUseHTTP = true; - mDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1; - mMinDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1; + mDesiredDiscardLevel = S8_MAX; + // S8_MAX = no cap. setMinDiscardLevel takes min(current, new), so + // explicit caps from terrain / avatar self / thumbnails still apply. + mMinDesiredDiscardLevel = S8_MAX; mDecodingAux = false; @@ -1977,21 +2040,10 @@ bool LLViewerFetchedTexture::processFetchResults(S32& desired_discard, S32 curre setIsMissingAsset(); desired_discard = -1; } - else - { - //LL_WARNS() << mID << ": Setting min discard to " << current_discard << LL_ENDL; - if (current_discard >= 0) - { - mMinDiscardLevel = current_discard; - //desired_discard = current_discard; - } - else - { - S32 dis_level = getDiscardLevel(); - mMinDiscardLevel = dis_level; - //desired_discard = dis_level; - } - } + // Transient failure (decoder OOM, network blip): don't latch + // mMinDiscardLevel - that would block all future fetches via + // the make_request gate. Permanent failures are caught above + // (getDiscardLevel()<0 -> setIsMissingAsset). destroyRawImage(); } else if (mRawImage.notNull()) @@ -2072,8 +2124,18 @@ bool LLViewerFetchedTexture::updateFetch() if (mRawImage.notNull()) sRawCount--; if (mAuxRawImage.notNull()) sAuxCount--; // keep in mind that fetcher still might need raw image, don't modify original + S32 codec_levels = 0; bool finished = LLAppViewer::getTextureFetch()->getRequestFinished(getID(), fetch_discard, mFetchState, mRawImage, mAuxRawImage, - mLastHttpGetStatus); + mLastHttpGetStatus, codec_levels); + if (codec_levels > 0) + { + mCodecMaxDiscardLevel = (S8)llmin(codec_levels, (S32)S8_MAX); + if (codec_levels > 5) + { + LL_DEBUGS("TextureStream") << "Texture " << mID << " codec-reported max discard " + << codec_levels << " (above the historical hardcoded cap of 5)" << LL_ENDL; + } + } if (mRawImage.notNull()) sRawCount++; if (mAuxRawImage.notNull()) { @@ -2108,7 +2170,9 @@ bool LLViewerFetchedTexture::updateFetch() } } - desired_discard = llmin(desired_discard, getMaxDiscardLevel()); + // Clamp the fetch request to what the codestream encodes; deeper + // discards are served from the GL mip pyramid via scaleDown. + desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel); bool make_request = true; if (decode_priority <= 0) @@ -2116,9 +2180,14 @@ bool LLViewerFetchedTexture::updateFetch() LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - priority <= 0"); make_request = false; } - else if (mDesiredDiscardLevel > getMaxDiscardLevel()) + else if (mDesiredDiscardLevel > (S32)mCodecMaxDiscardLevel && + current_discard >= 0 && + current_discard <= (S32)mCodecMaxDiscardLevel) { - LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > max"); + // scaleDown can serve this from the GL pyramid. (If current is + // already past codec_max, fall through so a zoom-in can rebuild — + // scaleDown only goes deeper.) + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > codec max"); make_request = false; } else if (mNeedsCreateTexture || mIsMissingAsset) @@ -2570,20 +2639,21 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() S32 gl_discard = getDiscardLevel(); - // If we don't have a legit GL image, set it to be lower than the worst discard level + // S32_MAX is the "no data" sentinel; real discards can now exceed + // MAX_DISCARD_LEVEL via dimDerivedMaxDiscard. if (gl_discard == -1) { - gl_discard = MAX_DISCARD_LEVEL + 1; + gl_discard = S32_MAX; } // // Determine the quality levels of textures that we can provide to callbacks // and whether we need to do decompression/readback to get it // - S32 current_raw_discard = MAX_DISCARD_LEVEL + 1; // We can always do a readback to get a raw discard + S32 current_raw_discard = S32_MAX; // We can always do a readback to get a raw discard S32 best_raw_discard = gl_discard; // Current GL quality level - S32 current_aux_discard = MAX_DISCARD_LEVEL + 1; - S32 best_aux_discard = MAX_DISCARD_LEVEL + 1; + S32 current_aux_discard = S32_MAX; + S32 best_aux_discard = S32_MAX; LLImageRaw *current_raw_image = nullptr; if (mIsRawImageValid) @@ -2683,7 +2753,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() // // Run raw/auxiliary data callbacks // - if (run_raw_callbacks && current_raw_image != nullptr && (current_raw_discard <= getMaxDiscardLevel())) + if (run_raw_callbacks && current_raw_image != nullptr && current_raw_discard != S32_MAX) { // Do callbacks which require raw image data. //LL_INFOS() << "doLoadedCallbacks raw for " << getID() << LL_ENDL; @@ -2723,7 +2793,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() // // Run GL callbacks // - if (run_gl_callbacks && (gl_discard <= getMaxDiscardLevel())) + if (run_gl_callbacks && gl_discard != S32_MAX) { //LL_INFOS() << "doLoadedCallbacks GL for " << getID() << LL_ENDL; @@ -3026,11 +3096,6 @@ S8 LLViewerLODTexture::getType() const return LLViewerTexture::LOD_TEXTURE; } -bool LLViewerLODTexture::isUpdateFrozen() -{ - return LLViewerTexture::sFreezeImageUpdates; -} - // This is gauranteed to get called periodically for every texture //virtual void LLViewerLODTexture::processTextureStats() @@ -3056,18 +3121,15 @@ void LLViewerLODTexture::processTextureStats() { mDesiredDiscardLevel = 0; } - // Generate the request priority and render priority - else if (mDontDiscard || !mUseMipMaps) + // HUD/UI/preview and mDontDiscard textures bypass streaming - no + // face_distance signal applies, they need native resolution. + else if (mBoostLevel >= LLGLTexture::BOOST_HIGH + || mDontDiscard + || !mUseMipMaps) { mDesiredDiscardLevel = 0; if (mFullWidth > MAX_IMAGE_SIZE_DEFAULT || mFullHeight > MAX_IMAGE_SIZE_DEFAULT) - mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096 - } - else if (mBoostLevel < LLGLTexture::BOOST_HIGH && mMaxVirtualSize <= 10.f) - { - // If the image has not been significantly visible in a while, we don't want it - mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, (S8)(MAX_DISCARD_LEVEL + 1)); - mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel); + mDesiredDiscardLevel = 1; // 4096^2 source can't be loaded full res } else if (!mFullWidth || !mFullHeight) { @@ -3076,28 +3138,86 @@ void LLViewerLODTexture::processTextureStats() } else { - //static const F64 log_2 = log(2.0); - static const F64 log_4 = log(4.0); - F32 discard_level = 0.f; - // If we know the output width and height, we can force the discard - // level to the correct value, and thus not decode more texture - // data than we need to. + // floor(log2(max(w, h))) - both the multiplier on the normalized + // factor and the cap clamp at the bottom of this function. + S32 dim_max_for_image_i = (mFullWidth > 0 && mFullHeight > 0) + ? LLImageGL::dimDerivedMaxDiscard(mFullWidth, mFullHeight) + : (S32)mCodecMaxDiscardLevel; + F32 dim_max_for_image = (F32)dim_max_for_image_i; + if (mKnownDrawWidth && mKnownDrawHeight) { + // UI-pinned target dimensions - use pixel-area math. + static const F64 log_4 = log(4.0); S32 draw_texels = mKnownDrawWidth * mKnownDrawHeight; draw_texels = llclamp(draw_texels, MIN_IMAGE_AREA, MAX_IMAGE_AREA); - - // Use log_4 because we're in square-pixel space, so an image - // with twice the width and twice the height will have mTexelsPerImage - // 4 * draw_size discard_level = (F32)(log(mTexelsPerImage / draw_texels) / log_4); } else { - // Calculate the required scale factor of the image using pixels per texel - discard_level = (F32)(log(mTexelsPerImage / mMaxVirtualSize) / log_4); + // Two 0..1 signals composed multiplicatively: + // discard = distance_factor * size_factor * max_discard + // distance_factor: face_dist / draw_dist, shaped by + // TextureDistanceDiscardPower (default 0.5 = sqrt). + // size_factor: 1 - (mMaxOnScreenSize / window_pixels), shaped + // by TextureSizeDiscardPower. + // Either factor near 0 keeps the result fine - both have to + // be high for the texture to go deep. + static LLCachedControl distance_power(gSavedSettings, "TextureDistanceDiscardPower", 0.5f); + F32 power = llmax((F32)distance_power, 0.0001f); + F32 distance_factor = (power == 1.f) ? mMinDistanceFactor : powf(mMinDistanceFactor, power); + + static LLCachedControl size_power(gSavedSettings, "TextureSizeDiscardPower", 1.f); + F32 sz_power = llmax((F32)size_power, 0.0001f); + F32 coverage = llclampf(mMaxOnScreenSize / sWindowPixelArea); + F32 inv_cov = 1.f - coverage; + F32 size_factor = (sz_power == 1.f) ? inv_cov : powf(inv_cov, sz_power); + + F32 combined = distance_factor * size_factor; + + // Per-channel exponent. 1.0 = baseline; <1.0 pushes combined + // toward 1 (max attenuation) faster. Edges are preserved: + // pow(0, p) = 0, pow(1, p) = 1. + S32 priority_channel = (mPriorityChannel >= 0 && mPriorityChannel < 4) ? (S32)mPriorityChannel : 1; + static LLCachedControl channel_priority(gSavedSettings, "TextureChannelPriority", + LLVector4(1.f, 1.f, 1.f, 1.f)); + F32 channel_power = llmax(channel_priority().mV[priority_channel], 0.0001f); + if (channel_power != 1.f) + { + combined = powf(combined, channel_power); + } + + // Own-avatar boost: shave combined for rigged/animated faces + // on gAgentAvatarp. Preference, not exemption - applied + // before the staleness/background/pressure floors so heavy + // pressure can still evict. + if (mOnAgentAvatar) + { + static LLCachedControl agent_avatar_boost(gSavedSettings, "TextureAgentAvatarBoost", 0.5f); + combined *= llclampf((F32)agent_avatar_boost); + } + + // Staleness / background floors. Avatar bakes exempt from + // background to avoid the universal-cloud bug when re-foregrounding. + combined = llmax(combined, mStalenessFactor); + if (!isAgentAvatarBoost(mBoostLevel)) + { + combined = llmax(combined, sBackgroundFactor); + } + + // VRAM pressure: pow(combined, 1 - factor) bends the curve + // without flattening it - pow(0, p) = 0 so close textures + // (combined ~ 0) stay near 0 while mid/far push toward 1. + // Avatar bakes exempt. + if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureFactor > 0.f) + { + F32 pressure_exp = llmax(1.f - sMemoryPressureFactor, 0.0001f); + combined = powf(combined, pressure_exp); + } + + discard_level = combined * dim_max_for_image; } discard_level = floorf(discard_level); @@ -3106,12 +3226,29 @@ void LLViewerLODTexture::processTextureStats() if (mFullWidth > max_tex_res || mFullHeight > max_tex_res) min_discard = 1.f; - discard_level = llclamp(discard_level, min_discard, (F32)MAX_DISCARD_LEVEL); + // dim_max_for_image_i is the per-texture cap. TextureMaxDiscardOverride + // raises it (debug). Codec_max applies only to fetches, not here. + static LLCachedControl max_discard_override(gSavedSettings, "TextureMaxDiscardOverride", 0); + S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i; + discard_level = llclamp(discard_level, min_discard, (F32)effective_cap); + + mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level); + + // Apply the setMinDiscardLevel cap, relaxed proportionally under + // VRAM pressure - at factor=1 the cap reaches dim_max so capped + // textures (terrain, etc.) participate fully in eviction. Caps of + // 0 (thumbnails) and avatar bakes are preserved. + S32 effective_min_cap = mMinDesiredDiscardLevel; + if (sMemoryPressureFactor > 0.f && + mMinDesiredDiscardLevel > 0 && mMinDesiredDiscardLevel < S8_MAX && + !isAgentAvatarBoost(mBoostLevel)) + { + F32 room = (F32)dim_max_for_image_i - (F32)mMinDesiredDiscardLevel; + effective_min_cap += (S32)(sMemoryPressureFactor * room); + effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i); + } + mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel); - // Can't go higher than the max discard level - mDesiredDiscardLevel = llmin(getMaxDiscardLevel() + 1, (S32)discard_level); - // Clamp to min desired discard - mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, mDesiredDiscardLevel); // // At this point we've calculated the quality level that we want, @@ -3120,7 +3257,9 @@ void LLViewerLODTexture::processTextureStats() // S32 current_discard = getDiscardLevel(); - if (mBoostLevel < LLGLTexture::BOOST_AVATAR_BAKED) + // Avatar bakes exempt: shrinking mid-bake can leave the avatar + // stuck as a cloud until the next bake completes. + if (!isAgentAvatarBoost(mBoostLevel)) { if (current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage) { // should scale down @@ -3128,13 +3267,6 @@ void LLViewerLODTexture::processTextureStats() } } - if (isUpdateFrozen() // we are out of memory and nearing max allowed bias - && mBoostLevel < LLGLTexture::BOOST_SCULPTED - && mDesiredDiscardLevel < current_discard) - { - // stop requesting more - mDesiredDiscardLevel = current_discard; - } mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel); } diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index 2937651995..f4770e5fac 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -203,6 +203,26 @@ protected: mutable S32 mMaxVirtualSizeResetInterval; LLFrameTimer mLastReferencedTimer; + // Index into TextureChannelPriority Vector4 (X=normals, Y=diffuse, + // Z=spec, W=emissive). -1 -> fall back to diffuse. + S8 mPriorityChannel = -1; + + // Bind-staleness floor, 0..1. Per-interval increment is 1/max_discard + // so any texture saturates after interval x max_discard seconds idle. + F32 mStalenessFactor = 0.f; + + // Closest face's face_distance / draw_distance, clamped 0..1. + // Defaults to 1 so never-measured textures resolve to deepest discard. + F32 mMinDistanceFactor = 1.f; + + // Largest per-face screen-space coverage in pixels. Raw - no bias or + // channel-priority contamination. + F32 mMaxOnScreenSize = 0.f; + + // Any face on the agent's avatar (rigged / animated). Drives the + // own-avatar quality boost in processTextureStats. + bool mOnAgentAvatar = false; + ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS]; LLFrameTimer mLastFaceListUpdateTimer ; @@ -224,16 +244,27 @@ public: static S32 sAuxCount; static LLFrameTimer sEvaluationTimer; static F32 sDesiredDiscardBias; + // Backgrounded-window discard floor, 0..1. Ramps while backgrounded, + // snaps to 0 in foreground. Avatar bakes exempt. + static F32 sBackgroundFactor; + + // VRAM-pressure factor, 0..1. Applied in processTextureStats as + // combined = pow(combined, 1 - factor) - bends the curve without + // flattening the distance gradient. + static F32 sMemoryPressureFactor; static U32 sBiasTexturesUpdated; static S32 sMaxSculptRez ; static U32 sMinLargeImageSize ; static U32 sMaxSmallImageSize ; - static bool sFreezeImageUpdates; static F32 sCurrentTime ; // estimated free memory for textures, by bias calculation static F32 sFreeVRAMMegabytes; + // Viewport pixel area, refreshed once per frame. Hoisted to keep the + // per-texture hot path out of gViewerWindow. + static F32 sWindowPixelArea; + enum EDebugTexels { DEBUG_TEXELS_OFF, @@ -279,6 +310,16 @@ public: LLViewerFetchedTexture(const LLImageRaw* raw, FTType f_type, bool usemipmaps); LLViewerFetchedTexture(const std::string& url, FTType f_type, const LLUUID& id, bool usemipmaps = true); + // Avatar bake/skin textures - exempt from non-visibility-driven discard + // (staleness, background, pressure) to avoid the universal-cloud bug. + static bool isAgentAvatarBoost(S32 boost_level) + { + return boost_level == BOOST_AVATAR + || boost_level == BOOST_AVATAR_BAKED + || boost_level == BOOST_AVATAR_SELF + || boost_level == BOOST_AVATAR_BAKED_SELF; + } + public: struct Compare @@ -461,6 +502,12 @@ protected: S8 mDesiredDiscardLevel; // The discard level we'd LIKE to have - if we have it and there's space S8 mMinDesiredDiscardLevel; // The minimum discard level we'd like to have + // Fetch-side discard cap from the J2C codestream's DWT level count + // (populated by the fetcher). Distinct from LLImageGL::mMaxDiscardLevel - + // scaleDown can still trim the GL pyramid past this. + static constexpr S8 sFallbackCodecMaxDiscardLevel = 5; // MIN_DECOMPOSITION_LEVELS + S8 mCodecMaxDiscardLevel = sFallbackCodecMaxDiscardLevel; + bool mNeedsAux; // We need to decode the auxiliary channels bool mHasAux; // We have aux channels bool mDecodingAux; // Are we decoding high components @@ -540,7 +587,6 @@ public: S8 getType() const override; // Process image stats to determine priority/quality requirements. void processTextureStats() override; - bool isUpdateFrozen() ; bool scaleDown() override; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7dd32074cf..235888e2d1 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -47,6 +47,7 @@ #include "message.h" #include "lldrawpoolbump.h" // to init bumpmap images +#include "llagentcamera.h" #include "lltexturecache.h" #include "lltexturefetch.h" #include "llviewercontrol.h" @@ -61,6 +62,8 @@ #include "lltracerecording.h" #include "llviewerdisplay.h" #include "llviewerwindow.h" +#include "llsurface.h" +#include "llvoavatarself.h" #include "llprogressview.h" //////////////////////////////////////////////////////////////////////////// @@ -93,6 +96,20 @@ LLTextureKey::LLTextureKey(LLUUID id, ETexListType tex_type) /////////////////////////////////////////////////////////////////////////////// +// eTexIndex -> TextureChannelPriority component index (X=normals, Y=diffuse, +// Z=spec, W=emissive). Single source of truth - route all channel-priority +// lookups through this table. +const S32 LLViewerTextureList::sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS] = +{ + 1, // DIFFUSE_MAP (0) -> Y (diffuse) + 0, // NORMAL_MAP / ALT_DIFFUSE (1) -> X (normals) + 2, // SPECULAR_MAP (2) -> Z (specular/metallic) + 1, // BASECOLOR_MAP (3) -> Y (diffuse) + 2, // METALLIC_ROUGHNESS_MAP (4) -> Z (specular/metallic) + 0, // GLTF_NORMAL_MAP (5) -> X (normals) + 3, // EMISSIVE_MAP (6) -> W (emissive) +}; + LLViewerTextureList::LLViewerTextureList() : mForceResetTextureStats(false), mInitialized(false) @@ -899,8 +916,7 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag { llassert(!gCubeSnapshot); - constexpr F32 BIAS_TRS_OUT_OF_SCREEN = 1.5f; - constexpr F32 BIAS_TRS_ON_SCREEN = 1.f; + constexpr F32 BIAS_TRS_ON_SCREEN = 1.f; // perf gate for face-loop early exit if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures { @@ -910,9 +926,35 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 max_vsize = 0.f; bool on_screen = false; + // Accumulators for the per-texture signals published below. + // Defaults map to "deepest discard wanted" until evidence updates them. + F32 min_distance_factor = 1.f; + F32 max_on_screen_size = 0.f; + bool on_agent_avatar = false; + F32 draw_distance = llmax(gAgentCamera.mDrawDistance, 0.001f); + U32 face_count = 0; U32 max_faces_to_check = 1024; + // Pick the least-aggressive channel across all uses, so a texture + // used as both diffuse and normal isn't penalized by its harshest + // role. -1 sentinel keeps emissive-only textures (W=3) from being + // clobbered by a smaller init value. + S32 priority_channel = -1; + for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i) + { + if (imagep->getNumFaces(i) > 0) + { + S32 mapped = sChannelToPriority[i]; + priority_channel = (priority_channel < 0) ? mapped : llmin(priority_channel, mapped); + } + } + if (priority_channel < 0) + { + priority_channel = 1; // no faces - default to diffuse + } + imagep->mPriorityChannel = (S8)priority_channel; + // get adjusted bias based on image resolution LLImageGL* img = imagep->getGLTexture(); F32 max_discard = F32(img ? img->getMaxDiscardLevel() : MAX_DISCARD_LEVEL); @@ -948,6 +990,14 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag on_screen |= face->mInFrustum; + F32 dist_factor = llclampf(face->mDistanceToCamera / draw_distance); + min_distance_factor = llmin(min_distance_factor, dist_factor); + + if (face->mAvatar && face->mAvatar == gAgentAvatarp) + { + on_agent_avatar = true; + } + // Scale desired texture resolution higher or lower depending on texture scale // // Minimum usage examples: a 1024x1024 texture with aplhabet (texture atlas), @@ -963,6 +1013,10 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag min_scale = llclamp(min_scale * min_scale, texture_scale_min(), texture_scale_max()); vsize /= min_scale; + // Raw screen-space coverage - taken before the bias / + // camera-boost mutations below so the size signal is clean. + max_on_screen_size = llmax(max_on_screen_size, vsize); + // apply bias to offscreen faces all the time, but only to onscreen faces when bias is large // use mImportanceToCamera to make bias switch a bit more gradual if (!face->mInFrustum || LLViewerTexture::sDesiredDiscardBias > 1.9f + face->mImportanceToCamera / 2.f) @@ -970,13 +1024,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag vsize /= bias; } - // boost resolution of textures that are important to the camera - if (face->mInFrustum) - { - static LLCachedControl texture_camera_boost(gSavedSettings, "TextureCameraBoost", 8.f); - vsize *= llmax(face->mImportanceToCamera*texture_camera_boost, 1.f); - } - max_vsize = llmax(max_vsize, vsize); // addTextureStats limits size to sMaxVirtualSize @@ -995,57 +1042,89 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag } } - if (face_count > max_faces_to_check) + bool used_face_fast_path = (face_count > max_faces_to_check); + if (used_face_fast_path) { // this texture is used in so many places we should just boost it and not bother checking its vsize // this is especially important because the above is not time sliced and can hit multiple ms for a single texture max_vsize = MAX_IMAGE_AREA; } - if (imagep->getType() == LLViewerTexture::LOD_TEXTURE && imagep->getBoostLevel() == LLViewerTexture::BOOST_NONE) - { // conditionally reset max virtual size for unboosted LOD_TEXTURES - // this is an alternative to decaying mMaxVirtualSize over time - // that keeps textures from continously downrezzing and uprezzing in the background + imagep->addTextureStats(max_vsize); - if (LLViewerTexture::sDesiredDiscardBias > BIAS_TRS_OUT_OF_SCREEN || - (!on_screen && LLViewerTexture::sDesiredDiscardBias > BIAS_TRS_ON_SCREEN)) - { - imagep->mMaxVirtualSize = 0.f; - } + // Publish per-texture signals for processTextureStats. Closest face + // wins for distance (min); biggest face wins for size (max). Default + // distance=1, size=0 maps to "deepest discard wanted" - never- + // measured textures stay coarse until distance/size evidence arrives. + if (used_face_fast_path) + { + // Fast path saw only a prefix of faces - force best-quality + // sentinels to match the MAX_IMAGE_AREA vsize boost above. + imagep->mMinDistanceFactor = 0.f; + imagep->mMaxOnScreenSize = (F32)MAX_IMAGE_AREA; + } + else if (face_count == 0 && imagep->getBoostLevel() == LLGLTexture::BOOST_TERRAIN) + { + // Terrain detail textures don't register faces with the texture + // (LLVOSurfacePatch addFace(NULL)). Drive distance from the LOD + // system; floor at a small nonzero value so pressure has + // something to bite into (pow(0, p) = 0). + static LLCachedControl terrain_distance_floor(gSavedSettings, "TextureTerrainDistanceFloor", 0.01f); + static LLCachedControl terrain_coverage(gSavedSettings, "TextureTerrainCoverageFraction", 0.99f); + F32 nearest = LLSurface::sNearestVisiblePatchDistance; + F32 dist = (nearest < FLT_MAX) ? llclampf(nearest / draw_distance) : 1.f; + imagep->mMinDistanceFactor = llmax(dist, llclampf((F32)terrain_distance_floor)); + imagep->mMaxOnScreenSize = LLViewerTexture::sWindowPixelArea * llclampf((F32)terrain_coverage); } + else + { + imagep->mMinDistanceFactor = min_distance_factor; + imagep->mMaxOnScreenSize = max_on_screen_size; + } + imagep->mOnAgentAvatar = on_agent_avatar; + + // Bind-staleness. Avatar bakes exempt (cloud-bug protection). + // Per-interval increment is 1/max_discard so saturation time is + // interval * max_discard seconds regardless of texture size. + // Never-bound textures defer to distance/size or initial fetch + // could never start. + if (LLViewerFetchedTexture::isAgentAvatarBoost(imagep->getBoostLevel())) + { + imagep->mStalenessFactor = 0.f; + } + else if (LLImageGL* gli = imagep->getGLTexture()) + { + static LLCachedControl bind_decay_seconds(gSavedSettings, "TextureBindDecaySeconds", 5.f); + static LLCachedControl staleness_interval(gSavedSettings, "TextureStalenessIntervalSeconds", 5.f); + F32 grace = llmax((F32)bind_decay_seconds, 0.f); + F32 interval = llmax((F32)staleness_interval, 0.0001f); - imagep->addTextureStats(max_vsize); + bool ever_bound = (gli->mLastBindTime > 0.f); + F32 time_since_bind = ever_bound ? (LLImageGL::sLastFrameTime - gli->mLastBindTime) : 0.f; - // Derive stream priority channel from face lists. - // Map render texture channels to priority channels: - // 0 = normal, 1 = diffuse, 2 = specular, 3 = emissive - { - static const S32 render_to_priority[] = { - 1, // DIFFUSE_MAP (0) - 0, // NORMAL_MAP / ALTERNATE_DIFFUSE_MAP (1) - 2, // SPECULAR_MAP (2) - 1, // BASECOLOR_MAP (3) - 2, // METALLIC_ROUGHNESS_MAP (4) - 0, // GLTF_NORMAL_MAP (5) - 3, // EMISSIVE_MAP (6) - }; - - S32 priority_channel = 1; // default to diffuse - for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i) + if (!ever_bound || time_since_bind <= grace) { - if (imagep->getNumFaces(i) > 0) - { - priority_channel = llmin(priority_channel, render_to_priority[i]); - } + imagep->mStalenessFactor = 0.f; } - - static LLCachedControl channel_priority(gSavedSettings, "TextureChannelPriority", - LLVector4(10.0f, 20.0f, 40.0f, 20.0f)); - F32 factor = llmax(channel_priority().mV[priority_channel], 0.1f); - if (factor != 1.0f) + else { - imagep->mMaxVirtualSize /= factor; + S32 full_w = imagep->getFullWidth(); + S32 full_h = imagep->getFullHeight(); + S32 max_discard = (full_w > 0 && full_h > 0) + ? LLImageGL::dimDerivedMaxDiscard(full_w, full_h) + : (S32)gli->getMaxDiscardLevel(); + if (max_discard > 0) + { + F32 steps = (time_since_bind - grace) / interval; + F32 step_size = 1.f / (F32)max_discard; + imagep->mStalenessFactor = llclampf(steps * step_size); + } + else + { + imagep->mStalenessFactor = 0.f; + } } } + } #if 0 @@ -1154,8 +1233,7 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) imagep->postCreateTexture(); imagep->mCreatePending = false; - if (imagep->hasGLTexture() && imagep->getDiscardLevel() < imagep->getDesiredDiscardLevel() && - (imagep->getDesiredDiscardLevel() <= MAX_DISCARD_LEVEL)) + if (imagep->hasGLTexture() && imagep->getDiscardLevel() < imagep->getDesiredDiscardLevel()) { // NOTE: this may happen if the desired discard reduces while a decode is in progress and does not // necessarily indicate a problem, but if log occurrences excede that of dsiplay_stats: FPS, @@ -1180,7 +1258,7 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) gCopyProgram.bind(); gPipeline.mScreenTriangleVB->setBuffer(); - // give time to downscaling first -- if mDownScaleQueue is not empty, we're running out of memory and need + // give time to downscaling first - if mDownScaleQueue is not empty, we're running out of memory and need // to free up memory by discarding off screen textures quickly // do at least 5 and make sure we don't get too far behind even if it violates diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 7c7112f4cf..dd8655cd6f 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -30,6 +30,7 @@ #include "lluuid.h" //#include "message.h" #include "llgl.h" +#include "llrender.h" #include "llviewertexture.h" #include "llui.h" #include @@ -92,6 +93,10 @@ class LLViewerTextureList friend class LLLocalBitmap; public: + // eTexIndex -> TextureChannelPriority component (X=normals, Y=diffuse, + // Z=spec, W=emissive). Single source of truth. + static const S32 sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS]; + static bool createUploadFile(LLPointer raw_image, const std::string& out_filename, const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT, diff --git a/indra/newview/llvlcomposition.cpp b/indra/newview/llvlcomposition.cpp index 3441e25c6a..eb0261b5e5 100644 --- a/indra/newview/llvlcomposition.cpp +++ b/indra/newview/llvlcomposition.cpp @@ -173,7 +173,10 @@ LLPointer fetch_terrain_texture(const LLUUID& id) return nullptr; } - LLPointer tex = LLViewerTextureManager::getFetchedTexture(id); + // LOD_TEXTURE so streaming math runs (the base-class processTextureStats + // pins mDesiredDiscardLevel at 0). + LLPointer tex = LLViewerTextureManager::getFetchedTexture( + id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); return tex; } @@ -343,18 +346,9 @@ bool LLTerrainMaterials::makeTextureReady(LLPointer& tex { if (boost) { + // Quality is driven by the streaming math via synthetic signals + // for BOOST_TERRAIN textures in updateImageDecodePriority. boost_minimap_texture(tex, BASE_SIZE*BASE_SIZE); - - S32 width = tex->getFullWidth(); - S32 height = tex->getFullHeight(); - S32 min_dim = llmin(width, height); - S32 ddiscard = 0; - while (min_dim > BASE_SIZE && ddiscard < MAX_DISCARD_LEVEL) - { - ddiscard++; - min_dim /= 2; - } - tex->setMinDiscardLevel(ddiscard); } return false; } -- cgit v1.3 From 01f4b1817d07b5edb850cc7ca1f6cf14e2406c7e Mon Sep 17 00:00:00 2001 From: Cosmic Linden Date: Thu, 14 May 2026 15:46:20 -0700 Subject: Fix display of ad-hoc session type in About and display p2p instead as appropriate --- indra/newview/llvoicewebrtc.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 333046adce..6d5a8eae6a 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -353,9 +353,16 @@ void LLWebRTCVoiceClient::updateVersion() { // 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()) + if (dynamic_cast(session.get())) { - mVoiceVersion.mBuildVersion = "ad-hoc"; + if (session->mHangupOnLastLeave) + { + mVoiceVersion.mBuildVersion = "p2p"; + } + else + { + mVoiceVersion.mBuildVersion = "ad-hoc"; + } } else if (session->isEstate()) { -- cgit v1.3 From 97f8dfaca2a60e228b84aedbf71620f0c000412c Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 14 May 2026 22:36:43 -0400 Subject: Add a background discard offset. --- indra/newview/app_settings/settings.xml | 12 ++++++++++++ indra/newview/llviewertexture.cpp | 11 ++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 0f1b704dc9..e9d91b7add 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11909,6 +11909,18 @@ Value 0.011 + TextureBackgroundDiscardOffset + + Comment + Backgrounded textures will only discard up to (dim_max - offset). e.g. a 2048 texture (dim_max 11) with offset 2 caps the background floor at discard 9. 0 disables the cap (background can drive to max discard). + Persist + 1 + Type + S32 + Value + 2 + + TextureDistanceDiscardPower Comment diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 79439b4d58..1e8951926e 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -3204,7 +3204,16 @@ void LLViewerLODTexture::processTextureStats() combined = llmax(combined, mStalenessFactor); if (!isAgentAvatarBoost(mBoostLevel)) { - combined = llmax(combined, sBackgroundFactor); + // Background floor capped at (dim_max - offset) so we can + // keep some baseline quality while backgrounded. + static LLCachedControl bg_offset(gSavedSettings, "TextureBackgroundDiscardOffset", 2); + F32 bg = sBackgroundFactor; + if ((S32)bg_offset > 0 && dim_max_for_image > 0.f) + { + F32 cap = llmax(dim_max_for_image - (F32)(S32)bg_offset, 0.f) / dim_max_for_image; + bg = llmin(bg, cap); + } + combined = llmax(combined, bg); } // VRAM pressure: pow(combined, 1 - factor) bends the curve -- cgit v1.3 From 97bcc7816d7084b4aaed67cdb90a5b7ae0ec1126 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Mon, 18 May 2026 13:26:34 -0400 Subject: Add a texture "bubble" near the camera to ensure high res textures closer to the camera. --- indra/newview/app_settings/settings.xml | 11 +++++++++++ indra/newview/llviewertexturelist.cpp | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index e9d91b7add..35b3665bec 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11921,6 +11921,17 @@ 2 + TextureCloseBubbleMeters + + Comment + Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. + Persist + 1 + Type + F32 + Value + 5.0 + TextureDistanceDiscardPower Comment diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 235888e2d1..d555cd21db 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -933,6 +933,13 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag bool on_agent_avatar = false; F32 draw_distance = llmax(gAgentCamera.mDrawDistance, 0.001f); + // Close-camera bubble: distances under bubble_meters resolve to + // dist_factor = 0 (no discard contribution). The ramp from 0 -> 1 + // spans (bubble, draw_distance] rather than (0, draw_distance]. + static LLCachedControl close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f); + F32 bubble = llclamp((F32)close_bubble, 0.f, draw_distance - 0.001f); + F32 ramp_range = llmax(draw_distance - bubble, 0.001f); + U32 face_count = 0; U32 max_faces_to_check = 1024; @@ -990,7 +997,8 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag on_screen |= face->mInFrustum; - F32 dist_factor = llclampf(face->mDistanceToCamera / draw_distance); + F32 dist_above_bubble = llmax(face->mDistanceToCamera - bubble, 0.f); + F32 dist_factor = llclampf(dist_above_bubble / ramp_range); min_distance_factor = llmin(min_distance_factor, dist_factor); if (face->mAvatar && face->mAvatar == gAgentAvatarp) @@ -1071,7 +1079,8 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag static LLCachedControl terrain_distance_floor(gSavedSettings, "TextureTerrainDistanceFloor", 0.01f); static LLCachedControl terrain_coverage(gSavedSettings, "TextureTerrainCoverageFraction", 0.99f); F32 nearest = LLSurface::sNearestVisiblePatchDistance; - F32 dist = (nearest < FLT_MAX) ? llclampf(nearest / draw_distance) : 1.f; + F32 nearest_above_bubble = (nearest < FLT_MAX) ? llmax(nearest - bubble, 0.f) : ramp_range; + F32 dist = llclampf(nearest_above_bubble / ramp_range); imagep->mMinDistanceFactor = llmax(dist, llclampf((F32)terrain_distance_floor)); imagep->mMaxOnScreenSize = LLViewerTexture::sWindowPixelArea * llclampf((F32)terrain_coverage); } -- cgit v1.3 From 1b1f59c1e21581c4c1968b9206b9d9c0dc8513a1 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Mon, 18 May 2026 13:40:59 -0400 Subject: Break out texture channel priorities. --- indra/newview/app_settings/settings.xml | 48 ++++++++++++++++++++++++++------- indra/newview/llviewercontrol.cpp | 24 ++++++++++------- indra/newview/llviewertexture.cpp | 15 ++++++++--- indra/newview/llviewertexture.h | 3 +-- indra/newview/llviewertexturelist.cpp | 6 ++--- indra/newview/llviewertexturelist.h | 4 +-- 6 files changed, 71 insertions(+), 29 deletions(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 35b3665bec..8559fa4ac3 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8016,7 +8016,7 @@ RenderTextureQuality Comment - Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution and TextureChannelPriority. + Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution, the four TextureChannel* exponents, and TextureDistanceDiscardPower. Persist 1 Type @@ -11816,21 +11816,49 @@ Value 20.0 - TextureChannelPriority + TextureChannelNormal Comment - Per-channel exponent on the combined discard factor. X=normals, Y=diffuse, Z=spec, W=emissive. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. + Per-channel discard exponent for normal maps. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. Persist 1 Type - Vector4 + F32 Value - - 1.0 - 0.75 - 0.5 - 0.75 - + 1.0 + + TextureChannelBaseColor + + Comment + Per-channel discard exponent for base color / diffuse. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. + Persist + 1 + Type + F32 + Value + 0.75 + + TextureChannelSpecular + + Comment + Per-channel discard exponent for specular / metallic-roughness. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureChannelEmissive + + Comment + Per-channel discard exponent for emissive. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset. + Persist + 1 + Type + F32 + Value + 0.75 TextureMaxDiscardOverride diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 3a6e4e2e2d..a4a7f1fd20 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -114,36 +114,42 @@ static bool handleRenderAvatarMouselookChanged(const LLSD& newvalue) static bool handleRenderTextureQualityChanged(const LLSD& newvalue) { - // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives max-resolution and the - // per-channel TextureChannelPriority + TextureDistanceDiscardPower - // exponents. Channel order: X=normals, Y=diffuse, Z=spec, W=emissive. + // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution, + // the four TextureChannel* exponents (Normal/BaseColor/Spec/Emissive), + // and TextureDistanceDiscardPower. U32 quality = (U32)newvalue.asInteger(); U32 max_res = 2048; - LLVector4 channel_priority(1.f, 0.75f, 0.5f, 0.75f); + F32 ch_normal = 1.0f; + F32 ch_basecolor = 0.75f; + F32 ch_specular = 0.5f; + F32 ch_emissive = 0.75f; F32 distance_power = 0.5f; switch (quality) { case 0: // Low max_res = 1024; - channel_priority.setVec(0.5f, 0.75f, 0.1f, 0.5f); + ch_normal = 0.5f; ch_basecolor = 0.75f; ch_specular = 0.1f; ch_emissive = 0.5f; distance_power = 0.15f; break; case 1: // Medium - channel_priority.setVec(0.75f, 0.75f, 0.3f, 0.75f); + ch_normal = 0.75f; ch_basecolor = 0.75f; ch_specular = 0.3f; ch_emissive = 0.75f; distance_power = 0.25f; break; case 2: // High - // channel defaults above (1, 0.75, 0.5, 0.75) + // channel defaults above distance_power = 0.35f; break; case 3: // Ultra default: - channel_priority.setVec(1.f, 1.f, 1.f, 1.f); + ch_normal = 1.f; ch_basecolor = 1.f; ch_specular = 1.f; ch_emissive = 1.f; distance_power = 0.5f; break; } gSavedSettings.setU32("RenderMaxTextureResolution", max_res); - gSavedSettings.setVector4("TextureChannelPriority", channel_priority); + gSavedSettings.setF32("TextureChannelNormal", ch_normal); + gSavedSettings.setF32("TextureChannelBaseColor", ch_basecolor); + gSavedSettings.setF32("TextureChannelSpecular", ch_specular); + gSavedSettings.setF32("TextureChannelEmissive", ch_emissive); gSavedSettings.setF32("TextureDistanceDiscardPower", distance_power); return true; } diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 1e8951926e..727c0510f6 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -3180,10 +3180,19 @@ void LLViewerLODTexture::processTextureStats() // Per-channel exponent. 1.0 = baseline; <1.0 pushes combined // toward 1 (max attenuation) faster. Edges are preserved: // pow(0, p) = 0, pow(1, p) = 1. + // mPriorityChannel order: 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive. S32 priority_channel = (mPriorityChannel >= 0 && mPriorityChannel < 4) ? (S32)mPriorityChannel : 1; - static LLCachedControl channel_priority(gSavedSettings, "TextureChannelPriority", - LLVector4(1.f, 1.f, 1.f, 1.f)); - F32 channel_power = llmax(channel_priority().mV[priority_channel], 0.0001f); + static LLCachedControl channel_normal (gSavedSettings, "TextureChannelNormal", 1.0f); + static LLCachedControl channel_basecolor(gSavedSettings, "TextureChannelBaseColor", 0.75f); + static LLCachedControl channel_specular (gSavedSettings, "TextureChannelSpecular", 0.5f); + static LLCachedControl channel_emissive (gSavedSettings, "TextureChannelEmissive", 0.75f); + const F32 channels[4] = { + (F32)channel_normal, + (F32)channel_basecolor, + (F32)channel_specular, + (F32)channel_emissive, + }; + F32 channel_power = llmax(channels[priority_channel], 0.0001f); if (channel_power != 1.f) { combined = powf(combined, channel_power); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index f4770e5fac..e2215700c0 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -203,8 +203,7 @@ protected: mutable S32 mMaxVirtualSizeResetInterval; LLFrameTimer mLastReferencedTimer; - // Index into TextureChannelPriority Vector4 (X=normals, Y=diffuse, - // Z=spec, W=emissive). -1 -> fall back to diffuse. + // 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive. -1 -> base color. S8 mPriorityChannel = -1; // Bind-staleness floor, 0..1. Per-interval increment is 1/max_discard diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index d555cd21db..2e66dc2a11 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -96,9 +96,9 @@ LLTextureKey::LLTextureKey(LLUUID id, ETexListType tex_type) /////////////////////////////////////////////////////////////////////////////// -// eTexIndex -> TextureChannelPriority component index (X=normals, Y=diffuse, -// Z=spec, W=emissive). Single source of truth - route all channel-priority -// lookups through this table. +// eTexIndex -> TextureChannel* index (0=Normal, 1=BaseColor, 2=Specular, +// 3=Emissive). Single source of truth - route all channel-priority lookups +// through this table. const S32 LLViewerTextureList::sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS] = { 1, // DIFFUSE_MAP (0) -> Y (diffuse) diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index dd8655cd6f..931f2ed50e 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -93,8 +93,8 @@ class LLViewerTextureList friend class LLLocalBitmap; public: - // eTexIndex -> TextureChannelPriority component (X=normals, Y=diffuse, - // Z=spec, W=emissive). Single source of truth. + // eTexIndex -> TextureChannel* index (0=Normal, 1=BaseColor, + // 2=Specular, 3=Emissive). Single source of truth. static const S32 sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS]; static bool createUploadFile(LLPointer raw_image, -- cgit v1.3 From e7263854de98868fd2b3205e09849fd6f04fb35b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 19 May 2026 12:19:11 -0400 Subject: More high pressure and quality changes. New "high res" bubble near the camera, minimum discard settings, and discard scaling. --- indra/llrender/llimagegl.cpp | 10 +- indra/llrender/llimagegl.h | 1 + indra/newview/app_settings/settings.xml | 116 ++++++++++++++++++- indra/newview/lltextureview.cpp | 8 +- indra/newview/llviewertexture.cpp | 195 ++++++++++++++++++++++++++------ indra/newview/llviewertexture.h | 14 ++- indra/newview/llviewertexturelist.cpp | 70 +++++++++--- indra/newview/llviewertexturelist.h | 4 + 8 files changed, 355 insertions(+), 63 deletions(-) (limited to 'indra') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index d79d13dc8b..a1396aba20 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -715,7 +715,10 @@ void LLImageGL::dump() //---------------------------------------------------------------------------- void LLImageGL::forceUpdateBindStats(void) const { - mLastBindTime = sLastFrameTime; + // Intentionally a no-op: mLastBindTime is written only by real bind + // paths so the staleness signal reflects actual GPU use. Callers that + // still invoke this (avatar "keep alive" sites, deleted-texture + // fallback) no longer falsely refresh staleness. } bool LLImageGL::updateBindStats() const @@ -1650,7 +1653,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S { destroyGLTexture(); mCurrentDiscardLevel = discard_level; - mLastBindTime = sLastFrameTime; mGLTextureCreated = false; return true ; } @@ -1766,9 +1768,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ mTextureMemory = (S64Bytes)getMipBytes(mCurrentDiscardLevel); - - // mark this as bound at this point, so we don't throw it out immediately - mLastBindTime = sLastFrameTime; + mGLCreateTime = sLastFrameTime; checkActiveThread(); return true; diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index a02c320738..0c85446b84 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -239,6 +239,7 @@ public: // Various GL/Rendering options S64Bytes mTextureMemory; mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives streaming staleness + F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created; staleness fallback for never-bound textures private: U32 createPickMask(S32 pWidth, S32 pHeight); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 8559fa4ac3..0c4df7a805 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11874,13 +11874,13 @@ TextureMemoryPressureRampRate Comment - Feedback rate (per second) for the VRAM-pressure factor (0..1). Higher = faster convergence on the budget; lower = gentler. + Geometric ramp/decay rate (per second) for the VRAM-pressure distance multiplier (>= 1). Higher = faster convergence on the budget; lower = gentler. Multiplier compresses the streaming distance signal: at mult=N, faces past (ramp_range / N) hit max discard. Persist 1 Type F32 Value - 3.0 + 1.0 TextureMemoryPressureBackoffStart @@ -11893,6 +11893,83 @@ Value 0.85 + TextureMemoryPressureMaxMultiplier + + Comment + Upper bound on the VRAM-pressure distance multiplier (>= 1). Mostly defensive -- at mult=64 the streaming ramp collapses to ~ramp_range/64, already extreme. Higher allows even more aggressive compression in tight-budget scenes. + Persist + 1 + Type + F32 + Value + 64.0 + + TextureLastDitchEngageProgress + + Comment + mult_progress (0..1) at which the last-ditch floor starts creeping up. The floor only advances when mult is at or above this fraction of its cap AND prediction is still over budget. Decays back toward 0 whenever prediction is under budget. + Persist + 1 + Type + F32 + Value + 0.95 + + TextureLastDitchRampRate + + Comment + Rate (discard levels/sec) at which sLastDitchMinDiscard creeps up while engaged. 0.5 = takes ~2 sec to add one discard level. Mirrors sDesiredDiscardBias ramp shape. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureLastDitchDecayRate + + Comment + Rate (discard levels/sec) at which sLastDitchMinDiscard decays back to 0 when prediction is under budget. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureLastDitchMinDiscardMax + + Comment + Hard ceiling on sLastDitchMinDiscard. At 13 the floor can climb all the way to the deepest meaningful mip; lower values cap how aggressive the last-ditch escalation can get before we are simply out of discards. + Persist + 1 + Type + F32 + Value + 13.0 + + TextureMemoryPressurePredictionGain + + Comment + Power exponent mapping predicted-over-budget ratio to target multiplier. target_mult = pred_over^gain. Higher gain saturates faster. + Persist + 1 + Type + F32 + Value + 10.0 + + TextureMemoryPressureSmoothingRate + + Comment + Lerp rate (1/sec) at which the pressure multiplier converges to its prediction-driven target. Higher = faster response, lower = smoother. Default 4 reaches ~63% in 0.25s. + Persist + 1 + Type + F32 + Value + 4.0 + TextureTerrainDistanceFloor Comment @@ -11952,7 +12029,7 @@ TextureCloseBubbleMeters Comment - Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. + Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. Shrinks toward TextureCloseBubbleMinMeters as VRAM pressure ramps the multiplier toward its cap. Persist 1 Type @@ -11960,6 +12037,39 @@ Value 5.0 + TextureCloseBubbleMinMeters + + Comment + Floor (meters) for the close-camera bubble under maximum VRAM pressure. At sMemoryPressureMultiplier = TextureMemoryPressureMaxMultiplier the bubble collapses to this value, allowing eviction of even close textures when nothing else fits. + Persist + 1 + Type + F32 + Value + 3.0 + + TextureCloseBubbleShrinkThreshold + + Comment + Bubble stays at full size until mult_progress exceeds this fraction (0..1) of its range to the cap. Above that, bubble lerps from full to TextureCloseBubbleMinMeters. Keeps the bubble out of the normal feedback loop. + Persist + 1 + Type + F32 + Value + 0.8 + + TextureCloseBubbleTrackRate + + Comment + Rate (1/sec) at which the actual bubble tracks its target. Lower = smoother, slower to react. Damps short-term multiplier swings so close textures don't yo-yo. + Persist + 1 + Type + F32 + Value + 0.5 + TextureDistanceDiscardPower Comment diff --git a/indra/newview/lltextureview.cpp b/indra/newview/lltextureview.cpp index 8cbede8303..4534db958f 100644 --- a/indra/newview/lltextureview.cpp +++ b/indra/newview/lltextureview.cpp @@ -572,9 +572,13 @@ void LLGLTexMemBar::draw() LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height*8, text_color, LLFontGL::LEFT, LLFontGL::TOP); - text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB)", image_count, raw_image_count, raw_image_bytes_MB, + text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB) Bubble: %.1fm PressMult: %.1fx LDMin: %.1f", + image_count, raw_image_count, raw_image_bytes_MB, saved_raw_image_count, saved_raw_image_bytes_MB, - aux_raw_image_count, aux_raw_image_bytes_MB); + aux_raw_image_count, aux_raw_image_bytes_MB, + LLViewerTextureList::sCurrentBubbleMeters, + LLViewerTexture::sMemoryPressureMultiplier, + LLViewerTexture::sLastDitchMinDiscard); LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height * 7, text_color, LLFontGL::LEFT, LLFontGL::TOP); diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 727c0510f6..e55e4016ec 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -88,7 +88,8 @@ S32 LLViewerTexture::sAuxCount = 0; LLFrameTimer LLViewerTexture::sEvaluationTimer; F32 LLViewerTexture::sDesiredDiscardBias = 0.f; F32 LLViewerTexture::sBackgroundFactor = 0.f; -F32 LLViewerTexture::sMemoryPressureFactor = 0.f; +F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f; +F32 LLViewerTexture::sLastDitchMinDiscard = 0.f; U32 LLViewerTexture::sBiasTexturesUpdated = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size @@ -532,26 +533,119 @@ void LLViewerTexture::updateClass() F32 over_pct = (used - target) / target; - // VRAM-pressure feedback loop with progressive backoff. Ramp starts at - // backoff_start x target, not at the budget cliff. Proportional in both - // directions: ramp = (over-1)(1-factor), decay = factor; converges at - // factor = 1 - 1/over_at_backoff_target. + // VRAM-pressure controller. Drives sMemoryPressureMultiplier from + // PREDICTED VRAM (used + in-flight refetch growth - in-flight downscale + // shrinkage) rather than instantaneous used. The feedback loop chasing + // instantaneous VRAM sawtooths because eviction is fast but refetch is + // slow; the prediction lets the controller see the equilibrium directly, + // so mult converges to a target value instead of cycling. { static LLCachedControl backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f); + static LLCachedControl max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + static LLCachedControl prediction_gain(gSavedSettings, "TextureMemoryPressurePredictionGain", 10.f); + static LLCachedControl smoothing_rate(gSavedSettings, "TextureMemoryPressureSmoothingRate", 4.f); + F32 backoff_target = target * llclamp((F32)backoff_start, 0.05f, 1.f); - F32 over = used / llmax(backoff_target, 1.f); - static LLCachedControl pressure_ramp_rate(gSavedSettings, "TextureMemoryPressureRampRate", 3.0f); - F32 dt = gFrameIntervalSeconds; - if (over > 1.f) + + // Walk the texture list once per frame, summing pending size deltas. + // Approximation: bytes(d) = (w>>d) * (h>>d) * 4 * 4/3. Units match + // `used` after the /1024/512 conversion that produced it. Coarse but + // proportionally correct - the gain knob tunes absolute magnitude. + S64 pending_bytes_increase = 0; + S64 pending_bytes_decrease = 0; { - sMemoryPressureFactor += - (over - 1.f) * (1.f - sMemoryPressureFactor) * (F32)pressure_ramp_rate * dt; + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict"); + for (auto& imagep : gTextureList) + { + if (imagep.isNull()) continue; + S32 fw = imagep->getFullWidth(); + S32 fh = imagep->getFullHeight(); + if (fw <= 0 || fh <= 0) continue; + S32 desired = imagep->getDesiredDiscardLevel(); + S32 current = imagep->getDiscardLevel(); + if (desired < 0 || current < 0 || desired == current) continue; + + S32 wd = llmax(1, fw >> desired); + S32 hd = llmax(1, fh >> desired); + S32 wc = llmax(1, fw >> current); + S32 hc = llmax(1, fh >> current); + // bpp=4, mip pyramid overhead 4/3 + S64 size_d = (S64)wd * hd * 4 * 4 / 3; + S64 size_c = (S64)wc * hc * 4 * 4 / 3; + + if (desired < current) + pending_bytes_increase += (size_d - size_c); + else + pending_bytes_decrease += (size_c - size_d); + } } - else + + // Match the unit conversion used to produce `used` (texture_bytes_alloc + // divided by 1024/512). 1024*512 = 524288. + constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f; + F32 predicted_used = used + + (F32)pending_bytes_increase * BYTES_TO_USED_UNITS + - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS; + F32 predicted_over = predicted_used / llmax(backoff_target, 1.f); + + // Direct target: at over=1, mult=1; growth governed by gain power. + // Smooth toward target so per-frame prediction noise doesn't jolt + // the controller. Lerp rate set so a step change in target reaches + // ~63% in 1/smoothing_rate seconds (default 0.25s). + F32 cap = llmax((F32)max_mult, 1.0001f); + F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); + F32 dt = (F32)gFrameIntervalSeconds; + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); + sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap); + + // Last-ditch global discard floor. Ramps up when mult is pegged near + // cap and we are still over budget; decays toward 0 when fitting. Once + // the normal compression knobs are exhausted, this creeps the discard + // floor up integer step by step, mirroring how sDesiredDiscardBias + // pushes background textures. + { + static LLCachedControl ld_engage(gSavedSettings, "TextureLastDitchEngageProgress", 0.95f); + static LLCachedControl ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f); + static LLCachedControl ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f); + static LLCachedControl ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f); + F32 progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); + bool mult_saturated = progress >= llclampf((F32)ld_engage); + if (mult_saturated && predicted_over > 1.f) + { + sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt; + } + else if (predicted_over < 1.f) + { + sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt; + } + sLastDitchMinDiscard = llclamp(sLastDitchMinDiscard, 0.f, llmax((F32)ld_max, 0.f)); + } + + F32 over = used / llmax(backoff_target, 1.f); // legacy alias used below + + // Throttled bisection log. Once per second. + static LLFrameTimer s_pressure_log_timer; + if (s_pressure_log_timer.getElapsedTimeF32() > 1.f) { - sMemoryPressureFactor -= sMemoryPressureFactor * (F32)pressure_ramp_rate * dt; + s_pressure_log_timer.reset(); + F32 mult_progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); + LL_INFOS("TextureStream") << "pressure" + << " mult=" << sMemoryPressureMultiplier + << " target_mult=" << target_mult + << " progress=" << mult_progress + << " used=" << used + << " predicted=" << predicted_used + << " target=" << target + << " over=" << over + << " pred_over=" << predicted_over + << " in+=" << (S32)(pending_bytes_increase / 1024 / 1024) + << "MB in-=" << (S32)(pending_bytes_decrease / 1024 / 1024) + << "MB bias=" << sDesiredDiscardBias + << " ldmin=" << sLastDitchMinDiscard + << " dsq=" << (S32)gTextureList.mDownScaleQueue.size() + << LL_ENDL; } - sMemoryPressureFactor = llclampf(sMemoryPressureFactor); } bool is_sys_low = isSystemMemoryLow(); @@ -2181,12 +2275,11 @@ bool LLViewerFetchedTexture::updateFetch() make_request = false; } else if (mDesiredDiscardLevel > (S32)mCodecMaxDiscardLevel && - current_discard >= 0 && - current_discard <= (S32)mCodecMaxDiscardLevel) + current_discard >= 0) { - // scaleDown can serve this from the GL pyramid. (If current is - // already past codec_max, fall through so a zoom-in can rebuild — - // scaleDown only goes deeper.) + // Desired is past codec_max. Only scaleDown can satisfy it. + // Applies even when current is also past codec_max (post-scaleDown); + // re-fetching at codec_max then scaleDown-ing again is pure thrash. LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > codec max"); make_request = false; } @@ -3177,6 +3270,16 @@ void LLViewerLODTexture::processTextureStats() F32 combined = distance_factor * size_factor; + // VRAM pressure: multiply the combined signal and clamp to 0..1. + // Compresses the effective draw range and picks up close-coverage + // textures (small combined) too. Applied before the channel + // exponent so subsequent transforms see a normalized 0..1 value. + // Avatar bakes exempt. + if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureMultiplier > 1.f) + { + combined = llmin(combined * sMemoryPressureMultiplier, 1.f); + } + // Per-channel exponent. 1.0 = baseline; <1.0 pushes combined // toward 1 (max attenuation) faster. Edges are preserved: // pow(0, p) = 0, pow(1, p) = 1. @@ -3225,16 +3328,6 @@ void LLViewerLODTexture::processTextureStats() combined = llmax(combined, bg); } - // VRAM pressure: pow(combined, 1 - factor) bends the curve - // without flattening it - pow(0, p) = 0 so close textures - // (combined ~ 0) stay near 0 while mid/far push toward 1. - // Avatar bakes exempt. - if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureFactor > 0.f) - { - F32 pressure_exp = llmax(1.f - sMemoryPressureFactor, 0.0001f); - combined = powf(combined, pressure_exp); - } - discard_level = combined * dim_max_for_image; } @@ -3252,21 +3345,36 @@ void LLViewerLODTexture::processTextureStats() mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level); - // Apply the setMinDiscardLevel cap, relaxed proportionally under - // VRAM pressure - at factor=1 the cap reaches dim_max so capped - // textures (terrain, etc.) participate fully in eviction. Caps of - // 0 (thumbnails) and avatar bakes are preserved. + // Apply the setMinDiscardLevel cap, relaxed under VRAM pressure + // (cap_relax = 1 - 1/mult: 0 at mult=1, ~0.5 at mult=2, ~0.9 at + // mult=10). Caps of 0 (thumbnails) and avatar bakes are preserved. S32 effective_min_cap = mMinDesiredDiscardLevel; - if (sMemoryPressureFactor > 0.f && + if (sMemoryPressureMultiplier > 1.f && mMinDesiredDiscardLevel > 0 && mMinDesiredDiscardLevel < S8_MAX && !isAgentAvatarBoost(mBoostLevel)) { + F32 cap_relax = 1.f - 1.f / sMemoryPressureMultiplier; F32 room = (F32)dim_max_for_image_i - (F32)mMinDesiredDiscardLevel; - effective_min_cap += (S32)(sMemoryPressureFactor * room); + effective_min_cap += (S32)(cap_relax * room); effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i); } mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel); + // Last-ditch global discard floor, driven by sLastDitchMinDiscard. + // That state creeps up step-by-step while mult is pegged and we are + // still over budget (see updateClass), and decays when we fit. Force + // every non-avatar-bake texture to at least floor(sLastDitchMinDiscard) + // discard, capped at the per-texture max. + if (!isAgentAvatarBoost(mBoostLevel)) + { + S32 forced = (S32)floorf(sLastDitchMinDiscard); + forced = llclamp(forced, 0, dim_max_for_image_i); + if (forced > mDesiredDiscardLevel) + { + mDesiredDiscardLevel = (S8)forced; + } + } + // // At this point we've calculated the quality level that we want, @@ -3310,6 +3418,23 @@ bool LLViewerLODTexture::scaleDown() return false; } + // Hard structural blocks only. Per-texture policy (icons pinned to full + // res, etc.) lives in processTextureStats; if that policy is later + // relaxed (e.g. honor mKnownDrawWidth for icons rendered at 8x8 in a + // friend list) the scaleDown path stays open. + if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH) + { + // No mip pyramid to drop into, texture is explicitly pinned full res, + // or BOOST_HIGH+ emergency-out (currently only the GLTF "force full + // res" hack hits this). + return false; + } + // Avatar bakes are exempt from mid-bake eviction (cloud avatar risk). + if (isAgentAvatarBoost(mBoostLevel)) + { + return false; + } + if (!mDownScalePending) { mDownScalePending = true; diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index e2215700c0..d40d3bc5ee 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -247,10 +247,14 @@ public: // snaps to 0 in foreground. Avatar bakes exempt. static F32 sBackgroundFactor; - // VRAM-pressure factor, 0..1. Applied in processTextureStats as - // combined = pow(combined, 1 - factor) - bends the curve without - // flattening the distance gradient. - static F32 sMemoryPressureFactor; + // VRAM-pressure distance multiplier, >= 1. Compresses the distance + // signal: dist_factor = clamp(mMinDistanceFactor * mult, 0, 1). + // Grows geometrically while over budget; decays back to 1 when fitting. + static F32 sMemoryPressureMultiplier; + // Last-ditch global discard floor. Creeps up when mult is pegged at cap + // and we are still over budget; decays back to 0 when fitting. Applied + // as a floor on mDesiredDiscardLevel for non-avatar-bake textures. + static F32 sLastDitchMinDiscard; static U32 sBiasTexturesUpdated; static S32 sMaxSculptRez ; static U32 sMinLargeImageSize ; @@ -379,7 +383,7 @@ public: void updateVirtualSize() ; - S32 getDesiredDiscardLevel() { return mDesiredDiscardLevel; } + S32 getDesiredDiscardLevel() const { return mDesiredDiscardLevel; } void setMinDiscardLevel(S32 discard) { mMinDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel,(S8)discard); } void setBoostLevel(S32 level) override; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 2e66dc2a11..ee21d6a4dd 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -71,6 +71,7 @@ void (*LLViewerTextureList::sUUIDCallback)(void **, const LLUUID&) = NULL; S32 LLViewerTextureList::sNumImages = 0; +F32 LLViewerTextureList::sCurrentBubbleMeters = 0.f; LLViewerTextureList gTextureList; @@ -937,8 +938,40 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag // dist_factor = 0 (no discard contribution). The ramp from 0 -> 1 // spans (bubble, draw_distance] rather than (0, draw_distance]. static LLCachedControl close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f); - F32 bubble = llclamp((F32)close_bubble, 0.f, draw_distance - 0.001f); + static LLCachedControl close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f); + static LLCachedControl max_pressure_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + static LLCachedControl bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f); + static LLCachedControl bubble_track_rate(gSavedSettings, "TextureCloseBubbleTrackRate", 0.5f); + F32 bubble_full = llmax((F32)close_bubble, 0.f); + F32 bubble_min = llclamp((F32)close_bubble_min, 0.f, bubble_full); + // Target bubble: stay at full size until pressure multiplier is + // deep into its range, then collapse toward bubble_min. The bubble + // is an emergency response, not part of the normal feedback loop. + F32 mult_cap = llmax((F32)max_pressure_mult, 1.0001f); + F32 mult_progress = llclampf((LLViewerTexture::sMemoryPressureMultiplier - 1.f) / (mult_cap - 1.f)); + F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold); + F32 shrink_t = (mult_progress > shrink_thresh) + ? (mult_progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f) + : 0.f; + F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_t; + // Slow-track the actual bubble toward target so short-term multiplier + // swings don't yo-yo close textures in and out. Advance the state + // ONCE per frame, not per texture - this function runs once per + // texture so a naive per-call lerp converges in a single frame. + static F32 s_tracked_bubble = -1.f; + static U32 s_tracked_bubble_frame = 0; + if (s_tracked_bubble < 0.f) s_tracked_bubble = bubble_full; + if (s_tracked_bubble_frame != LLFrameTimer::getFrameCount()) + { + s_tracked_bubble_frame = LLFrameTimer::getFrameCount(); + F32 dt = (F32)gFrameIntervalSeconds; + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)bubble_track_rate, 0.f)); + s_tracked_bubble += (target_bubble - s_tracked_bubble) * alpha; + s_tracked_bubble = llclamp(s_tracked_bubble, bubble_min, bubble_full); + } + F32 bubble = llclamp(s_tracked_bubble, 0.f, draw_distance - 0.001f); F32 ramp_range = llmax(draw_distance - bubble, 0.001f); + sCurrentBubbleMeters = bubble; U32 face_count = 0; U32 max_faces_to_check = 1024; @@ -1107,10 +1140,15 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 grace = llmax((F32)bind_decay_seconds, 0.f); F32 interval = llmax((F32)staleness_interval, 0.0001f); - bool ever_bound = (gli->mLastBindTime > 0.f); - F32 time_since_bind = ever_bound ? (LLImageGL::sLastFrameTime - gli->mLastBindTime) : 0.f; + // Clock starts at whichever is later: the last real bind or + // the GL-create time. The latter is the fallback for textures + // decoded into GL but never actually rendered - without it, + // mLastBindTime stays 0 forever and staleness can't evict. + F32 clock_time = llmax(gli->mLastBindTime, gli->mGLCreateTime); + bool has_clock = (clock_time > 0.f); + F32 time_since = has_clock ? (LLImageGL::sLastFrameTime - clock_time) : 0.f; - if (!ever_bound || time_since_bind <= grace) + if (!has_clock || time_since <= grace) { imagep->mStalenessFactor = 0.f; } @@ -1123,7 +1161,7 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag : (S32)gli->getMaxDiscardLevel(); if (max_discard > 0) { - F32 steps = (time_since_bind - grace) / interval; + F32 steps = (time_since - grace) / interval; F32 step_size = 1.f / (F32)max_discard; imagep->mStalenessFactor = llclampf(steps * step_size); } @@ -1270,10 +1308,13 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) // give time to downscaling first - if mDownScaleQueue is not empty, we're running out of memory and need // to free up memory by discarding off screen textures quickly - // do at least 5 and make sure we don't get too far behind even if it violates - // the time limit. If we don't downscale quickly the viewer will hit swap and may - // freeze. - S32 min_count = (S32)mCreateTextureList.size() / 20 + 5; + // Drain rate scales with both pending creates and the downscale + // queue itself. Without the queue term, a backlog of evictions + // could only drain 5/frame regardless of size, and the system + // can't actually free VRAM fast enough under pressure. + S32 min_count = (S32)mCreateTextureList.size() / 20 + + (S32)mDownScaleQueue.size() / 5 + + 5; create_timer.reset(); while (!mDownScaleQueue.empty()) @@ -1375,12 +1416,15 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time) //update MIN_UPDATE_COUNT or 5% of other textures, whichever is greater update_count = llmax((U32) MIN_UPDATE_COUNT, (U32) mUUIDMap.size()/20); - if (LLViewerTexture::sDesiredDiscardBias > 1.f + // Scale up the per-frame update window under VRAM pressure so eviction + // candidates get re-evaluated quickly. Both the legacy bias and the + // new pressure multiplier widen the window. + F32 pressure_scale = llmax(LLViewerTexture::sDesiredDiscardBias, + LLViewerTexture::sMemoryPressureMultiplier); + if (pressure_scale > 1.f && LLViewerTexture::sBiasTexturesUpdated < (U32)mUUIDMap.size()) { - // We are over memory target. Bias affects discard rates, so update - // existing textures agresively to free memory faster. - update_count = (S32)(update_count * LLViewerTexture::sDesiredDiscardBias); + update_count = (S32)(update_count * pressure_scale); // This isn't particularly precise and can overshoot, but it doesn't need // to be, just making sure it did a full circle and doesn't get stuck updating diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 931f2ed50e..dbed8b5c2f 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -244,6 +244,10 @@ private: bool mInitialized ; LLFrameTimer mForceDecodeTimer; +public: + // Current close-camera bubble in meters (frame-coherent, slow-tracked). + static F32 sCurrentBubbleMeters; + private: static S32 sNumImages; static void (*sUUIDCallback)(void**, const LLUUID &); -- cgit v1.3 From bbc850997cd095188127d971f6cb90052615849a Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 4 May 2026 23:31:14 +0300 Subject: #5756 Improve LLUICtrlFactory's performance --- indra/llui/lluictrlfactory.cpp | 2 +- indra/llui/lluictrlfactory.h | 11 ++++++++--- indra/newview/llappviewer.cpp | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/llui/lluictrlfactory.cpp b/indra/llui/lluictrlfactory.cpp index 33ffc3dfc6..740f187ed8 100644 --- a/indra/llui/lluictrlfactory.cpp +++ b/indra/llui/lluictrlfactory.cpp @@ -67,7 +67,7 @@ public: static LLDefaultChildRegistry::Register r1("locate"); // Build time optimization, generate this once in .cpp file -template class LLUICtrlFactory* LLSingleton::getInstance(); +template class LLUICtrlFactory* LLSimpleton::getInstance(); //----------------------------------------------------------------------------- // LLUICtrlFactory() diff --git a/indra/llui/lluictrlfactory.h b/indra/llui/lluictrlfactory.h index f44b4ba4dc..75cee5f004 100644 --- a/indra/llui/lluictrlfactory.h +++ b/indra/llui/lluictrlfactory.h @@ -81,12 +81,13 @@ class LLWidgetNameRegistry // Build time optimization, generate this once in .cpp file #ifndef LLUICTRLFACTORY_CPP -extern template class LLUICtrlFactory* LLSingleton::getInstance(); +extern template class LLUICtrlFactory* LLSimpleton::getInstance(); #endif -class LLUICtrlFactory : public LLSingleton +class LLUICtrlFactory : public LLSimpleton { - LLSINGLETON(LLUICtrlFactory); +public: + LLUICtrlFactory(); ~LLUICtrlFactory(); // only partial specialization allowed in inner classes, so use extra dummy parameter @@ -313,6 +314,10 @@ template LLChildRegistry::Register::Register(const char* tag, LLWidgetCreatorFunc func) : LLChildRegistry::StaticRegistrar(tag, func == nullptr ? (LLWidgetCreatorFunc)&LLUICtrlFactory::defaultBuilder : func) { + if (!LLUICtrlFactory::instanceExists()) + { + LLUICtrlFactory::createInstance(); + } // add this widget to various registries LLUICtrlFactory::instance().registerWidget(typeid(T), typeid(typename T::Params), tag); diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index bae9749772..8343cc3218 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -2183,6 +2183,8 @@ bool LLAppViewer::cleanup() // deleteSingleton() methods. LLSingletonBase::deleteAll(); + LLUICtrlFactory::deleteSingleton(); + LLSplashScreen::hide(); LL_INFOS() << "Goodbye!" << LL_ENDL; -- cgit v1.3 From c4bc64c7515c19bfb1c4d619a8e5d235954ba8a4 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 19 May 2026 13:32:58 -0400 Subject: Rework VRAM controller and bubble interaction — gate iteration on pressure, halve last-ditch floor inside bubble. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- indra/newview/app_settings/settings.xml | 11 ----- indra/newview/llviewertexture.cpp | 79 +++++++++++++++++---------------- indra/newview/llviewertexture.h | 10 +++-- indra/newview/llviewertexturelist.cpp | 31 +++++-------- 4 files changed, 59 insertions(+), 72 deletions(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 0c4df7a805..d33082b449 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11871,17 +11871,6 @@ Value 0 - TextureMemoryPressureRampRate - - Comment - Geometric ramp/decay rate (per second) for the VRAM-pressure distance multiplier (>= 1). Higher = faster convergence on the budget; lower = gentler. Multiplier compresses the streaming distance signal: at mult=N, faces past (ramp_range / N) hit max discard. - Persist - 1 - Type - F32 - Value - 1.0 - TextureMemoryPressureBackoffStart Comment diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index e55e4016ec..e84481bade 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -90,6 +90,14 @@ F32 LLViewerTexture::sDesiredDiscardBias = 0.f; F32 LLViewerTexture::sBackgroundFactor = 0.f; F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f; F32 LLViewerTexture::sLastDitchMinDiscard = 0.f; + +//static +F32 LLViewerTexture::getMemoryPressureProgress() +{ + static LLCachedControl max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + F32 cap = llmax((F32)max_mult, 1.0001f); + return llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); +} U32 LLViewerTexture::sBiasTexturesUpdated = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size @@ -533,12 +541,9 @@ void LLViewerTexture::updateClass() F32 over_pct = (used - target) / target; - // VRAM-pressure controller. Drives sMemoryPressureMultiplier from - // PREDICTED VRAM (used + in-flight refetch growth - in-flight downscale - // shrinkage) rather than instantaneous used. The feedback loop chasing - // instantaneous VRAM sawtooths because eviction is fast but refetch is - // slow; the prediction lets the controller see the equilibrium directly, - // so mult converges to a target value instead of cycling. + // Predicted-VRAM pressure controller. Eviction is fast, refetch is slow, + // so feedback on instantaneous `used` sawtooths; feeding `used + + // in_flight_delta` lets mult converge to equilibrium instead of cycling. { static LLCachedControl backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f); static LLCachedControl max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); @@ -546,24 +551,34 @@ void LLViewerTexture::updateClass() static LLCachedControl smoothing_rate(gSavedSettings, "TextureMemoryPressureSmoothingRate", 4.f); F32 backoff_target = target * llclamp((F32)backoff_start, 0.05f, 1.f); + F32 cap = llmax((F32)max_mult, 1.0001f); + F32 dt = (F32)gFrameIntervalSeconds; + + // Skip the full-list iteration when there is no pressure to react to: + // mult already at baseline, last-ditch at zero, and used well clear of + // the backoff target. Worst case the controller picks up the spike one + // frame later, from `used` alone. + bool need_predict = sMemoryPressureMultiplier > 1.001f + || sLastDitchMinDiscard > 0.f + || used > backoff_target * 0.5f; - // Walk the texture list once per frame, summing pending size deltas. - // Approximation: bytes(d) = (w>>d) * (h>>d) * 4 * 4/3. Units match - // `used` after the /1024/512 conversion that produced it. Coarse but - // proportionally correct - the gain knob tunes absolute magnitude. S64 pending_bytes_increase = 0; S64 pending_bytes_decrease = 0; + if (need_predict) { LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict"); for (auto& imagep : gTextureList) { if (imagep.isNull()) continue; + // Cheap inline checks first so the virtual getDiscardLevel() + // call only fires when there is a real chance of contribution. + S32 desired = imagep->getDesiredDiscardLevel(); + if (desired < 0) continue; S32 fw = imagep->getFullWidth(); S32 fh = imagep->getFullHeight(); if (fw <= 0 || fh <= 0) continue; - S32 desired = imagep->getDesiredDiscardLevel(); S32 current = imagep->getDiscardLevel(); - if (desired < 0 || current < 0 || desired == current) continue; + if (current < 0 || desired == current) continue; S32 wd = llmax(1, fw >> desired); S32 hd = llmax(1, fh >> desired); @@ -580,36 +595,26 @@ void LLViewerTexture::updateClass() } } - // Match the unit conversion used to produce `used` (texture_bytes_alloc - // divided by 1024/512). 1024*512 = 524288. + // 1024 * 512 = 524288: matches the unit reduction at line 513. constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f; F32 predicted_used = used + (F32)pending_bytes_increase * BYTES_TO_USED_UNITS - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS; F32 predicted_over = predicted_used / llmax(backoff_target, 1.f); - // Direct target: at over=1, mult=1; growth governed by gain power. - // Smooth toward target so per-frame prediction noise doesn't jolt - // the controller. Lerp rate set so a step change in target reaches - // ~63% in 1/smoothing_rate seconds (default 0.25s). - F32 cap = llmax((F32)max_mult, 1.0001f); F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); - F32 dt = (F32)gFrameIntervalSeconds; + // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap); - // Last-ditch global discard floor. Ramps up when mult is pegged near - // cap and we are still over budget; decays toward 0 when fitting. Once - // the normal compression knobs are exhausted, this creeps the discard - // floor up integer step by step, mirroring how sDesiredDiscardBias - // pushes background textures. + F32 progress = getMemoryPressureProgress(); + { static LLCachedControl ld_engage(gSavedSettings, "TextureLastDitchEngageProgress", 0.95f); static LLCachedControl ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f); static LLCachedControl ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f); static LLCachedControl ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f); - F32 progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); bool mult_saturated = progress >= llclampf((F32)ld_engage); if (mult_saturated && predicted_over > 1.f) { @@ -622,18 +627,16 @@ void LLViewerTexture::updateClass() sLastDitchMinDiscard = llclamp(sLastDitchMinDiscard, 0.f, llmax((F32)ld_max, 0.f)); } - F32 over = used / llmax(backoff_target, 1.f); // legacy alias used below - - // Throttled bisection log. Once per second. + // 1 Hz pressure log. static LLFrameTimer s_pressure_log_timer; if (s_pressure_log_timer.getElapsedTimeF32() > 1.f) { s_pressure_log_timer.reset(); - F32 mult_progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); + F32 over = used / llmax(backoff_target, 1.f); LL_INFOS("TextureStream") << "pressure" << " mult=" << sMemoryPressureMultiplier << " target_mult=" << target_mult - << " progress=" << mult_progress + << " progress=" << progress << " used=" << used << " predicted=" << predicted_used << " target=" << target @@ -3360,14 +3363,13 @@ void LLViewerLODTexture::processTextureStats() } mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel); - // Last-ditch global discard floor, driven by sLastDitchMinDiscard. - // That state creeps up step-by-step while mult is pegged and we are - // still over budget (see updateClass), and decays when we fit. Force - // every non-avatar-bake texture to at least floor(sLastDitchMinDiscard) - // discard, capped at the per-texture max. + // Halve the floor for bubble-resident textures (mMinDistanceFactor == 0 + // = at least one face inside the bubble) so the close-vs-far gradient + // is preserved at every pressure level. if (!isAgentAvatarBoost(mBoostLevel)) { S32 forced = (S32)floorf(sLastDitchMinDiscard); + if (mMinDistanceFactor <= 0.f) forced /= 2; forced = llclamp(forced, 0, dim_max_for_image_i); if (forced > mDesiredDiscardLevel) { @@ -3422,11 +3424,10 @@ bool LLViewerLODTexture::scaleDown() // res, etc.) lives in processTextureStats; if that policy is later // relaxed (e.g. honor mKnownDrawWidth for icons rendered at 8x8 in a // friend list) the scaleDown path stays open. + // BOOST_HIGH is the emergency-out for GLTF's "force full res" hack; + // the other two flags are structural. if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH) { - // No mip pyramid to drop into, texture is explicitly pinned full res, - // or BOOST_HIGH+ emergency-out (currently only the GLTF "force full - // res" hack hits this). return false; } // Avatar bakes are exempt from mid-bake eviction (cloud avatar risk). diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index d40d3bc5ee..991bb638a1 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -251,10 +251,14 @@ public: // signal: dist_factor = clamp(mMinDistanceFactor * mult, 0, 1). // Grows geometrically while over budget; decays back to 1 when fitting. static F32 sMemoryPressureMultiplier; - // Last-ditch global discard floor. Creeps up when mult is pegged at cap - // and we are still over budget; decays back to 0 when fitting. Applied - // as a floor on mDesiredDiscardLevel for non-avatar-bake textures. + // Last-ditch global discard floor. Mirrors sDesiredDiscardBias once the + // multiplier is exhausted. static F32 sLastDitchMinDiscard; + + // 0..1 progress of the pressure multiplier from baseline (1) to its + // configured cap (TextureMemoryPressureMaxMultiplier). Used to gate + // bubble shrink and last-ditch engagement. + static F32 getMemoryPressureProgress(); static U32 sBiasTexturesUpdated; static S32 sMaxSculptRez ; static U32 sMinLargeImageSize ; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index ee21d6a4dd..4d09eff74e 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -934,44 +934,37 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag bool on_agent_avatar = false; F32 draw_distance = llmax(gAgentCamera.mDrawDistance, 0.001f); - // Close-camera bubble: distances under bubble_meters resolve to - // dist_factor = 0 (no discard contribution). The ramp from 0 -> 1 - // spans (bubble, draw_distance] rather than (0, draw_distance]. + // Close-camera bubble: faces inside `bubble` meters resolve to + // dist_factor = 0, so the distance ramp spans (bubble, draw_distance]. static LLCachedControl close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f); static LLCachedControl close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f); - static LLCachedControl max_pressure_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); static LLCachedControl bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f); static LLCachedControl bubble_track_rate(gSavedSettings, "TextureCloseBubbleTrackRate", 0.5f); F32 bubble_full = llmax((F32)close_bubble, 0.f); F32 bubble_min = llclamp((F32)close_bubble_min, 0.f, bubble_full); - // Target bubble: stay at full size until pressure multiplier is - // deep into its range, then collapse toward bubble_min. The bubble - // is an emergency response, not part of the normal feedback loop. - F32 mult_cap = llmax((F32)max_pressure_mult, 1.0001f); - F32 mult_progress = llclampf((LLViewerTexture::sMemoryPressureMultiplier - 1.f) / (mult_cap - 1.f)); - F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold); - F32 shrink_t = (mult_progress > shrink_thresh) - ? (mult_progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f) - : 0.f; - F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_t; - // Slow-track the actual bubble toward target so short-term multiplier - // swings don't yo-yo close textures in and out. Advance the state - // ONCE per frame, not per texture - this function runs once per - // texture so a naive per-call lerp converges in a single frame. + // Advance the slow-track once per frame, not per texture: this + // function runs once per texture so a naive per-call lerp converges + // in a single frame. static F32 s_tracked_bubble = -1.f; static U32 s_tracked_bubble_frame = 0; if (s_tracked_bubble < 0.f) s_tracked_bubble = bubble_full; if (s_tracked_bubble_frame != LLFrameTimer::getFrameCount()) { s_tracked_bubble_frame = LLFrameTimer::getFrameCount(); + F32 progress = LLViewerTexture::getMemoryPressureProgress(); + F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold); + F32 shrink_frac = (progress > shrink_thresh) + ? (progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f) + : 0.f; + F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_frac; F32 dt = (F32)gFrameIntervalSeconds; F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)bubble_track_rate, 0.f)); s_tracked_bubble += (target_bubble - s_tracked_bubble) * alpha; s_tracked_bubble = llclamp(s_tracked_bubble, bubble_min, bubble_full); + sCurrentBubbleMeters = s_tracked_bubble; } F32 bubble = llclamp(s_tracked_bubble, 0.f, draw_distance - 0.001f); F32 ramp_range = llmax(draw_distance - bubble, 0.001f); - sCurrentBubbleMeters = bubble; U32 face_count = 0; U32 max_faces_to_check = 1024; -- cgit v1.3 From 87b333f9cfb4cd7efc3aa38bcd94cc62ecbec1e8 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 20 May 2026 00:19:20 +0300 Subject: #5809 Add 'mute' based filter for conversation history - Don't add blocked conversations - Menus and global setting to control this mechanics - Refresh on changes Decided just not to add blocked conversation to the list, an alternate way is to alter visibility logic or filter logic --- indra/newview/app_settings/settings.xml | 11 ++++ indra/newview/llconversationloglist.cpp | 65 ++++++++++++++++++++-- indra/newview/llconversationloglist.h | 10 +++- indra/newview/llfloaterconversationlog.cpp | 8 +++ .../default/xui/en/menu_conversation_log_view.xml | 10 ++++ 5 files changed, 99 insertions(+), 5 deletions(-) (limited to 'indra') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 6946ffa396..ec78603cfe 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11119,6 +11119,17 @@ Value 1 + ShowBlockedConvHistory + + Comment + Specifies whether blocked agents will be shown in Call Log + Persist + 1 + Type + Boolean + Value + 0 + ShowMatureEvents Comment diff --git a/indra/newview/llconversationloglist.cpp b/indra/newview/llconversationloglist.cpp index 65863f0a5e..e9c6df3ed8 100644 --- a/indra/newview/llconversationloglist.cpp +++ b/indra/newview/llconversationloglist.cpp @@ -45,6 +45,7 @@ LLConversationLogList::LLConversationLogList(const Params& p) mIsDirty(true) { LLConversationLog::instance().addObserver(this); + LLMuteList::instance().addObserver(this); // Set up context menu. LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; @@ -65,6 +66,15 @@ LLConversationLogList::LLConversationLogList(const Params& p) } mIsFriendsOnTop = gSavedSettings.getBOOL("SortFriendsFirst"); + LLControlVariable* cntrl_ptr = gSavedSettings.getControl("ShowBlockedConvHistory"); + if (cntrl_ptr) + { + mShowBlockedSignal = cntrl_ptr->getCommitSignal()->connect( + [this](LLControlVariable* control, const LLSD& value, const LLSD& previous) + { + setDirty(true); + }); + } } LLConversationLogList::~LLConversationLogList() @@ -74,6 +84,7 @@ LLConversationLogList::~LLConversationLogList() mContextMenu.get()->die(); } + LLMuteList::getInstance()->removeObserver(this); LLConversationLog::instance().removeObserver(this); } @@ -178,6 +189,44 @@ void LLConversationLogList::changed(const LLUUID& session_id, U32 mask) } } +// inherited from LLMuteListObserver +void LLConversationLogList::onChange() +{ +} + +// inherited from LLMuteListObserver +void LLConversationLogList::onChangeDetailed(const LLMute& mute) +{ + if (isDirty()) + { + // Going to refresh either way. + return; + } + + static LLCachedControl show_blocked(gSavedSettings, "ShowBlockedConvHistory", false); + if (!show_blocked()) + { + // Check if any conversation in the log has this participant + const std::vector& conversations = LLConversationLog::instance().getConversations(); + + // Note sure if actually need this update. List can be rather large + // and rebuilding it on every event can be expensive. + // But 'mute' events are also rare, so maybe not a problem. + // There also might be a way to react only to relevant changes, + // instead of reacting to everything, like ignoring voice only mutes, + // which are not relevant for this log by themselves. + for (const LLConversation& conversation : conversations) + { + if (conversation.getParticipantID() == mute.mID) + { + setDirty(true); + break; + } + } + } + // else don't need to hide anything +} + void LLConversationLogList::addNewItem(const LLConversation* conversation) { LLConversationLogListItem* item = new LLConversationLogListItem(&*conversation); @@ -207,6 +256,9 @@ void LLConversationLogList::rebuildList() const std::vector& conversations = log_instance.getConversations(); std::vector::const_iterator iter = conversations.begin(); + LLMuteList* mutes = LLMuteList::getInstance(); + + static LLCachedControl show_blocked(gSavedSettings, "ShowBlockedConvHistory", false); for (; iter != conversations.end(); ++iter) { @@ -214,6 +266,11 @@ void LLConversationLogList::rebuildList() if (not_found) continue; + if (!show_blocked() && mutes->isMuted(iter->getParticipantID())) + { + continue; + } + addNewItem(&*iter); } @@ -517,8 +574,8 @@ bool LLConversationLogListNameComparator::doCompare(const LLConversationLogListI LLStringUtil::toUpper(name1); LLStringUtil::toUpper(name2); - bool friends_first = gSavedSettings.getBOOL("SortFriendsFirst"); - if (friends_first && (LLAvatarActions::isFriend(id1) ^ LLAvatarActions::isFriend(id2))) + static LLCachedControl sort_friends_first(gSavedSettings, "SortFriendsFirst", true); + if (sort_friends_first() && (LLAvatarActions::isFriend(id1) ^ LLAvatarActions::isFriend(id2))) { return LLAvatarActions::isFriend(id1); } @@ -533,8 +590,8 @@ bool LLConversationLogListDateComparator::doCompare(const LLConversationLogListI const LLUUID& id1 = conversation1->getConversation()->getParticipantID(); const LLUUID& id2 = conversation2->getConversation()->getParticipantID(); - bool friends_first = gSavedSettings.getBOOL("SortFriendsFirst"); - if (friends_first && (LLAvatarActions::isFriend(id1) ^ LLAvatarActions::isFriend(id2))) + static LLCachedControl sort_friends_first(gSavedSettings, "SortFriendsFirst", true); + if (sort_friends_first() && (LLAvatarActions::isFriend(id1) ^ LLAvatarActions::isFriend(id2))) { return LLAvatarActions::isFriend(id1); } diff --git a/indra/newview/llconversationloglist.h b/indra/newview/llconversationloglist.h index 6372f6c9a3..39f129c9f1 100644 --- a/indra/newview/llconversationloglist.h +++ b/indra/newview/llconversationloglist.h @@ -39,7 +39,7 @@ class LLConversationLogListItem; * it's always in actual state. */ -class LLConversationLogList: public LLFlatListViewEx, public LLConversationLogObserver +class LLConversationLogList: public LLFlatListViewEx, public LLConversationLogObserver, public LLMuteListObserver { LOG_CLASS(LLConversationLogList); public: @@ -76,6 +76,12 @@ public: virtual void changed(); virtual void changed(const LLUUID& session_id, U32 mask); + /** + * Changes from LLMuteListObserver + */ + virtual void onChange(); + virtual void onChangeDetailed(const LLMute& mute); + private: void setDirty(bool dirty = true) { mIsDirty = dirty; } @@ -104,6 +110,8 @@ private: bool mIsDirty; bool mIsFriendsOnTop; std::string mNameFilter; + + boost::signals2::scoped_connection mShowBlockedSignal; }; /** diff --git a/indra/newview/llfloaterconversationlog.cpp b/indra/newview/llfloaterconversationlog.cpp index 97399c9cf7..40e2d2bf1e 100644 --- a/indra/newview/llfloaterconversationlog.cpp +++ b/indra/newview/llfloaterconversationlog.cpp @@ -100,6 +100,10 @@ void LLFloaterConversationLog::onCustomAction (const LLSD& userdata) { mConversationLogList->toggleSortFriendsOnTop(); } + else if ("show_blocked" == command_name) + { + gSavedSettings.setBOOL("ShowBlockedConvHistory", !gSavedSettings.getBOOL("ShowBlockedConvHistory")); + } else if ("view_nearby_chat_history" == command_name) { LLFloaterReg::showInstance("preview_conversation", LLSD(LLUUID::null), true); @@ -129,6 +133,10 @@ bool LLFloaterConversationLog::isActionChecked(const LLSD& userdata) { return gSavedSettings.getBOOL("SortFriendsFirst"); } + else if ("show_blocked" == command_name) + { + return gSavedSettings.getBOOL("ShowBlockedConvHistory"); + } return false; } diff --git a/indra/newview/skins/default/xui/en/menu_conversation_log_view.xml b/indra/newview/skins/default/xui/en/menu_conversation_log_view.xml index 864bec5d9a..1e10a984da 100644 --- a/indra/newview/skins/default/xui/en/menu_conversation_log_view.xml +++ b/indra/newview/skins/default/xui/en/menu_conversation_log_view.xml @@ -34,6 +34,16 @@ function="CallLog.Check" parameter="sort_friends_on_top" /> + + + + Date: Fri, 15 May 2026 19:11:26 +0300 Subject: #5804 Prevent focus steal when removing unselected conversation widget --- indra/newview/llfloaterimcontainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index a0f2dbe197..e80133f93c 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -1948,7 +1948,7 @@ bool LLFloaterIMContainer::removeConversationListItem(const LLUUID& uuid, bool c mConversationEventQueue.erase(uuid); // Don't let the focus fall IW, select and refocus on the first conversation in the list - if (change_focus && isInVisibleChain()) + if (change_focus && is_widget_selected && isInVisibleChain()) { setFocus(true); if (new_selection) -- cgit v1.3 From 1ee317bcb7a5988658612a69a7bd52fb1a9e37c7 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 19 May 2026 23:11:16 -0400 Subject: Bring back the pre-aug 2024 byte estimation for j2c streams, and add a configurable high water mark for VRAM. --- indra/llimage/llimagej2c.cpp | 54 +++++++++++++++++---------------- indra/llimage/llimagej2c.h | 6 ++++ indra/llimagej2coj/llimagej2coj.cpp | 29 ++++++++++++++++++ indra/llimagej2coj/llimagej2coj.h | 5 +++ indra/newview/app_settings/settings.xml | 11 +++++++ indra/newview/llviewertexture.cpp | 29 ++++++++++++++---- 6 files changed, 102 insertions(+), 32 deletions(-) (limited to 'indra') diff --git a/indra/llimage/llimagej2c.cpp b/indra/llimage/llimagej2c.cpp index 5a941dc958..512b370bae 100644 --- a/indra/llimage/llimagej2c.cpp +++ b/indra/llimage/llimagej2c.cpp @@ -265,40 +265,42 @@ S32 LLImageJ2C::calcHeaderSizeJ2C() return FIRST_PACKET_SIZE; // Hack. just needs to be >= actual header size... } -//static -S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) +// Lean pyramid-walk byte estimator suited to packet-by-packet decoders (KDU). +// Starts at one max-block, walks resolutions by doubling area, sums each +// layer's compressed-bytes contribution. +// Reference: https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study +S32 LLImageJ2CImpl::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const { - // Note: This provides an estimation for the first to last quality layer of a given discard level - // This is however an efficient approximation, as the true discard level boundary would be - // in general too big for fast fetching. - // For details about the equation used here, see https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study - - // Estimate the number of layers. This is consistent with what's done for j2c encoding in LLImageJ2CKDU::encodeImpl(). - constexpr S32 precision = 8; // assumed bitrate per component channel, might change in future for HDR support - constexpr S32 max_components = 4; // assumed the file has four components; three color and alpha - // Use MAX_IMAGE_SIZE_DEFAULT (currently 2048) if either dimension is unknown (zero) + constexpr S32 precision = 8; + constexpr S32 max_components = 4; S32 width = (w > 0) ? w : 2048; S32 height = (h > 0) ? h : 2048; - S32 max_dimension = llmax(width, height); // Find largest dimension - S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; // Calculated initial block area from established max block size (currently 64) - S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); // Find number of powers of two between extents and block size to a minimum of 4 - block_area *= llmax(max_layers, 1); // Adjust initial block area by max number of layers - S32 totalbytes = (S32) (MIN_LAYER_SIZE * max_components * precision); // Start estimation with a minimum reasonable size - S32 block_layers = 0; - while (block_layers <= max_layers) // Walk the layers + const S32 surface = width * height; + S32 nb_layers = 1; + S32 s = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; + S32 totalbytes = (S32)(s * max_components * precision * rate); + while (surface > s) { - if (block_layers <= (5 - discard_level)) // Walk backwards from discard 5 to required discard layer. - totalbytes += (S32) (block_area * max_components * precision * rate); // Add each block layer reduced by assumed compression rate - block_layers++; // Move to next layer - block_area *= 4; // Increase block area by power of four + if (nb_layers <= (5 - discard_level)) + totalbytes += (S32)(s * max_components * precision * rate); + nb_layers++; + s *= 4; } - - totalbytes /= 8; // to bytes - totalbytes += calcHeaderSizeJ2C(); // header - + totalbytes /= 8; + totalbytes += LLImageJ2C::calcHeaderSizeJ2C(); return totalbytes; } +//static +S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) +{ + // Dispatch to the linked impl so OpenJPEG (block-aligned, needs + // over-allocation) and KDU (packet-aligned, lean) each return what + // their decoder actually needs. + static std::unique_ptr s_estimator(fallbackCreateLLImageJ2CImpl()); + return s_estimator->estimateDataSize(w, h, comp, discard_level, rate); +} + S32 LLImageJ2C::calcHeaderSize() { return calcHeaderSizeJ2C(); diff --git a/indra/llimage/llimagej2c.h b/indra/llimage/llimagej2c.h index 19744a7f87..5173eed33b 100644 --- a/indra/llimage/llimagej2c.h +++ b/indra/llimage/llimagej2c.h @@ -106,6 +106,12 @@ class LLImageJ2CImpl { public: virtual ~LLImageJ2CImpl(); + + // Estimate the byte size of a J2C codestream sufficient to decode the + // given discard level. KDU streams packet-by-packet and uses the lean + // pyramid-walk default. OpenJPEG needs over-allocation to land on + // code-block boundaries even with strict mode off, so it overrides. + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const; protected: // Find out the image size and number of channels. // Return value: diff --git a/indra/llimagej2coj/llimagej2coj.cpp b/indra/llimagej2coj/llimagej2coj.cpp index 7cfadb889d..488c07e1c9 100644 --- a/indra/llimagej2coj/llimagej2coj.cpp +++ b/indra/llimagej2coj/llimagej2coj.cpp @@ -919,3 +919,32 @@ bool LLImageJ2COJ::getMetadata(LLImageJ2C &base) base.setSize(width, height, components); return true; } + + +// OpenJPEG-tuned byte estimator. Conservative pyramid walk that accounts for +// OJ's whole-code-block decode behavior (even with strict mode off). Larger +// images get a per-resolution multiplier so the byte range lands inside the +// last needed code-block boundary. +S32 LLImageJ2COJ::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const +{ + constexpr S32 precision = 8; + constexpr S32 max_components = 4; + S32 width = (w > 0) ? w : 2048; + S32 height = (h > 0) ? h : 2048; + S32 max_dimension = llmax(width, height); + S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; + S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); + block_area *= llmax(max_layers, 1); + S32 totalbytes = (S32)(MIN_LAYER_SIZE * max_components * precision); + S32 block_layers = 0; + while (block_layers <= max_layers) + { + if (block_layers <= (5 - discard_level)) + totalbytes += (S32)(block_area * max_components * precision * rate); + block_layers++; + block_area *= 4; + } + totalbytes /= 8; + totalbytes += LLImageJ2C::calcHeaderSizeJ2C(); + return totalbytes; +} diff --git a/indra/llimagej2coj/llimagej2coj.h b/indra/llimagej2coj/llimagej2coj.h index da49597302..6c6b379e5f 100644 --- a/indra/llimagej2coj/llimagej2coj.h +++ b/indra/llimagej2coj/llimagej2coj.h @@ -44,6 +44,11 @@ protected: virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL); virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0); virtual std::string getEngineInfo() const; +public: + // OpenJPEG decodes whole code-blocks even with strict mode off, so the + // lean packet-walk under-allocates and clips quality. Keep the older + // conservative pyramid-with-multiplier estimate here. + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const override; }; #endif diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d33082b449..0b700c28ae 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11871,6 +11871,17 @@ Value 0 + TextureMemoryHighWaterMark + + Comment + Fraction of budget (0..1) at which the pressure controller bypasses smoothing and slams to cap. Last-ditch min-discard also creeps without waiting for mult_progress. + Persist + 1 + Type + F32 + Value + 0.8 + TextureMemoryPressureBackoffStart Comment diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index e84481bade..82f4cc8341 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -602,10 +602,25 @@ void LLViewerTexture::updateClass() - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS; F32 predicted_over = predicted_used / llmax(backoff_target, 1.f); + // High water mark: when used crosses budget * high_water, skip the + // smoothed convergence and slam the controller into hard-cap state. + // Recovers the historical 90% behavior - immediate aggressive + // response instead of waiting for the lerp to chase the target. + static LLCachedControl high_water(gSavedSettings, "TextureMemoryHighWaterMark", 0.8f); + bool above_high_water = used >= budget * llclamp((F32)high_water, 0.5f, 1.f); + F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); - // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). - F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); - sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + if (above_high_water) + { + target_mult = cap; + sMemoryPressureMultiplier = cap; + } + else + { + // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); + sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + } sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap); F32 progress = getMemoryPressureProgress(); @@ -615,12 +630,14 @@ void LLViewerTexture::updateClass() static LLCachedControl ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f); static LLCachedControl ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f); static LLCachedControl ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f); - bool mult_saturated = progress >= llclampf((F32)ld_engage); - if (mult_saturated && predicted_over > 1.f) + // Above the high water mark, last-ditch creeps regardless of + // mult_progress: by definition we are out of normal headroom. + bool engage = above_high_water || progress >= llclampf((F32)ld_engage); + if (engage && predicted_over > 1.f) { sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt; } - else if (predicted_over < 1.f) + else if (!above_high_water && predicted_over < 1.f) { sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt; } -- cgit v1.3 From a4fdf69c2dae9ed5fafe4d292bfb4cd1ce881441 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 20 May 2026 00:36:25 -0400 Subject: Make tests happy. --- indra/llkdu/tests/llimagej2ckdu_test.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'indra') diff --git a/indra/llkdu/tests/llimagej2ckdu_test.cpp b/indra/llkdu/tests/llimagej2ckdu_test.cpp index bc52a15c4a..a016865fcd 100644 --- a/indra/llkdu/tests/llimagej2ckdu_test.cpp +++ b/indra/llkdu/tests/llimagej2ckdu_test.cpp @@ -83,6 +83,7 @@ void LLImageBase::setSize(S32 , S32 , S32 ) { } bool LLImageBase::isBufferInvalid() const { return false; } LLImageJ2CImpl::~LLImageJ2CImpl() { } +S32 LLImageJ2CImpl::estimateDataSize(S32, S32, S32, S32, F32) const { return 0; } LLImageFormatted::LLImageFormatted(S8 ) { } LLImageFormatted::~LLImageFormatted() { } -- cgit v1.3 From 57048769db20f5e84834c8c6dd15466083bd7585 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 20 May 2026 10:20:54 -0400 Subject: Move the KDU data size estimate into j2ckdu, make the base pure virtual. --- indra/llimage/llimagej2c.cpp | 26 -------------------------- indra/llimage/llimagej2c.h | 7 +++---- indra/llkdu/llimagej2ckdu.cpp | 30 ++++++++++++++++++++++++++++++ indra/llkdu/llimagej2ckdu.h | 2 ++ indra/llkdu/tests/llimagej2ckdu_test.cpp | 1 - 5 files changed, 35 insertions(+), 31 deletions(-) (limited to 'indra') diff --git a/indra/llimage/llimagej2c.cpp b/indra/llimage/llimagej2c.cpp index 512b370bae..8099a18045 100644 --- a/indra/llimage/llimagej2c.cpp +++ b/indra/llimage/llimagej2c.cpp @@ -265,32 +265,6 @@ S32 LLImageJ2C::calcHeaderSizeJ2C() return FIRST_PACKET_SIZE; // Hack. just needs to be >= actual header size... } -// Lean pyramid-walk byte estimator suited to packet-by-packet decoders (KDU). -// Starts at one max-block, walks resolutions by doubling area, sums each -// layer's compressed-bytes contribution. -// Reference: https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study -S32 LLImageJ2CImpl::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const -{ - constexpr S32 precision = 8; - constexpr S32 max_components = 4; - S32 width = (w > 0) ? w : 2048; - S32 height = (h > 0) ? h : 2048; - const S32 surface = width * height; - S32 nb_layers = 1; - S32 s = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; - S32 totalbytes = (S32)(s * max_components * precision * rate); - while (surface > s) - { - if (nb_layers <= (5 - discard_level)) - totalbytes += (S32)(s * max_components * precision * rate); - nb_layers++; - s *= 4; - } - totalbytes /= 8; - totalbytes += LLImageJ2C::calcHeaderSizeJ2C(); - return totalbytes; -} - //static S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) { diff --git a/indra/llimage/llimagej2c.h b/indra/llimage/llimagej2c.h index 5173eed33b..81b24cc0b3 100644 --- a/indra/llimage/llimagej2c.h +++ b/indra/llimage/llimagej2c.h @@ -108,10 +108,9 @@ public: virtual ~LLImageJ2CImpl(); // Estimate the byte size of a J2C codestream sufficient to decode the - // given discard level. KDU streams packet-by-packet and uses the lean - // pyramid-walk default. OpenJPEG needs over-allocation to land on - // code-block boundaries even with strict mode off, so it overrides. - virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const; + // given discard level. KDU uses a packet-by-packet impl; OpenJPEG + // overrides with a more conservative block-aligned estimate. + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const = 0; protected: // Find out the image size and number of channels. // Return value: diff --git a/indra/llkdu/llimagej2ckdu.cpp b/indra/llkdu/llimagej2ckdu.cpp index e7ac6bdb31..4330b1e5b1 100644 --- a/indra/llkdu/llimagej2ckdu.cpp +++ b/indra/llkdu/llimagej2ckdu.cpp @@ -1513,3 +1513,33 @@ void kdc_flow_control::process_components() } } } + +// Layer-factored byte estimator. Walks the resolution pyramid to count +// layers, weights by layer_factor, then picks between a sqrt-based "new" +// estimate and a raw-dimensions "old" estimate per TextureNewByteRange. +// Reference: https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study +S32 LLImageJ2CKDU::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const +{ + S32 width = (w > 0) ? w : 2048; + S32 height = (h > 0) ? h : 2048; + S32 nb_layers = 1; + S32 surface = width * height; + S32 s = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; + while (surface > s) + { + nb_layers++; + s *= 4; + } + F32 layer_factor = 3.0f * (7 - llclamp(nb_layers, 1, 6)); + + width >>= discard_level; + height >>= discard_level; + width = llmax(width, 1); + height = llmax(height, 1); + + S32 new_bytes = (S32)(sqrtf((F32)(width * height)) * (F32)comp * rate * 1000.f / layer_factor); + S32 old_bytes = (S32)((F32)(width * height * comp) * rate); + S32 bytes = (LLImage::useNewByteRange() && (new_bytes < old_bytes)) ? new_bytes : old_bytes; + bytes = llmax(bytes, LLImageJ2C::calcHeaderSizeJ2C()); + return bytes; +} diff --git a/indra/llkdu/llimagej2ckdu.h b/indra/llkdu/llimagej2ckdu.h index c9aa0c5250..6079585948 100644 --- a/indra/llkdu/llimagej2ckdu.h +++ b/indra/llkdu/llimagej2ckdu.h @@ -67,6 +67,8 @@ protected: virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL); virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0); virtual std::string getEngineInfo() const; +public: + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const; private: bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, ECodeStreamMode mode, S32 first_channel, S32 max_channel_count, int discard_level = -1, int* region = NULL); diff --git a/indra/llkdu/tests/llimagej2ckdu_test.cpp b/indra/llkdu/tests/llimagej2ckdu_test.cpp index a016865fcd..bc52a15c4a 100644 --- a/indra/llkdu/tests/llimagej2ckdu_test.cpp +++ b/indra/llkdu/tests/llimagej2ckdu_test.cpp @@ -83,7 +83,6 @@ void LLImageBase::setSize(S32 , S32 , S32 ) { } bool LLImageBase::isBufferInvalid() const { return false; } LLImageJ2CImpl::~LLImageJ2CImpl() { } -S32 LLImageJ2CImpl::estimateDataSize(S32, S32, S32, S32, F32) const { return 0; } LLImageFormatted::LLImageFormatted(S8 ) { } LLImageFormatted::~LLImageFormatted() { } -- cgit v1.3 From 80b557310bb980e17123a84a9f93eb87d1040e81 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 20 May 2026 10:33:58 -0400 Subject: Update llimagej2ckdu_test.cpp --- indra/llkdu/tests/llimagej2ckdu_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'indra') diff --git a/indra/llkdu/tests/llimagej2ckdu_test.cpp b/indra/llkdu/tests/llimagej2ckdu_test.cpp index bc52a15c4a..b8c50c9978 100644 --- a/indra/llkdu/tests/llimagej2ckdu_test.cpp +++ b/indra/llkdu/tests/llimagej2ckdu_test.cpp @@ -84,6 +84,10 @@ bool LLImageBase::isBufferInvalid() const { return false; } LLImageJ2CImpl::~LLImageJ2CImpl() { } +// LLImageJ2CKDU::estimateDataSize calls LLImage::useNewByteRange() which is +// inline and reads sUseNewByteRange; provide storage for the test link. +bool LLImage::sUseNewByteRange = false; + LLImageFormatted::LLImageFormatted(S8 ) { } LLImageFormatted::~LLImageFormatted() { } U8* LLImageFormatted::allocateData(S32 ) { return NULL; } -- cgit v1.3 From 804da736ca188433c242e8002b9f0c7c5bf2b246 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 20 May 2026 10:53:12 -0400 Subject: Update llimagej2ckdu_test.cpp --- indra/llkdu/tests/llimagej2ckdu_test.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'indra') diff --git a/indra/llkdu/tests/llimagej2ckdu_test.cpp b/indra/llkdu/tests/llimagej2ckdu_test.cpp index b8c50c9978..6be2d3a078 100644 --- a/indra/llkdu/tests/llimagej2ckdu_test.cpp +++ b/indra/llkdu/tests/llimagej2ckdu_test.cpp @@ -83,10 +83,8 @@ void LLImageBase::setSize(S32 , S32 , S32 ) { } bool LLImageBase::isBufferInvalid() const { return false; } LLImageJ2CImpl::~LLImageJ2CImpl() { } - -// LLImageJ2CKDU::estimateDataSize calls LLImage::useNewByteRange() which is -// inline and reads sUseNewByteRange; provide storage for the test link. bool LLImage::sUseNewByteRange = false; +S32 LLImageJ2C::calcHeaderSizeJ2C() { return 0; } LLImageFormatted::LLImageFormatted(S8 ) { } LLImageFormatted::~LLImageFormatted() { } -- cgit v1.3 From 6bef0d0b7fc26e44f6107503a2097172d9218d8a Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 20 May 2026 11:56:38 -0400 Subject: Make sure we're using override properly. --- indra/llimagej2coj/llimagej2coj.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'indra') diff --git a/indra/llimagej2coj/llimagej2coj.h b/indra/llimagej2coj/llimagej2coj.h index 6c6b379e5f..0cc8d5c34c 100644 --- a/indra/llimagej2coj/llimagej2coj.h +++ b/indra/llimagej2coj/llimagej2coj.h @@ -35,15 +35,15 @@ class LLImageJ2COJ : public LLImageJ2CImpl { public: LLImageJ2COJ(); - virtual ~LLImageJ2COJ(); + virtual ~LLImageJ2COJ() override; protected: - virtual bool getMetadata(LLImageJ2C &base); - virtual bool decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, S32 first_channel, S32 max_channel_count); + virtual bool getMetadata(LLImageJ2C &base) override; + virtual bool decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, S32 first_channel, S32 max_channel_count) override; virtual bool encodeImpl(LLImageJ2C &base, const LLImageRaw &raw_image, const char* comment_text, F32 encode_time=0.0, - bool reversible = false); - virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL); - virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0); - virtual std::string getEngineInfo() const; + bool reversible = false) override; + virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL) override; + virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0) override; + virtual std::string getEngineInfo() const override; public: // OpenJPEG decodes whole code-blocks even with strict mode off, so the // lean packet-walk under-allocates and clips quality. Keep the older -- cgit v1.3 From 5e03a1b32ce69760cf293401ece7f631322f9fe9 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Thu, 21 May 2026 17:47:14 +0300 Subject: #5732 fix showing incorrect 'Packets Lost' statistic --- indra/newview/llvoicewebrtc.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 3b98ca49ef..0125ea4e47 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -3429,9 +3429,10 @@ void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& { if (attributes.contains("packetsLost")) { - U32 out_packets_lost = 0; - LLStringUtil::convertToU32(attributes.at("packetsLost"), out_packets_lost); - sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, out_packets_lost); + // packetsLost may be negative, clamp to zero for unsigned Viewer stats + S32 out_packets_lost = 0; + LLStringUtil::convertToS32(attributes.at("packetsLost"), out_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, static_cast(llmax(out_packets_lost, 0))); } if (attributes.contains("jitter")) { @@ -3445,9 +3446,10 @@ void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& { if (attributes.contains("packetsLost")) { - U32 in_packets_lost = 0; - LLStringUtil::convertToU32(attributes.at("packetsLost"), in_packets_lost); - sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, in_packets_lost); + // packetsLost may be negative, clamp to zero for unsigned Viewer stats + S32 in_packets_lost = 0; + LLStringUtil::convertToS32(attributes.at("packetsLost"), in_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, static_cast(llmax(in_packets_lost, 0))); } if (attributes.contains("packetsReceived")) { -- cgit v1.3 From 99ab6316b4ae9058f22d9f57d21e795ca45797fd Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Thu, 21 May 2026 18:59:24 +0300 Subject: #5733 reduce the tabs width on the Profile --- indra/newview/skins/default/xui/en/floater_profile.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/newview/skins/default/xui/en/floater_profile.xml b/indra/newview/skins/default/xui/en/floater_profile.xml index 32ab811a6e..3a33572785 100644 --- a/indra/newview/skins/default/xui/en/floater_profile.xml +++ b/indra/newview/skins/default/xui/en/floater_profile.xml @@ -31,7 +31,7 @@ follows="all" layout="topleft" halign="center" - tab_min_width="81" + tab_min_width="80" tab_height="30" tab_position="top" > -- cgit v1.3 From 755c19a05053481b19e6f6febd51aedcb238db79 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 22 May 2026 12:33:30 -0400 Subject: Move texture quality to auto detect on VRAM --- indra/newview/featuretable.txt | 27 +++++++++++++++++++-------- indra/newview/featuretable_mac.txt | 27 +++++++++++++++++++-------- indra/newview/llfeaturemanager.cpp | 24 ++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 18 deletions(-) (limited to 'indra') diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt index c408edf963..f05f77c222 100644 --- a/indra/newview/featuretable.txt +++ b/indra/newview/featuretable.txt @@ -1,4 +1,4 @@ -version 75 +version 76 // The version number above should be incremented IF AND ONLY IF some // change has been made that is sufficiently important to justify // resetting the graphics preferences of all users to the recommended @@ -128,7 +128,6 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderTextureQuality 1 0 RenderReflectionProbeCount 1 1 // @@ -171,7 +170,6 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderTextureQuality 1 0 RenderReflectionProbeCount 1 32 // @@ -213,7 +211,6 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -255,7 +252,6 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -297,7 +293,6 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 2 RenderReflectionProbeCount 1 128 // @@ -339,7 +334,6 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 2 RenderReflectionProbeCount 1 256 // @@ -381,7 +375,6 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -399,6 +392,24 @@ RenderDisableVintageMode 1 0 list VRAMGT512 RenderCompressTextures 1 0 +// +// VRAM 4GB to 8GB +// +list VRAMLT8GB +RenderTextureQuality 1 2 + +// +// VRAM 2GB to 4GB +// +list VRAMLT4GB +RenderTextureQuality 1 1 + +// +// VRAM 2GB and under +// +list VRAMLT2GB +RenderTextureQuality 1 0 + // // "Default" setups for safe, low, medium, high // diff --git a/indra/newview/featuretable_mac.txt b/indra/newview/featuretable_mac.txt index 7bb0e3f7f1..b11fa28c48 100644 --- a/indra/newview/featuretable_mac.txt +++ b/indra/newview/featuretable_mac.txt @@ -1,4 +1,4 @@ -version 74 +version 75 // The version number above should be incremented IF AND ONLY IF some // change has been made that is sufficiently important to justify // resetting the graphics preferences of all users to the recommended @@ -128,7 +128,6 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderTextureQuality 1 0 RenderReflectionProbeCount 1 1 // @@ -171,7 +170,6 @@ RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 RenderDisableVintageMode 1 0 -RenderTextureQuality 1 0 RenderReflectionProbeCount 1 32 // @@ -213,7 +211,6 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -255,7 +252,6 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 1 RenderReflectionProbeCount 1 64 // @@ -297,7 +293,6 @@ RenderCASSharpness 1 0 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 2 RenderReflectionProbeCount 1 128 // @@ -339,7 +334,6 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 2 RenderReflectionProbeCount 1 256 // @@ -381,7 +375,6 @@ RenderCASSharpness 1 0.4 RenderExposure 1 1 RenderTonemapType 1 1 RenderTonemapMix 1 0.7 -RenderTextureQuality 1 3 RenderReflectionProbeCount 1 256 // @@ -400,6 +393,24 @@ RenderDisableVintageMode 1 0 list VRAMGT512 RenderCompressTextures 1 0 +// +// VRAM 4GB to 8GB +// +list VRAMLT8GB +RenderTextureQuality 1 2 + +// +// VRAM 2GB to 4GB +// +list VRAMLT4GB +RenderTextureQuality 1 1 + +// +// VRAM 2GB and under +// +list VRAMLT2GB +RenderTextureQuality 1 0 + // // "Default" setups for safe, low, medium, high // diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp index c8692224f1..aab2865ef7 100644 --- a/indra/newview/llfeaturemanager.cpp +++ b/indra/newview/llfeaturemanager.cpp @@ -752,9 +752,29 @@ void LLFeatureManager::applyBaseMasks() { maskFeatures("VRAMGT512"); } - if (gGLManager.mVRAM < 2048) + + // Texture quality is driven by detected VRAM. Feature masks take the MIN + // of applied values, so cascading lower tiers downgrade RenderTextureQuality: + // <= 2GB -> Low(0), <= 4GB -> Medium(1), < 8GB -> High(2), >= 8GB -> Ultra(3). + // When VRAM cannot be detected (mVRAM == 0, common on Linux) fall back to Medium. + if (gGLManager.mVRAM == 0) { - maskFeatures("VRAMLT2GB"); + maskFeatures("VRAMLT4GB"); + } + else + { + if (gGLManager.mVRAM < 8192) + { + maskFeatures("VRAMLT8GB"); + } + if (gGLManager.mVRAM <= 4096) + { + maskFeatures("VRAMLT4GB"); + } + if (gGLManager.mVRAM <= 2048) + { + maskFeatures("VRAMLT2GB"); + } } if (gGLManager.mGLVersion < 3.99f) { -- cgit v1.3 From dad44ba5d67d04a73708a9a25bbe1ddec29a6a9a Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 22 May 2026 13:12:58 -0400 Subject: A few OpenGL state fixes provided by Rye from the Alchemy Viewer. --- indra/llrender/llimagegl.cpp | 2 +- indra/llrender/llrender.cpp | 15 ++++++++++++++- indra/newview/lldrawpoolwater.cpp | 8 ++++---- 3 files changed, 19 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index a1396aba20..6bcc34938c 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1895,7 +1895,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LLGLint is_compressed = 0; if (compressed_ok) { - glGetTexLevelParameteriv(mTarget, is_compressed, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); + glGetTexLevelParameteriv(mTarget, gl_discard, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); } //----------------------------------------------------------------------------------------------- diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 696a0d145f..8ac906b5a1 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -197,6 +197,7 @@ void LLTexUnit::bindFast(LLTexture* texture) glActiveTexture(GL_TEXTURE0 + mIndex); gGL.mCurrTextureUnitIndex = mIndex; mCurrTexture = gl_tex->getTexName(); + mCurrTexType = gl_tex->getTarget(); // bindFast bypasses updateBindStats(); stamp directly so the staleness // signal sees per-frame use of batched textures. gl_tex->stampBound(); @@ -1732,7 +1733,16 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) LLVertexBuffer * vb = new LLVertexBuffer(attribute_mask); vb->allocateBuffer(count, 0); - vb->setBuffer(); + // Non-Apple path uses glBufferSubData inside setXxxData, so the VBO + // must already be bound. On Apple, the VBO is lazily created in + // _unmapBuffer (LLAppleVBOPool); calling setBuffer() here would bind + // mGLBuffer == 0 and then setupVertexBuffer would issue + // glVertexAttribIPointer with a non-null offset against no bound + // GL_ARRAY_BUFFER -> GL_INVALID_OPERATION in core profile. + if (!gGLManager.mIsApple) + { + vb->setBuffer(); + } vb->setPositionData(mVerticesp.get()); @@ -1747,6 +1757,9 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) } #if LL_DARWIN + // unmapBuffer creates the GL buffer, uploads, and leaves it bound, + // drawBuffer's later setBuffer() then runs setupVertexBuffer against + // a valid VBO. vb->unmapBuffer(); #endif vb->unbind(); diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp index cdf3244389..fbecaa1583 100644 --- a/indra/newview/lldrawpoolwater.cpp +++ b/indra/newview/lldrawpoolwater.cpp @@ -204,22 +204,22 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) if (tex_a && (!tex_b || (tex_a == tex_b))) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); tex_a->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); blend_factor = 0; // only one tex provided, no blending } else if (tex_b && !tex_a) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b); tex_b->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b); blend_factor = 0; // only one tex provided, no blending } else if (tex_b != tex_a) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); tex_a->setFilteringOption(filter_mode); - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b); tex_b->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b); } shader->bindTexture(LLShaderMgr::WATER_EXCLUSIONTEX, &gPipeline.mWaterExclusionMask); -- cgit v1.3 From 41858caec0ea7badcff670c62b459bf06a74aa03 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 5 May 2026 23:13:12 +0300 Subject: #4802 Handle KDU crashes --- indra/llkdu/llimagej2ckdu.cpp | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/llkdu/llimagej2ckdu.cpp b/indra/llkdu/llimagej2ckdu.cpp index 4330b1e5b1..74aaffbd65 100644 --- a/indra/llkdu/llimagej2ckdu.cpp +++ b/indra/llkdu/llimagej2ckdu.cpp @@ -158,6 +158,9 @@ public: kdu_codestream* codestreamp); ~LLKDUDecodeState(); bool processTileDecode(F32 decode_time, bool limit_time = true); +#ifdef LL_WINDOWS + bool processTileDecodeSEH(F32 decode_time, bool limit_time = true); +#endif private: S32 mNumComponents; @@ -282,10 +285,16 @@ void LLImageJ2CKDU::setupCodeStream(LLImageJ2C &base, bool keep_codestream, ECod try { - S32 data_size = base.getDataSize(); S32 max_bytes = (base.getMaxBytes() ? base.getMaxBytes() : data_size); + // Data sanity checks + // 0 data size appears to be valid. + if (data_size < 0 || data_size > 128 * 1024 * 1024) // 128MB + { + LLTHROW(KDUError(STRINGIZE("Invalid data size: " << data_size))); + } + // // Initialization // @@ -385,7 +394,8 @@ void LLImageJ2CKDU::setupCodeStream(LLImageJ2C &base, bool keep_codestream, ECod catch (std::bad_alloc&) { // we are in a thread, can't show an 'out of memory' here, - // main thread will keep going + // as main thread will keep going + // Todo: should cause main thread to stop and show an error. throw; } catch (...) @@ -602,7 +612,11 @@ bool LLImageJ2CKDU::decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 deco // Do the actual processing F32 remaining_time = limit_time ? decode_time - decode_timer.getElapsedTimeF32().value() : 0.0f; // This is where we do the actual decode. If we run out of time, return false. +#ifdef LL_WINDOWS + if (mDecodeState->processTileDecodeSEH(remaining_time, limit_time)) +#else if (mDecodeState->processTileDecode(remaining_time, limit_time)) +#endif { mDecodeState.reset(); } @@ -1302,6 +1316,11 @@ all necessary level shifting, type conversion, rounding and truncation. */ LLKDUDecodeState::LLKDUDecodeState(kdu_tile tile, kdu_byte *buf, S32 row_gap, kdu_codestream* codestreamp) { + if (!buf) + { + LLTHROW(KDUError("Null buffer passed to LLKDUDecodeState")); + } + S32 c; mTile = tile; @@ -1311,6 +1330,11 @@ LLKDUDecodeState::LLKDUDecodeState(kdu_tile tile, kdu_byte *buf, S32 row_gap, mNumComponents = tile.get_num_components(); llassert(mNumComponents <= 4); + if (mNumComponents <= 0 || mNumComponents > 4) + { + LL_WARNS() << "Invalid component count: " << mNumComponents << ", clamping to valid range" << LL_ENDL; + mNumComponents = llclamp(mNumComponents, 1, 4); + } mUseYCC = tile.get_ycc(); for (c = 0; c < 4; ++c) @@ -1363,6 +1387,20 @@ LLKDUDecodeState::~LLKDUDecodeState() mTile.close(); } +#ifdef LL_WINDOWS +bool LLKDUDecodeState::processTileDecodeSEH(F32 decode_time, bool limit_time) +{ + __try + { + return processTileDecode(decode_time, limit_time); + } + __except (msc_exception_filter(GetExceptionCode(), GetExceptionInformation())) + { + return false; + } +} +#endif + bool LLKDUDecodeState::processTileDecode(F32 decode_time, bool limit_time) /* Decompresses a tile, writing the data into the supplied byte buffer. The buffer contains interleaved image components, if there are any. -- cgit v1.3 From 0c5fd5e8110f55c713c5cbf721769fa485746f77 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Mon, 11 May 2026 23:35:24 +0300 Subject: #5784 fix crash at LLViewerRegion::killInvisibleObjects --- indra/newview/llviewerregion.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indra') diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index f31befd1ab..dd02cca50a 100755 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -1768,6 +1768,11 @@ void LLViewerRegion::killInvisibleObjects(F32 max_time) if(iter == mImpl->mActiveSet.end()) { iter = mImpl->mActiveSet.begin(); + if (iter == mImpl->mActiveSet.end()) + { + // Set became empty + break; + } } if((*iter)->getParentID() > 0) { -- cgit v1.3 From 663bf4d3eba16e1d0a781ac5261541e7e2d6b4f2 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 19 May 2026 22:17:00 +0300 Subject: #4823 WebRTC crashes --- indra/llwebrtc/llwebrtc.cpp | 87 ++++++++++++++++++++++------------------- indra/newview/llvoicewebrtc.cpp | 32 +++++++++++++-- 2 files changed, 74 insertions(+), 45 deletions(-) (limited to 'indra') diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index a286f75f42..f4ecce63a6 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -841,22 +841,23 @@ void LLWebRTCPeerConnectionImpl::init(LLWebRTCImpl * webrtc_impl) void LLWebRTCPeerConnectionImpl::terminate() { mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this]() + [self]() { - if (mPeerConnection) + if (self->mPeerConnection) { - if (mDataChannel) + if (self->mDataChannel) { { - mDataChannel->Close(); - mDataChannel = nullptr; + self->mDataChannel->Close(); + self->mDataChannel = nullptr; } } // to remove 'Secondlife is recording' icon from taskbar // if user was speaking - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto& sender : senders) { auto track = sender->track(); @@ -866,24 +867,24 @@ void LLWebRTCPeerConnectionImpl::terminate() } } - mPeerConnection->Close(); - if (mLocalStream) + self->mPeerConnection->Close(); + if (self->mLocalStream) { - auto tracks = mLocalStream->GetAudioTracks(); + auto tracks = self->mLocalStream->GetAudioTracks(); for (auto& track : tracks) { - mLocalStream->RemoveTrack(track); + self->mLocalStream->RemoveTrack(track); } - mLocalStream = nullptr; + self->mLocalStream = nullptr; } - mPeerConnection = nullptr; + self->mPeerConnection = nullptr; - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnPeerConnectionClosed(); } } - mPendingJobs--; + self->mPendingJobs--; }); } @@ -906,8 +907,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti mAnswerReceived = false; mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this,options]() + [self,options]() { webrtc::PeerConnectionInterface::RTCConfiguration config; for (auto server : options.mServers) @@ -926,42 +928,42 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti config.set_min_port(60000); config.set_max_port(60100); - webrtc::PeerConnectionDependencies pc_dependencies(this); + webrtc::PeerConnectionDependencies pc_dependencies(self.get()); // Other thread manages mPeerConnectionFactory's lifetime and it can be reset // at any momment, create own scoped_refptr (atomic). - webrtc::scoped_refptr peer_connection_factory = mPeerConnectionFactory; + webrtc::scoped_refptr peer_connection_factory = self->mPeerConnectionFactory; if (peer_connection_factory == nullptr) { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection, factory doesn't exist"; // Too early? - mPendingJobs--; + self->mPendingJobs--; return; } auto error_or_peer_connection = peer_connection_factory->CreatePeerConnectionOrError(config, std::move(pc_dependencies)); if (error_or_peer_connection.ok()) { - mPeerConnection = std::move(error_or_peer_connection.value()); + self->mPeerConnection = std::move(error_or_peer_connection.value()); } else { RTC_LOG(LS_ERROR) << __FUNCTION__ << "Error creating peer connection: " << error_or_peer_connection.error().message(); - for (auto &observer : mSignalingObserverList) + for (auto &observer : self->mSignalingObserverList) { observer->OnRenegotiationNeeded(); } - mPendingJobs--; + self->mPendingJobs--; return; } webrtc::DataChannelInit init; init.ordered = true; - auto data_channel_or_error = mPeerConnection->CreateDataChannelOrError("SLData", &init); + auto data_channel_or_error = self->mPeerConnection->CreateDataChannelOrError("SLData", &init); if (data_channel_or_error.ok()) { - mDataChannel = std::move(data_channel_or_error.value()); + self->mDataChannel = std::move(data_channel_or_error.value()); - mDataChannel->RegisterObserver(this); + self->mDataChannel->RegisterObserver(self.get()); } webrtc::AudioOptions audioOptions; @@ -970,16 +972,16 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti audioOptions.noise_suppression = true; audioOptions.init_recording_on_send = false; - mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); + self->mLocalStream = peer_connection_factory->CreateLocalMediaStream("SLStream"); webrtc::scoped_refptr audio_track( peer_connection_factory->CreateAudioTrack("SLAudio", peer_connection_factory->CreateAudioSource(audioOptions).get())); audio_track->set_enabled(false); - mLocalStream->AddTrack(audio_track); + self->mLocalStream->AddTrack(audio_track); - mPeerConnection->AddTrack(audio_track, {"SLStream"}); + self->mPeerConnection->AddTrack(audio_track, {"SLStream"}); - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); for (auto &sender : senders) { @@ -995,7 +997,7 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti sender->SetParameters(params); } - auto receivers = mPeerConnection->GetReceivers(); + auto receivers = self->mPeerConnection->GetReceivers(); for (auto &receiver : receivers) { webrtc::RtpParameters params; @@ -1011,9 +1013,9 @@ bool LLWebRTCPeerConnectionImpl::initializeConnection(const LLWebRTCPeerConnecti } webrtc::PeerConnectionInterface::RTCOfferAnswerOptions offerOptions; - this->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. - mPeerConnection->CreateOffer(this, offerOptions); - mPendingJobs--; + self->AddRef(); // CreateOffer will deref this when it's done. Without this, the callbacks never get called. + self->mPeerConnection->CreateOffer(self.get(), offerOptions); + self->mPendingJobs--; }); return true; @@ -1090,14 +1092,15 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) mPendingJobs++; + webrtc::scoped_refptr self(this); mWebRTCImpl->PostSignalingTask( - [this, force_reset, enable]() + [self, force_reset, enable]() { - if (mPeerConnection) + if (self->mPeerConnection) { - auto senders = mPeerConnection->GetSenders(); + auto senders = self->mPeerConnection->GetSenders(); - RTC_LOG(LS_INFO) << __FUNCTION__ << (mMute ? "disabling" : "enabling") << " streams count " << senders.size(); + RTC_LOG(LS_INFO) << __FUNCTION__ << (self->mMute ? "disabling" : "enabling") << " streams count " << senders.size(); for (auto &sender : senders) { auto track = sender->track(); @@ -1113,7 +1116,7 @@ void LLWebRTCPeerConnectionImpl::setMute(bool mute) track->set_enabled(enable); } } - mPendingJobs--; + self->mPendingJobs--; } }); } @@ -1253,12 +1256,14 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { mPendingJobs++; - mWebRTCImpl->PostWorkerTask([this]() { - for (auto &observer : mSignalingObserverList) + webrtc::scoped_refptr self(this); + mWebRTCImpl->PostWorkerTask([self]() + { + for (auto &observer : self->mSignalingObserverList) { - observer->OnAudioEstablished(this); + observer->OnAudioEstablished(self.get()); } - mPendingJobs--; + self->mPendingJobs--; }); break; } diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 0125ea4e47..ecf963039f 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -1678,6 +1678,10 @@ void LLWebRTCVoiceClient::setVoiceVolume(F32 volume) void LLWebRTCVoiceClient::predSetSpeakerVolume(const LLWebRTCVoiceClient::sessionStatePtr_t &session, F32 volume) { + if (session->mShuttingDown) + { + return; + } session->setSpeakerVolume(volume); } @@ -1906,6 +1910,10 @@ LLWebRTCVoiceClient::sessionState::sessionState() : void LLWebRTCVoiceClient::predUpdateOwnVolume(const LLWebRTCVoiceClient::sessionStatePtr_t &session, F32 audio_level) { + if (session->mShuttingDown) + { + return; + } participantStatePtr_t participant = session->findParticipantByID(gAgentID); if (participant) { @@ -1934,9 +1942,16 @@ void LLWebRTCVoiceClient::sessionState::sendData(const std::string &data) void LLWebRTCVoiceClient::sessionState::setMuteMic(bool muted) { mMuted = muted; + if (mShuttingDown) + { + return; + } for (auto &connection : mWebRTCConnections) { - connection->setMuteMic(muted); + if (!connection->isShuttingDown()) + { + connection->setMuteMic(muted); + } } } @@ -1945,7 +1960,10 @@ void LLWebRTCVoiceClient::sessionState::setSpeakerVolume(F32 volume) mSpeakerVolume = volume; for (auto &connection : mWebRTCConnections) { - connection->setSpeakerVolume(volume); + if (!connection->isShuttingDown()) + { + connection->setSpeakerVolume(volume); + } } } @@ -1957,7 +1975,10 @@ void LLWebRTCVoiceClient::sessionState::setUserVolume(const LLUUID &id, F32 volu } for (auto &connection : mWebRTCConnections) { - connection->setUserVolume(id, volume); + if (!connection->isShuttingDown()) + { + connection->setUserVolume(id, volume); + } } } @@ -1969,7 +1990,10 @@ void LLWebRTCVoiceClient::sessionState::setUserMute(const LLUUID &id, bool mute) } for (auto &connection : mWebRTCConnections) { - connection->setUserMute(id, mute); + if (!connection->isShuttingDown()) + { + connection->setUserMute(id, mute); + } } } /*static*/ -- cgit v1.3