From afae8046e01264af9fa310aae77ac476eb35212a Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 8 Jun 2026 20:10:44 -0400 Subject: [PATCH 1/3] Fix LLUICtrl control-variable signal leak; trim base-widget memory LLUICtrl never disconnected its control-variable signal connections on destruction, leaking a dead slot onto the session-lived LLControlVariable signal for every control bound via control_name/enabled/visibility that was later destroyed. Fold the five control-variable pointers and their connections into a lazily-allocated ControlVariables struct whose connections are scoped_connection, so destruction auto-disconnects; the struct is allocated only when a control actually binds a variable (~120 bytes saved per unbound control). Route llmenugl/llcombobox through getControlVariable()/getEnabledControlVariable() accessors. Also make LLView::mToolTipMsg a lazily-allocated LLUIString*, null until a tooltip is set, so tooltip-less widgets no longer carry ~100 bytes of empty string storage. Co-Authored-By: Claude Opus 4.8 (1M context) --- indra/llui/llcombobox.cpp | 20 ++++----- indra/llui/llmenugl.cpp | 6 +-- indra/llui/lluictrl.cpp | 92 ++++++++++++++++++++++----------------- indra/llui/lluictrl.h | 35 ++++++++++----- indra/llui/llview.cpp | 33 +++++++++++--- indra/llui/llview.h | 5 ++- 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp index 25331dadefd..e99d9d65224 100644 --- a/indra/llui/llcombobox.cpp +++ b/indra/llui/llcombobox.cpp @@ -173,9 +173,9 @@ void LLComboBox::initFromParams(const LLComboBox::Params& p) // virtual bool LLComboBox::postBuild() { - if (mControlVariable) + if (getControlVariable()) { - setValue(mControlVariable->getValue()); // selects the appropriate item + setValue(getControlVariable()->getValue()); // selects the appropriate item } return true; } @@ -278,9 +278,9 @@ LLScrollListItem* LLComboBox::add(const std::string& name, EAddPosition pos, boo item->setEnabled(enabled); if (!mAllowTextEntry && mLabel.empty()) { - if (mControlVariable) + if (getControlVariable()) { - setValue(mControlVariable->getValue()); // selects the appropriate item + setValue(getControlVariable()->getValue()); // selects the appropriate item } else { @@ -297,9 +297,9 @@ LLScrollListItem* LLComboBox::add(const std::string& name, const LLUUID& id, EAd item->setEnabled(enabled); if (!mAllowTextEntry && mLabel.empty()) { - if (mControlVariable) + if (getControlVariable()) { - setValue(mControlVariable->getValue()); // selects the appropriate item + setValue(getControlVariable()->getValue()); // selects the appropriate item } else { @@ -317,9 +317,9 @@ LLScrollListItem* LLComboBox::add(const std::string& name, void* userdata, EAddP item->setUserdata( userdata ); if (!mAllowTextEntry && mLabel.empty()) { - if (mControlVariable) + if (getControlVariable()) { - setValue(mControlVariable->getValue()); // selects the appropriate item + setValue(getControlVariable()->getValue()); // selects the appropriate item } else { @@ -336,9 +336,9 @@ LLScrollListItem* LLComboBox::add(const std::string& name, LLSD value, EAddPosit item->setEnabled(enabled); if (!mAllowTextEntry && mLabel.empty()) { - if (mControlVariable) + if (getControlVariable()) { - setValue(mControlVariable->getValue()); // selects the appropriate item + setValue(getControlVariable()->getValue()); // selects the appropriate item } else { diff --git a/indra/llui/llmenugl.cpp b/indra/llui/llmenugl.cpp index 5764ee735e3..48d85599559 100644 --- a/indra/llui/llmenugl.cpp +++ b/indra/llui/llmenugl.cpp @@ -838,12 +838,12 @@ void LLMenuItemCallGL::updateEnabled( void ) if (mEnableSignal.num_slots() > 0) { bool enabled = mEnableSignal(this, LLSD()); - if (mEnabledControlVariable) + if (getEnabledControlVariable()) { if (!enabled) { // callback overrides control variable; this will call setEnabled() - mEnabledControlVariable->set(false); + getEnabledControlVariable()->set(false); } } else @@ -947,7 +947,7 @@ void LLMenuItemCheckGL::buildDrawLabel( void ) { // Note: mCheckSignal() returns true if no callbacks are set bool checked = mCheckSignal(this, LLSD()); - if (mControlVariable) + if (getControlVariable()) { if (!checked) setControlValue(false); // callback overrides control variable; this will call setValue() diff --git a/indra/llui/lluictrl.cpp b/indra/llui/lluictrl.cpp index 80c6b9dab5b..1a27ff5cf14 100644 --- a/indra/llui/lluictrl.cpp +++ b/indra/llui/lluictrl.cpp @@ -104,11 +104,6 @@ LLUICtrl::LLUICtrl(const LLUICtrl::Params& p, const LLViewModelPtr& viewmodel) mTabStop(false), mTentative(false), mViewModel(viewmodel), - mControlVariable(NULL), - mEnabledControlVariable(NULL), - mDisabledControlVariable(NULL), - mMakeVisibleControlVariable(NULL), - mMakeInvisibleControlVariable(NULL), mCommitSignal(NULL), mValidateSignal(NULL), mMouseEnterSignal(NULL), @@ -258,6 +253,9 @@ LLUICtrl::~LLUICtrl() delete mRightMouseDownSignal; delete mRightMouseUpSignal; delete mDoubleClickSignal; + + // scoped_connection members disconnect from any bound control variables here + delete mControlVariables; } void default_commit_handler(LLUICtrl* ctrl, const LLSD& param) @@ -500,11 +498,20 @@ bool LLUICtrl::postBuild() return LLView::postBuild(); } +LLUICtrl::ControlVariables& LLUICtrl::getControlVars() +{ + if (!mControlVariables) + { + mControlVariables = new ControlVariables(); + } + return *mControlVariables; +} + bool LLUICtrl::setControlValue(const LLSD& value) { - if (mControlVariable) + if (LLControlVariable* control = getControlVariable()) { - mControlVariable->set(value); + control->set(value); return true; } return false; @@ -512,28 +519,29 @@ bool LLUICtrl::setControlValue(const LLSD& value) void LLUICtrl::setControlVariable(LLControlVariable* control) { - if (mControlVariable) + if (mControlVariables && mControlVariables->mControlVariable) { //RN: this will happen in practice, should we try to avoid it? //LL_WARNS() << "setControlName called twice on same control!" << LL_ENDL; - mControlConnection.disconnect(); // disconnect current signal - mControlVariable = NULL; + mControlVariables->mControlConnection.disconnect(); // disconnect current signal + mControlVariables->mControlVariable = NULL; } if (control) { - mControlVariable = control; - mControlConnection = mControlVariable->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("value"))); - setValue(mControlVariable->getValue()); + ControlVariables& vars = getControlVars(); + vars.mControlVariable = control; + vars.mControlConnection = control->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("value"))); + setValue(control->getValue()); } } void LLUICtrl::removeControlVariable() { - if (mControlVariable) + if (mControlVariables && mControlVariables->mControlVariable) { - mControlConnection.disconnect(); - mControlVariable = NULL; + mControlVariables->mControlConnection.disconnect(); + mControlVariables->mControlVariable = NULL; } } @@ -560,61 +568,65 @@ void LLUICtrl::setControlName(const std::string& control_name, LLView *context) void LLUICtrl::setEnabledControlVariable(LLControlVariable* control) { - if (mEnabledControlVariable) + if (mControlVariables && mControlVariables->mEnabledControlVariable) { - mEnabledControlConnection.disconnect(); // disconnect current signal - mEnabledControlVariable = NULL; + mControlVariables->mEnabledControlConnection.disconnect(); // disconnect current signal + mControlVariables->mEnabledControlVariable = NULL; } if (control) { - mEnabledControlVariable = control; - mEnabledControlConnection = mEnabledControlVariable->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("enabled"))); - setEnabled(mEnabledControlVariable->getValue().asBoolean()); + ControlVariables& vars = getControlVars(); + vars.mEnabledControlVariable = control; + vars.mEnabledControlConnection = control->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("enabled"))); + setEnabled(control->getValue().asBoolean()); } } void LLUICtrl::setDisabledControlVariable(LLControlVariable* control) { - if (mDisabledControlVariable) + if (mControlVariables && mControlVariables->mDisabledControlVariable) { - mDisabledControlConnection.disconnect(); // disconnect current signal - mDisabledControlVariable = NULL; + mControlVariables->mDisabledControlConnection.disconnect(); // disconnect current signal + mControlVariables->mDisabledControlVariable = NULL; } if (control) { - mDisabledControlVariable = control; - mDisabledControlConnection = mDisabledControlVariable->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("disabled"))); - setEnabled(!(mDisabledControlVariable->getValue().asBoolean())); + ControlVariables& vars = getControlVars(); + vars.mDisabledControlVariable = control; + vars.mDisabledControlConnection = control->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("disabled"))); + setEnabled(!(control->getValue().asBoolean())); } } void LLUICtrl::setMakeVisibleControlVariable(LLControlVariable* control) { - if (mMakeVisibleControlVariable) + if (mControlVariables && mControlVariables->mMakeVisibleControlVariable) { - mMakeVisibleControlConnection.disconnect(); // disconnect current signal - mMakeVisibleControlVariable = NULL; + mControlVariables->mMakeVisibleControlConnection.disconnect(); // disconnect current signal + mControlVariables->mMakeVisibleControlVariable = NULL; } if (control) { - mMakeVisibleControlVariable = control; - mMakeVisibleControlConnection = mMakeVisibleControlVariable->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("visible"))); - setVisible(mMakeVisibleControlVariable->getValue().asBoolean()); + ControlVariables& vars = getControlVars(); + vars.mMakeVisibleControlVariable = control; + vars.mMakeVisibleControlConnection = control->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("visible"))); + setVisible(control->getValue().asBoolean()); } } void LLUICtrl::setMakeInvisibleControlVariable(LLControlVariable* control) { - if (mMakeInvisibleControlVariable) + if (mControlVariables && mControlVariables->mMakeInvisibleControlVariable) { - mMakeInvisibleControlConnection.disconnect(); // disconnect current signal - mMakeInvisibleControlVariable = NULL; + mControlVariables->mMakeInvisibleControlConnection.disconnect(); // disconnect current signal + mControlVariables->mMakeInvisibleControlVariable = NULL; } if (control) { - mMakeInvisibleControlVariable = control; - mMakeInvisibleControlConnection = mMakeInvisibleControlVariable->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("invisible"))); - setVisible(!(mMakeInvisibleControlVariable->getValue().asBoolean())); + ControlVariables& vars = getControlVars(); + vars.mMakeInvisibleControlVariable = control; + vars.mMakeInvisibleControlConnection = control->getSignal()->connect(boost::bind(&controlListener, _2, getHandle(), std::string("invisible"))); + setVisible(!(control->getValue().asBoolean())); } } diff --git a/indra/llui/lluictrl.h b/indra/llui/lluictrl.h index 749999bbfe0..3162f81447c 100644 --- a/indra/llui/lluictrl.h +++ b/indra/llui/lluictrl.h @@ -180,7 +180,8 @@ class LLUICtrl virtual void setControlName(const std::string& control, LLView *context = NULL); void removeControlVariable(); - LLControlVariable* getControlVariable() { return mControlVariable; } + LLControlVariable* getControlVariable() { return mControlVariables ? mControlVariables->mControlVariable : nullptr; } + LLControlVariable* getEnabledControlVariable() { return mControlVariables ? mControlVariables->mEnabledControlVariable : nullptr; } void setEnabledControlVariable(LLControlVariable* control); void setDisabledControlVariable(LLControlVariable* control); @@ -305,17 +306,6 @@ class LLUICtrl LLViewModelPtr mViewModel; - LLControlVariable* mControlVariable; - boost::signals2::connection mControlConnection; - LLControlVariable* mEnabledControlVariable; - boost::signals2::connection mEnabledControlConnection; - LLControlVariable* mDisabledControlVariable; - boost::signals2::connection mDisabledControlConnection; - LLControlVariable* mMakeVisibleControlVariable; - boost::signals2::connection mMakeVisibleControlConnection; - LLControlVariable* mMakeInvisibleControlVariable; - boost::signals2::connection mMakeInvisibleControlConnection; - std::string mFunctionName; static F32 sActiveControlTransparency; @@ -331,6 +321,27 @@ class LLUICtrl bool mTentative; ETypeTransparency mTransparencyType; + + // Binding a control to a setting variable is uncommon, so keep the + // pointers and their scoped (auto-disconnecting) signal connections in a + // block allocated only on first use. This both avoids ~120 bytes of + // mostly-unused members on every control and ensures the connections are + // disconnected when the control is destroyed (scoped_connection RAII). + struct ControlVariables + { + LLControlVariable* mControlVariable{ nullptr }; + LLControlVariable* mEnabledControlVariable{ nullptr }; + LLControlVariable* mDisabledControlVariable{ nullptr }; + LLControlVariable* mMakeVisibleControlVariable{ nullptr }; + LLControlVariable* mMakeInvisibleControlVariable{ nullptr }; + boost::signals2::scoped_connection mControlConnection; + boost::signals2::scoped_connection mEnabledControlConnection; + boost::signals2::scoped_connection mDisabledControlConnection; + boost::signals2::scoped_connection mMakeVisibleControlConnection; + boost::signals2::scoped_connection mMakeInvisibleControlConnection; + }; + ControlVariables* mControlVariables{ nullptr }; + ControlVariables& getControlVars(); // lazily allocates mControlVariables }; // Build time optimization, generate once in .cpp file diff --git a/indra/llui/llview.cpp b/indra/llui/llview.cpp index 7d6c937b857..d896a9ffaf4 100644 --- a/indra/llui/llview.cpp +++ b/indra/llui/llview.cpp @@ -156,12 +156,17 @@ LLView::LLView(const LLView::Params& p) mUseBoundingRect(p.use_bounding_rect), mDefaultTabGroup(p.default_tab_group), mLastTabGroup(0), - mToolTipMsg((LLStringExplicit)p.tool_tip()), mDefaultWidgets(NULL) { // create rect first, as this will supply initial follows flags setShape(p.rect); parseFollowsFlags(p); + + // allocate the tooltip holder only when a tooltip is actually provided + if (!p.tool_tip().empty()) + { + mToolTipMsg = new LLUIString((LLStringExplicit)p.tool_tip()); + } } LLView::~LLView() @@ -194,6 +199,8 @@ LLView::~LLView() delete mDefaultWidgets; mDefaultWidgets = NULL; } + + delete mToolTipMsg; } // virtual @@ -208,20 +215,36 @@ bool LLView::isPanel() const return false; } +LLUIString& LLView::getOrCreateToolTipMsg() +{ + if (!mToolTipMsg) + { + mToolTipMsg = new LLUIString(); + } + return *mToolTipMsg; +} + void LLView::setToolTip(const LLStringExplicit& msg) { - mToolTipMsg = msg; + if (mToolTipMsg) + { + *mToolTipMsg = msg; + } + else if (!msg.empty()) + { + mToolTipMsg = new LLUIString(msg); + } } bool LLView::setToolTipArg(const LLStringExplicit& key, const LLStringExplicit& text) { - mToolTipMsg.setArg(key, text); + getOrCreateToolTipMsg().setArg(key, text); return true; } void LLView::setToolTipArgs( const LLStringUtil::format_map_t& args ) { - mToolTipMsg.setArgList(args); + getOrCreateToolTipMsg().setArgList(args); } // virtual @@ -931,7 +954,7 @@ const std::string LLView::getToolTip() const } } - return mToolTipMsg.getString(); + return mToolTipMsg ? mToolTipMsg->getString() : LLStringUtil::null; } bool LLView::handleToolTip(S32 x, S32 y, MASK mask) diff --git a/indra/llui/llview.h b/indra/llui/llview.h index d747ef95551..4e5d0150eab 100644 --- a/indra/llui/llview.h +++ b/indra/llui/llview.h @@ -605,7 +605,7 @@ class LLView bool mEnabled; // Enabled means "accepts input that has an effect on the state of the application." // A disabled view, for example, may still have a scrollbar that responds to mouse events. bool mMouseOpaque; // Opaque views handle all mouse events that are over their rect. - LLUIString mToolTipMsg; // isNull() is true if none. + LLUIString* mToolTipMsg { nullptr }; // allocated lazily; null when no tooltip is set U8 mSoundFlags; bool mFromXUI; @@ -625,6 +625,9 @@ class LLView LLView& getDefaultWidgetContainer() const; + // tooltip holder is allocated on demand; most widgets never set one + LLUIString& getOrCreateToolTipMsg(); + // This allows special mouse-event targeting logic for testing. typedef std::function DrilldownFunc; static DrilldownFunc sDrilldown; From de8874219482327370becfd32c3756ca5882a4b2 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 8 Jun 2026 21:03:17 -0400 Subject: [PATCH 2/3] Reduce LLFolderView per-item memory + fix three folderview defects View items (cost paid per item, per open inventory window): - Intern per-item-constant layout + colors into a shared LLFolderViewItemStyle const*, deduplicated across items (~100 B/item); also drop two dead fields (mLeftPad, mTextPadRight). - Lazily build the label/suffix LLFontVertexBuffers and release them when an item goes off-screen (setVisible override), so only on-screen items hold text geometry instead of every built item. Defects: - draw(): the icon-overlay branch dereferenced the nullable mIcon for its height; fall back to the overlay's own height. - onIdleUpdateFavorites(): the 'nothing changed' path left mFavoritesDirtyFlags set, leaking a per-idle callback that rescanned children every frame; clear it and deregister. - arrange(): drop two redundant sFonts map lookups (use getLabelFont()/sSuffixFont). Inventory bridge: - Remove dead shadowed base LLInvFVBridge::mTimeSinceRequestStart (~16 B per bridge, per window); only LLFolderBridge's own copy is used. Co-Authored-By: Claude Opus 4.8 (1M context) --- indra/llui/llfolderview.cpp | 10 +- indra/llui/llfolderviewitem.cpp | 167 +++++++++++++++++++-------- indra/llui/llfolderviewitem.h | 54 ++++++--- indra/newview/llconversationview.cpp | 8 +- indra/newview/llinventorybridge.h | 1 - 5 files changed, 169 insertions(+), 71 deletions(-) diff --git a/indra/llui/llfolderview.cpp b/indra/llui/llfolderview.cpp index 10985b47284..aa31b64b8a1 100644 --- a/indra/llui/llfolderview.cpp +++ b/indra/llui/llfolderview.cpp @@ -207,7 +207,7 @@ LLFolderView::LLFolderView(const Params& p) mAutoOpenCandidate = NULL; mAutoOpenTimer.stop(); mKeyboardSelection = false; - mIndentation = getParentFolder() ? getParentFolder()->getIndentation() + mLocalIndentation : 0; + mIndentation = getParentFolder() ? getParentFolder()->getIndentation() + mStyle->localIndentation : 0; //clear label // go ahead and render root folder as usual @@ -230,11 +230,11 @@ LLFolderView::LLFolderView(const Params& p) // Textbox LLTextBox::Params text_p; LLFontGL* font = getLabelFontForStyle(mLabelStyle); - //mIconPad, mTextPad are set in folder_view_item.xml - LLRect new_r = LLRect(rect.mLeft + mIconPad, - rect.mTop - mTextPad, + //icon_pad, text_pad are set in folder_view_item.xml (shared via mStyle) + LLRect new_r = LLRect(rect.mLeft + mStyle->iconPad, + rect.mTop - mStyle->textPad, rect.mRight, - rect.mTop - mTextPad - font->getLineHeight()); + rect.mTop - mStyle->textPad - font->getLineHeight()); text_p.rect(new_r); text_p.name(std::string(p.name)); text_p.font(font); diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index fcc1964bd6d..7ad2e2091e1 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -167,6 +167,51 @@ LLFolderViewItem::Params::Params() max_folder_item_overlap("max_folder_item_overlap", 0) { } +//static +const LLFolderViewItemStyle* LLFolderViewItem::internStyle(const Params& p) +{ + // These come from widget params and never change per item, so we share one + // immutable instance across every item built from the same params. There are + // only a few distinct param sets (inventory items/folders, conversation view, + // etc.), so a linear scan is cheap even when building 100k+ items. + // Main-thread only (UI construction); no synchronization needed. + static std::vector> sStyles; + + LLFolderViewItemStyle candidate; + candidate.fontColor = p.font_color; + candidate.fontHighlightColor = p.font_highlight_color; + candidate.itemHeight = p.item_height; + candidate.localIndentation = p.folder_indentation; + candidate.iconPad = p.icon_pad; + candidate.iconWidth = p.icon_width; + candidate.textPad = p.text_pad; + candidate.textPadTop = p.text_pad_top; + candidate.arrowSize = p.arrow_size; + candidate.arrowPadTop = p.arrow_pad_top; + candidate.maxFolderItemOverlap = p.max_folder_item_overlap; + + for (const std::unique_ptr& style : sStyles) + { + if (style->itemHeight == candidate.itemHeight + && style->localIndentation == candidate.localIndentation + && style->iconPad == candidate.iconPad + && style->iconWidth == candidate.iconWidth + && style->textPad == candidate.textPad + && style->textPadTop == candidate.textPadTop + && style->arrowSize == candidate.arrowSize + && style->arrowPadTop == candidate.arrowPadTop + && style->maxFolderItemOverlap == candidate.maxFolderItemOverlap + && LLInitParam::ParamCompare::equals(style->fontColor, candidate.fontColor) + && LLInitParam::ParamCompare::equals(style->fontHighlightColor, candidate.fontHighlightColor)) + { + return style.get(); + } + } + + sStyles.push_back(std::make_unique(candidate)); + return sStyles.back().get(); +} + // Default constructor LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) : LLView(p), @@ -185,9 +230,8 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) mLabelStyle( LLFontGL::NORMAL ), pLabelFont(nullptr), mHasVisibleChildren(false), - mLocalIndentation(p.folder_indentation), + mStyle(internStyle(p)), mIndentation(0), - mItemHeight(p.item_height), mControlLabelRotation(0.f), mDragAndDropTarget(false), mLabel(utf8str_to_wstring(p.name)), @@ -196,18 +240,7 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) mIsMouseOverTitle(false), mMarketplaceItem(p.marketplace_item), mAllowDrop(p.allow_drop), - mFontColor(p.font_color), - mFontHighlightColor(p.font_highlight_color), - mLeftPad(p.left_pad), - mIconPad(p.icon_pad), - mIconWidth(p.icon_width), - mTextPad(p.text_pad), - mTextPadRight(p.text_pad_right), - mTextPadTop(p.text_pad_top), - mArrowSize(p.arrow_size), - mArrowPadTop(p.arrow_pad_top), mSingleFolderMode(p.single_folder_mode), - mMaxFolderItemOverlap(p.max_folder_item_overlap), mDoubleClickOverride(p.double_click_override) { if (!sColorSetInitialized) @@ -361,7 +394,10 @@ void LLFolderViewItem::refresh() LLFolderViewModelItem& vmi = *getViewModelItem(); mLabel = utf8str_to_wstring(vmi.getDisplayName()); - mLabelFontBuffer.reset(); + if (mLabelFontBuffer) + { + mLabelFontBuffer->reset(); + } mIsFavorite = vmi.isFavorite() && !vmi.isItemInTrash(); setToolTip(vmi.getName()); // icons are slightly expensive to get, can be optimized @@ -377,7 +413,12 @@ void LLFolderViewItem::refresh() mLabelStyle = vmi.getLabelStyle(); pLabelFont = nullptr; // refresh can be called from a coro, don't use getLabelFontForStyle, coro trips font list tread safety mLabelSuffix = utf8str_to_wstring(vmi.getLabelSuffix()); - mSuffixFontBuffer.reset(); + // Only invalidate cached geometry if the buffer exists; if it doesn't, + // draw() will lazily create it. Don't allocate here just to reset it. + if (mSuffixFontBuffer) + { + mSuffixFontBuffer->reset(); + } } // Dirty the filter flag of the model from the view (CHUI-849) @@ -445,7 +486,7 @@ void LLFolderViewItem::addToFolder(LLFolderViewFolder* folder) // Compute indentation since parent folder changed mIndentation = (getParentFolder()) - ? getParentFolder()->getIndentation() + mLocalIndentation + ? getParentFolder()->getIndentation() + mStyle->localIndentation : 0; } @@ -456,7 +497,7 @@ S32 LLFolderViewItem::arrange( S32* width, S32* height ) { // Only indent deeper items in hierarchy mIndentation = (getParentFolder()) - ? getParentFolder()->getIndentation() + mLocalIndentation + ? getParentFolder()->getIndentation() + mStyle->localIndentation : 0; if (mLabelWidthDirty) { @@ -466,7 +507,9 @@ S32 LLFolderViewItem::arrange( S32* width, S32* height ) // it is purely visual, so it is fine to do at our laisure refreshSuffix(); } - mLabelWidth = getLabelXPos() + getLabelFontForStyle(mLabelStyle)->getWidth(mLabel.c_str()) + getLabelFontForStyle(LLFontGL::NORMAL)->getWidth(mLabelSuffix.c_str()) + mLabelPaddingRight; + // getLabelFont() is the cached font for mLabelStyle; sSuffixFont is the cached NORMAL-style + // font. Both avoid the per-call sFonts map lookup that getLabelFontForStyle() does. + mLabelWidth = getLabelXPos() + getLabelFont()->getWidth(mLabel.c_str()) + sSuffixFont->getWidth(mLabelSuffix.c_str()) + mLabelPaddingRight; mLabelWidthDirty = false; if (mIsFavorite) { @@ -489,22 +532,22 @@ S32 LLFolderViewItem::arrange( S32* width, S32* height ) S32 LLFolderViewItem::getItemHeight() const { - return mItemHeight; + return mStyle->itemHeight; } S32 LLFolderViewItem::getLabelXPos() { - return getIndentation() + mArrowSize + mTextPad + mIconWidth + mIconPad; + return getIndentation() + mStyle->arrowSize + mStyle->textPad + mStyle->iconWidth + mStyle->iconPad; } S32 LLFolderViewItem::getIconPad() { - return mIconPad; + return mStyle->iconPad; } S32 LLFolderViewItem::getTextPad() { - return mTextPad; + return mStyle->textPad; } // *TODO: This can be optimized a lot by simply recording that it is @@ -675,7 +718,7 @@ bool LLFolderViewItem::handleMouseDown( S32 x, S32 y, MASK mask ) bool LLFolderViewItem::handleHover( S32 x, S32 y, MASK mask ) { - mIsMouseOverTitle = (y > (getRect().getHeight() - mItemHeight)); + mIsMouseOverTitle = (y > (getRect().getHeight() - mStyle->itemHeight)); if( hasMouseCapture() && isMovable() ) { @@ -815,8 +858,8 @@ void LLFolderViewItem::drawOpenFolderArrow() if (hasVisibleChildren() || !isFolderComplete()) { gl_draw_scaled_rotated_image( - mIndentation, getRect().getHeight() - mArrowSize - mArrowPadTop - sTopPad, - mArrowSize, mArrowSize, mControlLabelRotation, sFolderArrowImg->getImage(), sFgColor); + mIndentation, getRect().getHeight() - mStyle->arrowSize - mStyle->arrowPadTop - sTopPad, + mStyle->arrowSize, mStyle->arrowSize, mControlLabelRotation, sFolderArrowImg->getImage(), sFgColor); } } @@ -851,7 +894,7 @@ void LLFolderViewItem::drawFavoriteIcon() } gl_draw_scaled_image( x_offset - FAVORITE_IMAGE_SIZE - FAVORITE_IMAGE_PAD, - getRect().getHeight() - mItemHeight + FAVORITE_IMAGE_PAD, + getRect().getHeight() - mStyle->itemHeight + FAVORITE_IMAGE_PAD, FAVORITE_IMAGE_SIZE, FAVORITE_IMAGE_SIZE, favorite_image->getImage(), @@ -887,8 +930,8 @@ void LLFolderViewItem::drawHighlight(bool showContent, bool hasKeyboardFocus, const LLUIColor& focusOutlineColor, const LLUIColor& mouseOverColor) { const S32 focus_top = getRect().getHeight(); - const S32 focus_bottom = getRect().getHeight() - mItemHeight; - const bool folder_open = (getRect().getHeight() > mItemHeight + 4); + const S32 focus_bottom = getRect().getHeight() - mStyle->itemHeight; + const bool folder_open = (getRect().getHeight() > mStyle->itemHeight + 4); const S32 FOCUS_LEFT = 1; // Determine which background color to use for highlighting @@ -992,12 +1035,30 @@ void LLFolderViewItem::drawHighlight(bool showContent, bool hasKeyboardFocus, } } +void LLFolderViewItem::setVisible(bool visible) +{ + if (!visible) + { + // Off-screen items don't need cached text geometry; release it (and the + // buffer objects themselves). Rebuilt lazily by drawLabel()/draw() if the + // item is shown again. Resetting an already-null buffer is a no-op, so the + // repeated setVisible(false) calls during folder open/close animation are cheap. + mLabelFontBuffer.reset(); + mSuffixFontBuffer.reset(); + } + LLView::setVisible(visible); +} + void LLFolderViewItem::drawLabel(const LLFontGL * font, const F32 x, const F32 y, const LLColor4& color, F32 &right_x) { //--------------------------------------------------------------------------------// // Draw the actual label text // - mLabelFontBuffer.render(font, mLabel, 0, x, y, color, + if (!mLabelFontBuffer) + { + mLabelFontBuffer = std::make_unique(); + } + mLabelFontBuffer->render(font, mLabel, 0, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, getRect().getWidth() - (S32) x - mLabelPaddingRight, &right_x, /*use_ellipses*/true); } @@ -1023,7 +1084,7 @@ void LLFolderViewItem::draw() //--------------------------------------------------------------------------------// // Draw open icon // - const S32 icon_x = mIndentation + mArrowSize + mTextPad; + const S32 icon_x = mIndentation + mStyle->arrowSize + mStyle->textPad; const S32 rect_height = getRect().getHeight(); if (!mIconOpen.isNull() && (llabs(mControlLabelRotation) > 80)) // For open folders { @@ -1036,7 +1097,10 @@ void LLFolderViewItem::draw() if (mIconOverlay && getRoot()->showItemLinkOverlays()) { - mIconOverlay->draw(icon_x, rect_height - mIcon->getHeight() - sTopPad + 1); + // mIcon may be null (see the 'else if (mIcon)' above); fall back to the + // overlay's own height rather than dereferencing a null icon. + S32 overlay_icon_height = mIcon ? mIcon->getHeight() : mIconOverlay->getHeight(); + mIconOverlay->draw(icon_x, rect_height - overlay_icon_height - sTopPad + 1); } //--------------------------------------------------------------------------------// @@ -1049,7 +1113,7 @@ void LLFolderViewItem::draw() S32 filter_string_length = mViewModelItem->hasFilterStringMatch() ? (S32)mViewModelItem->getFilterStringSize() : 0; F32 right_x = 0; - F32 y = (F32)rect_height - line_height - (F32)mTextPadTop - (F32)sTopPad; + F32 y = (F32)rect_height - line_height - (F32)mStyle->textPadTop - (F32)sTopPad; F32 text_left = (F32)getLabelXPos(); LLWString combined_string = mLabel + mLabelSuffix; @@ -1092,7 +1156,7 @@ void LLFolderViewItem::draw() LLColor4 color; if (mIsSelected && filled) { - color = mFontHighlightColor; + color = mStyle->fontHighlightColor; } else if (mIsFavorite && highlight_color) { @@ -1100,7 +1164,7 @@ void LLFolderViewItem::draw() } else { - color = mFontColor; + color = mStyle->fontColor; } if (isFadeItem()) @@ -1115,7 +1179,11 @@ void LLFolderViewItem::draw() // if (!mLabelSuffix.empty()) { - mSuffixFontBuffer.render(sSuffixFont, mLabelSuffix, 0, right_x, y, isFadeItem() ? color : sSuffixColor.get(), + if (!mSuffixFontBuffer) + { + mSuffixFontBuffer = std::make_unique(); + } + mSuffixFontBuffer->render(sSuffixFont, mLabelSuffix, 0, right_x, y, isFadeItem() ? color : sSuffixColor.get(), LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, S32_MAX, &right_x); } @@ -1128,7 +1196,7 @@ void LLFolderViewItem::draw() if(mLabelSuffix.empty() || (font == sSuffixFont)) { F32 match_string_left = text_left + font->getWidthF32(combined_string.c_str(), 0, filter_offset + filter_string_length) - font->getWidthF32(combined_string.c_str(), filter_offset, filter_string_length); - F32 yy = (F32)rect_height - line_height - (F32)mTextPadTop - (F32)sTopPad; + F32 yy = (F32)rect_height - line_height - (F32)mStyle->textPadTop - (F32)sTopPad; font->render(combined_string, filter_offset, match_string_left, yy, sFilterTextColor, LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, filter_string_length, S32_MAX, &right_x); @@ -1139,7 +1207,7 @@ void LLFolderViewItem::draw() if(label_filter_length > 0) { F32 match_string_left = text_left + font->getWidthF32(mLabel.c_str(), 0, filter_offset + label_filter_length) - font->getWidthF32(mLabel.c_str(), filter_offset, label_filter_length); - F32 yy = (F32)rect_height - line_height - (F32)mTextPadTop - (F32)sTopPad; + F32 yy = (F32)rect_height - line_height - (F32)mStyle->textPadTop - (F32)sTopPad; font->render(mLabel, filter_offset, match_string_left, yy, sFilterTextColor, LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, label_filter_length, S32_MAX, &right_x); @@ -1150,7 +1218,7 @@ void LLFolderViewItem::draw() { S32 suffix_offset = llmax(0, filter_offset - (S32)mLabel.size()); F32 match_string_left = text_left + font->getWidthF32(mLabel.c_str(), 0, static_cast(mLabel.size())) + sSuffixFont->getWidthF32(mLabelSuffix.c_str(), 0, suffix_offset + suffix_filter_length) - sSuffixFont->getWidthF32(mLabelSuffix.c_str(), suffix_offset, suffix_filter_length); - F32 yy = (F32)rect_height - sSuffixFont->getLineHeight() - (F32)mTextPadTop - (F32)sTopPad; + F32 yy = (F32)rect_height - sSuffixFont->getLineHeight() - (F32)mStyle->textPadTop - (F32)sTopPad; sSuffixFont->render(mLabelSuffix, suffix_offset, match_string_left, yy, sFilterTextColor, LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, suffix_filter_length, S32_MAX, &right_x); @@ -1236,7 +1304,7 @@ void LLFolderViewFolder::addToFolder(LLFolderViewFolder* folder) // Compute indentation since parent folder changed mIndentation = (getParentFolder()) - ? getParentFolder()->getIndentation() + mLocalIndentation + ? getParentFolder()->getIndentation() + mStyle->localIndentation : 0; if(isOpen() && folder->isOpen()) @@ -1386,7 +1454,7 @@ S32 LLFolderViewFolder::arrange( S32* width, S32* height ) folders_t::iterator fit = iter++; // number of pixels that bottom of folder label is from top of parent folder if (getRect().getHeight() - (*fit)->getRect().mTop + (*fit)->getItemHeight() - > ll_round(mCurHeight) + mMaxFolderItemOverlap) + > ll_round(mCurHeight) + mStyle->maxFolderItemOverlap) { // hide if beyond current folder height (*fit)->setVisible(false); @@ -1399,7 +1467,7 @@ S32 LLFolderViewFolder::arrange( S32* width, S32* height ) items_t::iterator iit = iter++; // number of pixels that bottom of item label is from top of parent folder if (getRect().getHeight() - (*iit)->getRect().mBottom - > ll_round(mCurHeight) + mMaxFolderItemOverlap) + > ll_round(mCurHeight) + mStyle->maxFolderItemOverlap) { (*iit)->setVisible(false); } @@ -1982,7 +2050,16 @@ void LLFolderViewFolder::onIdleUpdateFavorites(void* data) } else { - // Nothing changed + // Nothing changed. On a later iteration 'self' was already + // flagged FAVORITE_CLEANUP before we advanced, but if we bail + // on the very first iteration (parent == self) it never was, + // so clear it here. Otherwise this idle callback stays + // registered and re-scans every frame, never getting removed. + if (parent == self) + { + self->mFavoritesDirtyFlags = 0; + gIdleCallbacks.deleteFunction(&LLFolderViewFolder::onIdleUpdateFavorites, self); + } break; } } @@ -2317,7 +2394,7 @@ bool LLFolderViewFolder::handleRightMouseDown( S32 x, S32 y, MASK mask ) bool LLFolderViewFolder::handleHover(S32 x, S32 y, MASK mask) { - mIsMouseOverTitle = (y > (getRect().getHeight() - mItemHeight)); + mIsMouseOverTitle = (y > (getRect().getHeight() - mStyle->itemHeight)); bool handled = LLView::handleHover(x, y, mask); @@ -2339,7 +2416,7 @@ bool LLFolderViewFolder::handleMouseDown( S32 x, S32 y, MASK mask ) } if( !handled ) { - if((mIndentation < x && x < mIndentation + (isCollapsed() ? 0 : mArrowSize) + mTextPad) + if((mIndentation < x && x < mIndentation + (isCollapsed() ? 0 : mStyle->arrowSize) + mStyle->textPad) && !mSingleFolderMode) { toggleOpen(); @@ -2398,7 +2475,7 @@ bool LLFolderViewFolder::handleDoubleClick( S32 x, S32 y, MASK mask ) return true; } } - if(mIndentation < x && x < mIndentation + (isCollapsed() ? 0 : mArrowSize) + mTextPad) + if(mIndentation < x && x < mIndentation + (isCollapsed() ? 0 : mStyle->arrowSize) + mStyle->textPad) { // don't select when user double-clicks plus sign // so as not to contradict single-click behavior diff --git a/indra/llui/llfolderviewitem.h b/indra/llui/llfolderviewitem.h index 258a806b913..958a8ab43e8 100644 --- a/indra/llui/llfolderviewitem.h +++ b/indra/llui/llfolderviewitem.h @@ -31,6 +31,8 @@ #include "lluiimage.h" #include "llfontvertexbuffer.h" +#include + class LLFolderView; class LLFolderViewModelItem; class LLFolderViewFolder; @@ -38,6 +40,27 @@ class LLFolderViewFunctor; class LLFolderViewFilter; class LLFolderViewModelInterface; +// Per-item layout and colors that come from Params and never vary per item. +// They are identical for every item built from the same widget params, so rather +// than copying ~100 bytes into each of (potentially) hundreds of thousands of +// items, the values are interned (see LLFolderViewItem::internStyle) and shared +// by const pointer. Anything an item mutates per-instance (e.g. indentation, +// label padding) stays a direct member. +struct LLFolderViewItemStyle +{ + LLUIColor fontColor; + LLUIColor fontHighlightColor; + S32 itemHeight{ 0 }; + S32 localIndentation{ 0 }; + S32 iconPad{ 0 }; + S32 iconWidth{ 0 }; + S32 textPad{ 0 }; + S32 textPadTop{ 0 }; + S32 arrowSize{ 0 }; + S32 arrowPadTop{ 0 }; + S32 maxFolderItemOverlap{ 0 }; +}; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Class LLFolderViewItem // @@ -108,22 +131,12 @@ class LLFolderViewItem : public LLView LLUIImagePtr mIcon, mIconOpen, mIconOverlay; - S32 mLocalIndentation; + // Shared, interned const layout + colors (see LLFolderViewItemStyle). + const LLFolderViewItemStyle* mStyle; S32 mIndentation; - S32 mItemHeight; S32 mDragStartX, mDragStartY; - S32 mLeftPad, - mIconPad, - mIconWidth, - mTextPad, - mTextPadRight, - mTextPadTop, - mArrowSize, - mArrowPadTop, - mMaxFolderItemOverlap; - F32 mControlLabelRotation; LLFolderView* mRoot; bool mHasVisibleChildren, @@ -139,8 +152,6 @@ class LLFolderViewItem : public LLView S32 mCutGeneration; - LLUIColor mFontColor; - LLUIColor mFontHighlightColor; static bool sColorSetInitialized; // For now assuming all colors are the same in derived classes. @@ -308,6 +319,9 @@ class LLFolderViewItem : public LLView //virtual LLView* findChildView(const std::string& name, bool recurse) const { return LLView::findChildView(name, recurse); } + // Releases cached text geometry when hidden so off-screen items hold no font buffers. + virtual void setVisible(bool visible); + // virtual void handleDropped(); virtual void draw(); void drawOpenFolderArrow(); @@ -329,8 +343,16 @@ class LLFolderViewItem : public LLView static LLUIImagePtr sFavoriteContentImg; static LLFontGL* sSuffixFont; - LLFontVertexBuffer mLabelFontBuffer; - LLFontVertexBuffer mSuffixFontBuffer; + // Returns a shared, immutable style for the given params, deduplicated across + // all items (there are only a handful of distinct param sets). Main-thread only. + static const LLFolderViewItemStyle* internStyle(const Params& p); + + // Cached text geometry. Lazily (re)built during draw() and released when the + // item goes off-screen (see setVisible), so only on-screen items hold vertex + // buffers and their MSVC per-std::list sentinel allocations. Null until first + // drawn, after being hidden, or (for the suffix) when there is no suffix. + std::unique_ptr mLabelFontBuffer; + std::unique_ptr mSuffixFontBuffer; LLFontGL* pLabelFont{nullptr}; }; diff --git a/indra/newview/llconversationview.cpp b/indra/newview/llconversationview.cpp index 4701aad64a2..22188b1f513 100644 --- a/indra/newview/llconversationview.cpp +++ b/indra/newview/llconversationview.cpp @@ -418,7 +418,7 @@ S32 LLConversationViewSession::arrange(S32* width, S32* height) //LLFolderViewFolder::arrange computes value for getIndentation() function below S32 arranged = LLFolderViewFolder::arrange(width, height); - S32 h_pad = mHasArrow ? getIndentation() + mArrowSize : getIndentation(); + S32 h_pad = mHasArrow ? getIndentation() + mStyle->arrowSize : getIndentation(); LLRect rect(mCollapsedMode ? getLocalRect().mLeft : h_pad, getLocalRect().mTop, @@ -454,7 +454,7 @@ void LLConversationViewSession::toggleCollapsedMode(bool is_collapsed) // except for the icon which we display in minimized mode getChild("conversation_item_stack")->setVisible(!mCollapsedMode); - S32 h_pad = mHasArrow ? getIndentation() + mArrowSize : getIndentation(); + S32 h_pad = mHasArrow ? getIndentation() + mStyle->arrowSize : getIndentation(); mItemPanel->translate(mCollapsedMode ? -h_pad : h_pad, 0); } @@ -692,7 +692,7 @@ void LLConversationViewParticipant::draw() const LLFontGL* font = getLabelFontForStyle(mLabelStyle); F32 right_x = 0; - F32 y = (F32)getRect().getHeight() - font->getLineHeight() - (F32)mTextPad; + F32 y = (F32)getRect().getHeight() - font->getLineHeight() - (F32)mStyle->textPad; F32 text_left = (F32)getLabelXPos(); LLUIColor* color; @@ -837,7 +837,7 @@ void LLConversationViewParticipant::onMouseLeave(S32 x, S32 y, MASK mask) S32 LLConversationViewParticipant::getLabelXPos() { - return getIndentation() + mAvatarIcon->getRect().getWidth() + mIconPad; + return getIndentation() + mAvatarIcon->getRect().getWidth() + mStyle->iconPad; } // static diff --git a/indra/newview/llinventorybridge.h b/indra/newview/llinventorybridge.h index bb869e0944b..c20c4302399 100644 --- a/indra/newview/llinventorybridge.h +++ b/indra/newview/llinventorybridge.h @@ -201,7 +201,6 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory const LLUUID mUUID; // item id LLInventoryType::EType mInvType; bool mIsLink; - LLTimer mTimeSinceRequestStart; mutable std::string mDisplayName; mutable std::string mSearchableName; From 61ccaf92ebf81ef0566156898171f03272a164db Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 9 Jun 2026 23:17:20 -0400 Subject: [PATCH 3/3] Invalidate cached suffix geometry in LLFolderViewItem::refreshSuffix LLFontVertexBuffer::render does not compare the rendered string, so any path that changes mLabelSuffix must reset the cached geometry or draw() replays the old text. refresh() already does this; mirror it in refreshSuffix(), which is reached via arrange() for items deferred from postBuild(). Currently the buffer is always null on that path (items arrange before first draw), so this is hardening against reordering or future callers rather than a user-visible defect. Co-Authored-By: Claude Fable 5 --- indra/llui/llfolderviewitem.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index 7ad2e2091e1..0bf4a5fa8d4 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -447,6 +447,13 @@ void LLFolderViewItem::refreshSuffix() mLabelStyle = vmi->getLabelStyle(); pLabelFont = nullptr; mLabelSuffix = utf8str_to_wstring(vmi->getLabelSuffix()); + // LLFontVertexBuffer::render doesn't compare the string, so cached + // geometry must be invalidated here or it would replay the old suffix. + // (A style change is covered by render's font-pointer compare.) + if (mSuffixFontBuffer) + { + mSuffixFontBuffer->reset(); + } } mLabelWidthDirty = true;