diff --git a/indra/llprimitive/llmaterial.cpp b/indra/llprimitive/llmaterial.cpp
index f6cb3c8b707bc0f727fbda3538a33f5cc9e2aedd..0d146de949eb76114aaadc4ce39456c6119545ec 100644
--- a/indra/llprimitive/llmaterial.cpp
+++ b/indra/llprimitive/llmaterial.cpp
@@ -332,17 +332,6 @@ void LLMaterial::setAlphaMaskCutoff(U8 cutoff)
     mAlphaMaskCutoff = cutoff;
 }
 
-LLUUID LLMaterial::getMaterialID() const
-{
-    // TODO - not null
-    return LLUUID::null;
-}
-
-void LLMaterial::setMaterialID(const LLUUID &material_id)
-{
-    // TODO - set
-}
-
 LLSD LLMaterial::asLLSD() const
 {
     LLSD material_data;
diff --git a/indra/llprimitive/llmaterial.h b/indra/llprimitive/llmaterial.h
index b46d85c2d14ee0aa7e806cc037969fe4b9fc2552..81f41ddc511d46f93e368d83ee42c6ef8b4262f3 100644
--- a/indra/llprimitive/llmaterial.h
+++ b/indra/llprimitive/llmaterial.h
@@ -115,8 +115,6 @@ class LLMaterial : public LLRefCount
     void        setDiffuseAlphaMode(U8 alpha_mode);
     U8          getAlphaMaskCutoff() const;
     void        setAlphaMaskCutoff(U8 cutoff);
-    LLUUID      getMaterialID() const;
-    void        setMaterialID(LLUUID const & material_id);
 
     bool        isNull() const;
     static const LLMaterial null;
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index c6d82ea260ccdd15a5a27666441942984c056bbc..d413c12de9462638f2c91add037be8004e4d666b 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -246,6 +246,7 @@ set(viewer_SOURCE_FILES
     llfloatermyscripts.cpp
     llfloatermyenvironment.cpp
     llfloaternamedesc.cpp
+    llfloaternewfeaturenotification.cpp
     llfloaternotificationsconsole.cpp
     llfloaternotificationstabbed.cpp
     llfloateroutfitphotopreview.cpp 
@@ -895,6 +896,7 @@ set(viewer_HEADER_FILES
     llfloatermyscripts.h
     llfloatermyenvironment.h
     llfloaternamedesc.h
+    llfloaternewfeaturenotification.h
     llfloaternotificationsconsole.h
     llfloaternotificationstabbed.h
     llfloateroutfitphotopreview.h
diff --git a/indra/newview/app_settings/logcontrol.xml b/indra/newview/app_settings/logcontrol.xml
index 2a26cb9a43bee986175fd10463fc4e59432bfa0c..482012cdd6b0c2774b1c21dd1f39eedacfbfbf2b 100644
--- a/indra/newview/app_settings/logcontrol.xml
+++ b/indra/newview/app_settings/logcontrol.xml
@@ -73,7 +73,6 @@
 						     <string>Avatar</string>
 						     <string>Voice</string>		
 						-->
-              <string>Capabilities</string>
 						</array>
 				</map>
       </array>
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 4612ee1a822f6539d69ecac8b65be9f356443729..ee4c76943ea8fc8f44f75ea3d8173471668501c9 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -250,7 +250,7 @@
     <key>TextureLivePreview</key>
     <map>
       <key>Comment</key>
-      <string>Preview selections in texture picker immediately</string>
+      <string>Preview selections in texture picker or material picker immediately</string>
       <key>Persist</key>
       <integer>1</integer>
       <key>Type</key>
@@ -5519,6 +5519,17 @@
       <key>Value</key>
       <integer>0</integer>
     </map>
+    <key>LastUIFeatureVersion</key>
+    <map>
+        <key>Comment</key>
+        <string>UI Feature Version number for tracking feature notification between viewer builds</string>
+        <key>Persist</key>
+        <integer>1</integer>
+        <key>Type</key>
+        <string>LLSD</string>
+        <key>Value</key>
+        <string></string>
+    </map>
     <key>LastFindPanel</key>
     <map>
       <key>Comment</key>
diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp
index ea176dd8de8a0c16c7ab67a6719e083c6e88b924..be764287cc7a43b238cc6ca73501c83f95ff0173 100644
--- a/indra/newview/llagent.cpp
+++ b/indra/newview/llagent.cpp
@@ -120,6 +120,11 @@ const F64 CHAT_AGE_FAST_RATE = 3.0;
 const F32 MIN_FIDGET_TIME = 8.f; // seconds
 const F32 MAX_FIDGET_TIME = 20.f; // seconds
 
+const S32 UI_FEATURE_VERSION = 1;
+// For version 1: 1 - inventory, 2 - gltf
+// Will need to change to 3 once either inventory or gltf releases and cause a conflict
+const S32 UI_FEATURE_FLAGS = 2;
+
 // The agent instance.
 LLAgent gAgent;
 
@@ -372,7 +377,7 @@ LLAgent::LLAgent() :
 	mHideGroupTitle(FALSE),
 	mGroupID(),
 
-	mInitialized(FALSE),
+	mInitialized(false),
 	mListener(),
 
 	mDoubleTapRunTimer(),
@@ -447,7 +452,7 @@ LLAgent::LLAgent() :
 
 	mNextFidgetTime(0.f),
 	mCurrentFidget(0),
-	mFirstLogin(FALSE),
+	mFirstLogin(false),
 	mOutfitChosen(FALSE),
 
 	mVoiceConnected(false),
@@ -504,7 +509,7 @@ void LLAgent::init()
 
 	mHttpPolicy = app_core_http.getPolicy(LLAppCoreHttp::AP_AGENT);
 
-	mInitialized = TRUE;
+	mInitialized = true;
 }
 
 //-----------------------------------------------------------------------------
@@ -559,6 +564,93 @@ void LLAgent::onAppFocusGained()
 	}
 }
 
+void LLAgent::setFirstLogin(bool b)
+{
+    mFirstLogin = b;
+
+    if (mFirstLogin)
+    {
+        // Don't notify new users about new features
+        if (getFeatureVersion() <= UI_FEATURE_VERSION)
+        {
+            setFeatureVersion(UI_FEATURE_VERSION, UI_FEATURE_FLAGS);
+        }
+    }
+}
+
+void LLAgent::setFeatureVersion(S32 version, S32 flags)
+{
+    LLSD updated_version;
+    updated_version["version"] = version;
+    updated_version["flags"] = flags;
+    gSavedSettings.setLLSD("LastUIFeatureVersion", updated_version);
+}
+
+S32 LLAgent::getFeatureVersion()
+{
+    S32 version;
+    S32 flags;
+    getFeatureVersionAndFlags(version, flags);
+    return version;
+}
+
+void LLAgent::getFeatureVersionAndFlags(S32& version, S32& flags)
+{
+    version = 0;
+    flags = 0;
+    LLSD feature_version = gSavedSettings.getLLSD("LastUIFeatureVersion");
+    if (feature_version.isInteger())
+    {
+        version = feature_version.asInteger();
+        flags = 1; // inventory flag
+    }
+    else if (feature_version.isMap())
+    {
+        version = feature_version["version"];
+        flags = feature_version["flags"];
+    }
+    else if (!feature_version.isString() && !feature_version.isUndefined())
+    {
+        // is something newer inside?
+        version = UI_FEATURE_VERSION;
+        flags = UI_FEATURE_FLAGS;
+    }
+}
+
+void LLAgent::showLatestFeatureNotification(const std::string key)
+{
+    S32 version;
+    S32 flags; // a single release can have multiple new features
+    getFeatureVersionAndFlags(version, flags);
+    if (version <= UI_FEATURE_VERSION && (flags & UI_FEATURE_FLAGS) != UI_FEATURE_FLAGS)
+    {
+        S32 flag = 0;
+
+        if (key == "inventory")
+        {
+            // Notify user about new thumbnail support
+            flag = 1;
+        }
+
+        if (key == "gltf")
+        {
+            flag = 2;
+        }
+
+        if ((flags & flag) == 0)
+        {
+            // Need to open on top even if called from onOpen,
+            // do on idle to make sure it's on top
+            LLSD floater_key(key);
+            doOnIdleOneTime([floater_key]()
+                            {
+                                LLFloaterReg::showInstance("new_feature_notification", floater_key);
+                            });
+
+            setFeatureVersion(UI_FEATURE_VERSION, flags | flag);
+        }
+    }
+}
 
 void LLAgent::ageChat()
 {
diff --git a/indra/newview/llagent.h b/indra/newview/llagent.h
index 498bea3c0701bc60cd359928b1ac04d488b04d91..0ce6fda131859bddd1dbc9994663bfa7cc574830 100644
--- a/indra/newview/llagent.h
+++ b/indra/newview/llagent.h
@@ -117,15 +117,20 @@ class LLAgent : public LLOldEvents::LLObservable
 	//--------------------------------------------------------------------
 public:
 	void			onAppFocusGained();
-	void			setFirstLogin(BOOL b) 	{ mFirstLogin = b; }
+	void			setFirstLogin(bool b);
 	// Return TRUE if the database reported this login as the first for this particular user.
-	BOOL 			isFirstLogin() const 	{ return mFirstLogin; }
-	BOOL 			isInitialized() const 	{ return mInitialized; }
+	bool 			isFirstLogin() const 	{ return mFirstLogin; }
+	bool 			isInitialized() const 	{ return mInitialized; }
+
+    void            setFeatureVersion(S32 version, S32 flags);
+    S32             getFeatureVersion();
+    void            getFeatureVersionAndFlags(S32 &version, S32 &flags);
+    void            showLatestFeatureNotification(const std::string key);
 public:
 	std::string		mMOTD; 					// Message of the day
 private:
-	BOOL			mInitialized;
-	BOOL			mFirstLogin;
+	bool			mInitialized;
+	bool			mFirstLogin;
 	boost::shared_ptr<LLAgentListener> mListener;
 
 	//--------------------------------------------------------------------
diff --git a/indra/newview/llfloaterbulkpermission.cpp b/indra/newview/llfloaterbulkpermission.cpp
index a3cc939f85f4b59c1f3b108d702f98dfc87dc05f..abc9cdbcc24d83b4102e11d2841d9635f638faa9 100644
--- a/indra/newview/llfloaterbulkpermission.cpp
+++ b/indra/newview/llfloaterbulkpermission.cpp
@@ -76,6 +76,8 @@ BOOL LLFloaterBulkPermission::postBuild()
 	mBulkChangeIncludeSounds = gSavedSettings.getBOOL("BulkChangeIncludeSounds");
 	mBulkChangeIncludeTextures = gSavedSettings.getBOOL("BulkChangeIncludeTextures");
 	mBulkChangeIncludeSettings = gSavedSettings.getBOOL("BulkChangeIncludeSettings");
+    mBulkChangeIncludeMaterials = gSavedSettings.getBOOL("BulkChangeIncludeMaterials");
+
 	mBulkChangeShareWithGroup = gSavedSettings.getBOOL("BulkChangeShareWithGroup");
 	mBulkChangeEveryoneCopy = gSavedSettings.getBOOL("BulkChangeEveryoneCopy");
 	mBulkChangeNextOwnerModify = gSavedSettings.getBOOL("BulkChangeNextOwnerModify");
@@ -188,6 +190,8 @@ void LLFloaterBulkPermission::onCloseBtn()
 	gSavedSettings.setBOOL("BulkChangeIncludeSounds", mBulkChangeIncludeSounds);
 	gSavedSettings.setBOOL("BulkChangeIncludeTextures", mBulkChangeIncludeTextures);
 	gSavedSettings.setBOOL("BulkChangeIncludeSettings", mBulkChangeIncludeSettings);
+    gSavedSettings.setBOOL("BulkChangeIncludeMaterials", mBulkChangeIncludeMaterials);
+
 	gSavedSettings.setBOOL("BulkChangeShareWithGroup", mBulkChangeShareWithGroup);
 	gSavedSettings.setBOOL("BulkChangeEveryoneCopy", mBulkChangeEveryoneCopy);
 	gSavedSettings.setBOOL("BulkChangeNextOwnerModify", mBulkChangeNextOwnerModify);
@@ -284,6 +288,7 @@ void LLFloaterBulkPermission::doCheckUncheckAll(BOOL check)
 	gSavedSettings.setBOOL("BulkChangeIncludeSounds"    , check);
 	gSavedSettings.setBOOL("BulkChangeIncludeTextures"  , check);
 	gSavedSettings.setBOOL("BulkChangeIncludeSettings"  , check);
+    gSavedSettings.setBOOL("BulkChangeIncludeMaterials"  , check);
 }
 
 
diff --git a/indra/newview/llfloaterbulkpermission.h b/indra/newview/llfloaterbulkpermission.h
index 1afc876bba981406d3dde48443505d6add8d7e87..ab5d568667d9bc87327005d68d1961971ea9e942 100644
--- a/indra/newview/llfloaterbulkpermission.h
+++ b/indra/newview/llfloaterbulkpermission.h
@@ -103,6 +103,8 @@ class LLFloaterBulkPermission : public LLFloater, public LLVOInventoryListener
 	bool mBulkChangeIncludeSounds;
 	bool mBulkChangeIncludeTextures;
 	bool mBulkChangeIncludeSettings;
+    bool mBulkChangeIncludeMaterials;
+
 	bool mBulkChangeShareWithGroup;
 	bool mBulkChangeEveryoneCopy;
 	bool mBulkChangeNextOwnerModify;
diff --git a/indra/newview/llfloaternewfeaturenotification.cpp b/indra/newview/llfloaternewfeaturenotification.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1e5002496744298a6afdda57d09e95bf958886ae
--- /dev/null
+++ b/indra/newview/llfloaternewfeaturenotification.cpp
@@ -0,0 +1,83 @@
+/** 
+ * @file llfloaternewfeaturenotification.cpp
+ * @brief LLFloaterNewFeatureNotification class implementation
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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 "llfloaternewfeaturenotification.h"
+
+
+LLFloaterNewFeatureNotification::LLFloaterNewFeatureNotification(const LLSD& key)
+  : LLFloater(key)
+{
+}
+
+LLFloaterNewFeatureNotification::~LLFloaterNewFeatureNotification()
+{
+}
+
+BOOL LLFloaterNewFeatureNotification::postBuild()
+{
+    setCanDrag(FALSE);
+    getChild<LLButton>("close_btn")->setCommitCallback(boost::bind(&LLFloaterNewFeatureNotification::onCloseBtn, this));
+
+    const std::string title_txt = "title_txt";
+    const std::string dsc_txt = "description_txt";
+    std::string feature = "_" + getKey().asString();
+    
+    getChild<LLUICtrl>(title_txt)->setValue(getString(title_txt + feature));
+    getChild<LLUICtrl>(dsc_txt)->setValue(getString(dsc_txt + feature));
+
+    if (getKey().asString() == "gltf")
+    {
+        LLRect rect = getRect();
+        // make automatic?
+        reshape(rect.getWidth() + 90, rect.getHeight() + 45);
+    }
+
+    return TRUE;
+}
+
+void LLFloaterNewFeatureNotification::onOpen(const LLSD& key)
+{
+    centerOnScreen();
+}
+
+void LLFloaterNewFeatureNotification::onCloseBtn()
+{
+    closeFloater();
+}
+
+void LLFloaterNewFeatureNotification::centerOnScreen()
+{
+    LLVector2 window_size = LLUI::getInstance()->getWindowSize();
+    centerWithin(LLRect(0, 0, ll_round(window_size.mV[VX]), ll_round(window_size.mV[VY])));
+    LLFloaterView* parent = dynamic_cast<LLFloaterView*>(getParent());
+    if (parent)
+    {
+        parent->bringToFront(this);
+    }
+}
+
diff --git a/indra/newview/llfloaternewfeaturenotification.h b/indra/newview/llfloaternewfeaturenotification.h
new file mode 100644
index 0000000000000000000000000000000000000000..95501451dc51c3f8b9b96051c9fe13d0b4ddb0c6
--- /dev/null
+++ b/indra/newview/llfloaternewfeaturenotification.h
@@ -0,0 +1,49 @@
+/** 
+ * @file llfloaternewfeaturenotification.h
+ * @brief LLFloaterNewFeatureNotification class definition
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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_FLOATER_NEW_FEATURE_NOTOFICATION_H
+#define LL_FLOATER_NEW_FEATURE_NOTOFICATION_H
+
+#include "llfloater.h"
+
+class LLFloaterNewFeatureNotification:
+    public LLFloater
+{
+    friend class LLFloaterReg;
+public:
+    BOOL postBuild() override;
+    void onOpen(const LLSD& key) override;
+
+private:
+    LLFloaterNewFeatureNotification(const LLSD& key);
+    /*virtual*/	~LLFloaterNewFeatureNotification();
+
+    void centerOnScreen();
+
+    void onCloseBtn();	
+};
+
+#endif
diff --git a/indra/newview/llmaterialeditor.cpp b/indra/newview/llmaterialeditor.cpp
index 3a0e64985c70cac02aac2a2d80d07d62ffaf5997..54d85c87acc042c7757262eacbc1d4852aceb020 100644
--- a/indra/newview/llmaterialeditor.cpp
+++ b/indra/newview/llmaterialeditor.cpp
@@ -37,6 +37,7 @@
 #include "llfilesystem.h"
 #include "llgltfmateriallist.h"
 #include "llinventorymodel.h"
+#include "llinventoryobserver.h"
 #include "lllocalgltfmaterials.h"
 #include "llnotificationsutil.h"
 #include "lltexturectrl.h"
@@ -237,7 +238,9 @@ struct LLSelectedTEGetMatData : public LLSelectedTEFunctor
     LLUUID mTexEmissiveId;
     LLUUID mTexNormalId;
     LLUUID mObjectId;
+    LLViewerObject* mObject = nullptr;
     S32 mObjectTE;
+    LLUUID mMaterialId;
     LLPointer<LLGLTFMaterial> mMaterial;
     LLPointer<LLLocalGLTFMaterial> mLocalMaterial;
 };
@@ -259,6 +262,7 @@ bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
         return false;
     }
     LLUUID mat_id = objectp->getRenderMaterialID(te_index);
+    mMaterialId = mat_id;
     bool can_use = mIsOverride ? objectp->permModify() : objectp->permCopy();
     LLTextureEntry *tep = objectp->getTE(te_index);
     // We might want to disable this entirely if at least
@@ -290,6 +294,7 @@ bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
                 mTexEmissiveId = tex_emissive_id;
                 mTexNormalId = tex_normal_id;
                 mObjectTE = te_index;
+                mObject = objectp;
                 mObjectId = objectp->getID();
                 mFirst = false;
             }
@@ -318,6 +323,8 @@ bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
             LLGLTFMaterial *mat = tep->getGLTFMaterial();
             LLLocalGLTFMaterial *local_mat = dynamic_cast<LLLocalGLTFMaterial*>(mat);
 
+            mObject = objectp;
+            mObjectId = objectp->getID();
             if (local_mat)
             {
                 mLocalMaterial = local_mat;
@@ -1192,7 +1199,9 @@ bool LLMaterialEditor::saveIfNeeded()
     { 
         //make a new inventory item
         std::string res_desc = buildMaterialDescription();
-        createInventoryItem(buffer, mMaterialName, res_desc);
+        LLPermissions local_permissions;
+        local_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+        createInventoryItem(buffer, mMaterialName, res_desc, local_permissions);
 
         // We do not update floater with uploaded asset yet, so just close it.
         closeFloater();
@@ -1286,36 +1295,37 @@ bool LLMaterialEditor::updateInventoryItem(const std::string &buffer, const LLUU
     return true;
 }
 
-void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc)
+// Callback intended for when a material is saved from an object and needs to
+// be modified to reflect the new asset/name.
+class LLObjectsMaterialItemCallback : public LLInventoryCallback
 {
-    // gen a new uuid for this asset
-    LLTransactionID tid;
-    tid.generate();     // timestamp-based randomization + uniquification
-    U32 next_owner_perm = LLFloaterPerms::getNextOwnerPerms("Materials");
-    LLUUID parent = gInventory.findUserDefinedCategoryUUIDForType(LLFolderType::FT_MATERIAL);
-    const U8 subtype = NO_INV_SUBTYPE;  // TODO maybe use AT_SETTINGS and LLSettingsType::ST_MATERIAL ?
+public:
+    LLObjectsMaterialItemCallback(const LLPermissions& permissions, const std::string& asset_data, const std::string& new_name)
+        : mPermissions(permissions),
+        mAssetData(asset_data),
+        mNewName(new_name)
+    {
+    }
 
-    create_inventory_item(gAgent.getID(), gAgent.getSessionID(), parent, tid, name, desc,
-        LLAssetType::AT_MATERIAL, LLInventoryType::IT_MATERIAL, subtype, next_owner_perm,
-        new LLBoostFuncInventoryCallback([output = buffer](LLUUID const& inv_item_id)
+    void fire(const LLUUID& inv_item_id) override
     {
         LLViewerInventoryItem* item = gInventory.getItem(inv_item_id);
-        if (item)
+        if (!item)
         {
-            // create_inventory_item doesn't allow presetting some permissions, fix it now
-            LLPermissions perm = item->getPermissions();
-            if (perm.getMaskEveryone() != LLFloaterPerms::getEveryonePerms("Materials")
-                || perm.getMaskGroup() != LLFloaterPerms::getGroupPerms("Materials"))
-            {
-                perm.setMaskEveryone(LLFloaterPerms::getEveryonePerms("Materials"));
-                perm.setMaskGroup(LLFloaterPerms::getGroupPerms("Materials"));
+            return;
+        }
 
-                item->setPermissions(perm);
+        // create_inventory_item/copy_inventory_item don't allow presetting some permissions, fix it now
+        item->setPermissions(mPermissions);
+        item->updateServer(FALSE);
+        gInventory.updateItem(item);
+        gInventory.notifyObservers();
 
-                item->updateServer(FALSE);
-                gInventory.updateItem(item);
-                gInventory.notifyObservers();
-            }
+        if (item->getName() != mNewName)
+        {
+            LLSD updates;
+            updates["name"] = mNewName;
+            update_inventory_item(inv_item_id, updates, NULL);
         }
 
         // from reference in LLSettingsVOBase::createInventoryItem()/updateInventoryItem()
@@ -1323,7 +1333,7 @@ void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std:
             std::make_shared<LLBufferedAssetUploadInfo>(
                 inv_item_id,
                 LLAssetType::AT_MATERIAL,
-                output,
+                mAssetData,
                 [](LLUUID item_id, LLUUID new_asset_id, LLUUID new_item_id, LLSD response)
                 {
                     // done callback
@@ -1342,8 +1352,25 @@ void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std:
             }
             LLViewerAssetUpload::EnqueueInventoryUpload(agent_url, uploadInfo);
         }
-    })
-    );
+    }
+private:
+    LLPermissions mPermissions;
+    std::string mAssetData;
+    std::string mNewName;
+};
+
+void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc, const LLPermissions& permissions)
+{
+    // gen a new uuid for this asset
+    LLTransactionID tid;
+    tid.generate();     // timestamp-based randomization + uniquification
+    LLUUID parent = gInventory.findUserDefinedCategoryUUIDForType(LLFolderType::FT_MATERIAL);
+    const U8 subtype = NO_INV_SUBTYPE;  // TODO maybe use AT_SETTINGS and LLSettingsType::ST_MATERIAL ?
+
+    LLPointer<LLObjectsMaterialItemCallback> cb = new LLObjectsMaterialItemCallback(permissions, buffer, name);
+    create_inventory_item(gAgent.getID(), gAgent.getSessionID(), parent, tid, name, desc,
+        LLAssetType::AT_MATERIAL, LLInventoryType::IT_MATERIAL, subtype, permissions.getMaskNextOwner(),
+        cb);
 }
 
 void LLMaterialEditor::finishInventoryUpload(LLUUID itemId, LLUUID newAssetId, LLUUID newItemId)
@@ -1792,55 +1819,151 @@ void LLMaterialEditor::loadLive()
     }
 }
 
-void LLMaterialEditor::saveObjectsMaterialAs()
+// *NOTE: permissions_out includes user preferences for new item creation (LLFloaterPerms)
+bool can_use_objects_material(LLSelectedTEGetMatData& func, const std::vector<PermissionBit>& ops, LLPermissions& permissions_out, LLViewerInventoryItem*& item_out)
 {
-    LLSelectedTEGetMatData func(false);
+    if (!LLMaterialEditor::capabilitiesAvailable())
+    {
+        return false;
+    }
+
+    // func.mIsOverride=true is used for the singleton material editor floater
+    // associated with the build floater. This flag also excludes objects from
+    // the selection that do not satisfy PERM_MODIFY.
+    llassert(func.mIsOverride);
     LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func, true /*first applicable*/);
-    saveMaterialAs(func.mMaterial, func.mLocalMaterial);
-}
-void LLMaterialEditor::savePickedMaterialAs()
-{
-    LLPickInfo pick = LLToolPie::getInstance()->getPick();
-    if (pick.mPickType != LLPickInfo::PICK_OBJECT || !pick.getObject())
+
+    LLViewerObject* selected_object = func.mObject;
+    if (!selected_object)
     {
-        return;
+        // LLSelectedTEGetMatData can fail if there are no selected faces
+        // with materials, but we expect at least some object is selected.
+        llassert(LLSelectMgr::getInstance()->getSelection()->getFirstObject());
+        return false;
+    }
+    if (selected_object->isInventoryPending())
+    {
+        return false;
+    }
+    for (PermissionBit op : ops)
+    {
+        if (op == PERM_MODIFY && selected_object->isPermanentEnforced())
+        {
+            return false;
+        }
     }
 
-    LLPointer<LLGLTFMaterial> render_material;
-    LLPointer<LLLocalGLTFMaterial> local_material;
+    item_out = selected_object->getInventoryItemByAsset(func.mMaterialId);
 
-    LLViewerObject *objectp = pick.getObject();
-    LLUUID mat_id = objectp->getRenderMaterialID(pick.mObjectFace);
-    if (mat_id.notNull() && objectp->permCopy())
+    LLPermissions item_permissions;
+    if (item_out)
     {
-        // Try a face user picked first
-        // (likely the only method we need, but in such case
-        // enable_object_save_gltf_material will need to check this)
-        LLTextureEntry *tep = objectp->getTE(pick.mObjectFace);
-        LLGLTFMaterial *mat = tep->getGLTFMaterial();
-        LLLocalGLTFMaterial *local_mat = dynamic_cast<LLLocalGLTFMaterial*>(mat);
+        item_permissions.set(item_out->getPermissions());
+        for (PermissionBit op : ops)
+        {
+            if (!gAgent.allowOperation(op, item_permissions, GP_OBJECT_MANIPULATE))
+            {
+                return false;
+            }
+        }
+        // Update flags for new owner
+        if (!item_permissions.setOwnerAndGroup(LLUUID::null, gAgent.getID(), LLUUID::null, true))
+        {
+            llassert(false);
+            return false;
+        }
+    }
+    else
+    {
+        item_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+    }
 
-        if (local_mat)
+    // Use root object for permissions checking
+    LLViewerObject* root_object = selected_object->getRootEdit();
+    LLPermissions* object_permissions_p = LLSelectMgr::getInstance()->findObjectPermissions(root_object);
+    LLPermissions object_permissions;
+    if (object_permissions_p)
+    {
+        object_permissions.set(*object_permissions_p);
+        for (PermissionBit op : ops)
         {
-            local_material = local_mat;
+            if (!gAgent.allowOperation(op, object_permissions, GP_OBJECT_MANIPULATE))
+            {
+                return false;
+            }
         }
-        render_material = tep->getGLTFRenderMaterial();
+        // Update flags for new owner
+        if (!object_permissions.setOwnerAndGroup(LLUUID::null, gAgent.getID(), LLUUID::null, true))
+        {
+            llassert(false);
+            return false;
+        }
+    }
+    else
+    {
+        object_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+    }
+
+    LLPermissions floater_perm;
+    floater_perm.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+    floater_perm.setMaskEveryone(LLFloaterPerms::getEveryonePerms("Materials"));
+    floater_perm.setMaskGroup(LLFloaterPerms::getGroupPerms("Materials"));
+    floater_perm.setMaskNext(LLFloaterPerms::getNextOwnerPerms("Materials"));
+
+    // *NOTE: A close inspection of LLPermissions::accumulate shows that
+    // conflicting UUIDs will be unset. This is acceptable behavior for now.
+    // The server will populate creator info based on the item creation method
+    // used.
+    // *NOTE: As far as I'm aware, there is currently no good way to preserve
+    // creation history when there's no material item present. In that case,
+    // the agent who saved the material will be considered the creator.
+    // -Cosmic,2023-08-07
+    if (item_out)
+    {
+        permissions_out.set(item_permissions);
     }
     else
     {
-        // Find an applicable material.
-        // Do this before showing message, because
-        // message is going to drop selection.
-        LLSelectedTEGetMatData func(false);
-        LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func, true /*first applicable*/);
-        local_material = func.mLocalMaterial;
-        render_material = func.mMaterial;
+        permissions_out.set(object_permissions);
     }
+    permissions_out.accumulate(floater_perm);
+
+    return true;
+}
+
+bool LLMaterialEditor::canModifyObjectsMaterial()
+{
+    LLSelectedTEGetMatData func(true);
+    LLPermissions permissions;
+    LLViewerInventoryItem* item_out;
+    return can_use_objects_material(func, std::vector({PERM_MODIFY}), permissions, item_out);
+}
+
+bool LLMaterialEditor::canSaveObjectsMaterial()
+{
+    LLSelectedTEGetMatData func(true);
+    LLPermissions permissions;
+    LLViewerInventoryItem* item_out;
+    return can_use_objects_material(func, std::vector({PERM_COPY, PERM_MODIFY}), permissions, item_out);
+}
 
-    saveMaterialAs(render_material, local_material);
+void LLMaterialEditor::saveObjectsMaterialAs()
+{
+    LLSelectedTEGetMatData func(true);
+    LLPermissions permissions;
+    LLViewerInventoryItem* item = nullptr;
+    bool allowed = can_use_objects_material(func, std::vector({PERM_COPY, PERM_MODIFY}), permissions, item);
+    if (!allowed)
+    {
+        LL_WARNS("MaterialEditor") << "Failed to save GLTF material from object" << LL_ENDL;
+        return;
+    }
+    const LLUUID item_id = item ? item->getUUID() : LLUUID::null;
+    saveObjectsMaterialAs(func.mMaterial, func.mLocalMaterial, permissions, func.mObjectId, item_id);
 }
 
-void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, const LLLocalGLTFMaterial *local_material)
+
+void LLMaterialEditor::saveObjectsMaterialAs(const LLGLTFMaterial* render_material, const LLLocalGLTFMaterial *local_material, const LLPermissions& permissions, const LLUUID& object_id, const LLUUID& item_id)
 {
     if (local_material)
     {
@@ -1916,26 +2039,98 @@ void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, con
     LLSD args;
     args["DESC"] = LLTrans::getString("New Material");
 
-    LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2));
+    if (local_material)
+    {
+        LLPermissions local_permissions;
+        local_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+        LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2, local_permissions));
+    }
+    else
+    {
+        if (item_id.notNull())
+        {
+            // Copy existing item from object inventory, and create new composite asset on top of it
+            LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onCopyObjectsMaterialAsMsgCallback, _1, _2, permissions, object_id, item_id));
+        }
+        else
+        {
+            LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2, permissions));
+        }
+    }
 }
 
-void LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response)
+// static
+void LLMaterialEditor::onCopyObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions, const LLUUID& object_id, const LLUUID& item_id)
 {
     S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
-    if (0 == option)
+    if (0 != option)
+    {
+        return;
+    }
+
+    LLSD asset;
+    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
+    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
+    // This is the string serialized from LLGLTFMaterial::asJSON
+    asset["data"] = notification["payload"]["data"];
+
+    std::ostringstream str;
+    LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
+
+    LLViewerObject* object = gObjectList.findObject(object_id);
+    if (!object)
+    {
+        return;
+    }
+    const LLInventoryItem* item = object->getInventoryItem(item_id);
+    if (!item)
     {
-        LLSD asset;
-        asset["version"] = LLGLTFMaterial::ASSET_VERSION;
-        asset["type"] = LLGLTFMaterial::ASSET_TYPE;
-        // This is the string serialized from LLGLTFMaterial::asJSON
-        asset["data"] = notification["payload"]["data"];
+        return;
+    }
 
-        std::ostringstream str;
-        LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
+    std::string new_name = response["message"].asString();
+    LLInventoryObject::correctInventoryName(new_name);
+    if (new_name.empty())
+    {
+        return;
+    }
 
-        std::string new_name = response["message"].asString();
-        createInventoryItem(str.str(), new_name, std::string());
+    const LLUUID destination_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MATERIAL);
+
+    LLPointer<LLInventoryCallback> cb = new LLObjectsMaterialItemCallback(permissions, str.str(), new_name);
+    // NOTE: This should be an item copy. Saving a material to an inventory should be disabled when the associated material is no-copy.
+    move_or_copy_inventory_from_object(destination_id,
+                                       object_id,
+                                       item_id,
+                                       cb);
+}
+
+// static
+void LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions)
+{
+    S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
+    if (0 != option)
+    {
+        return;
     }
+
+    LLSD asset;
+    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
+    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
+    // This is the string serialized from LLGLTFMaterial::asJSON
+    asset["data"] = notification["payload"]["data"];
+
+    std::ostringstream str;
+    LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
+
+    std::string new_name = response["message"].asString();
+    LLInventoryObject::correctInventoryName(new_name);
+    if (new_name.empty())
+    {
+        return;
+    }
+
+    createInventoryItem(str.str(), new_name, std::string(), permissions);
 }
 
 const void upload_bulk(const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter type);
@@ -2708,7 +2903,10 @@ bool LLMaterialEditor::setFromSelection()
     if (func.mMaterial.notNull())
     {
         setFromGLTFMaterial(func.mMaterial);
-        setEnableEditing(true);
+        LLViewerObject* selected_object = func.mObject;
+        const LLViewerInventoryItem* item = selected_object->getInventoryItemByAsset(func.mMaterialId);
+        const bool allow_modify = !item || canModify(selected_object, item);
+        setEnableEditing(allow_modify);
     }
     else
     {
diff --git a/indra/newview/llmaterialeditor.h b/indra/newview/llmaterialeditor.h
index 6f674a41708a22f8ae7b03326cc0603fcf6c3f4b..6b2f49e2fc6697cdce086e977049ce5671a0eeae 100644
--- a/indra/newview/llmaterialeditor.h
+++ b/indra/newview/llmaterialeditor.h
@@ -38,6 +38,7 @@ class LLGLTFMaterial;
 class LLLocalGLTFMaterial;
 class LLTextureCtrl;
 class LLTextBox;
+class LLViewerInventoryItem;
 
 namespace tinygltf
 {
@@ -112,9 +113,11 @@ class LLMaterialEditor : public LLPreview, public LLVOInventoryListener
     static void updateLive(const LLUUID &object_id, S32 te);
     static void loadLive();
 
+    static bool canModifyObjectsMaterial();
+    static bool canSaveObjectsMaterial();
     static void saveObjectsMaterialAs();
-    static void savePickedMaterialAs();
-    static void onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response);
+    static void onCopyObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions, const LLUUID& object_id, const LLUUID& item_id);
+    static void onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions);
 
     static void onLoadComplete(const LLUUID& asset_uuid, LLAssetType::EType type, void* user_data, S32 status, LLExtStat ext_status);
 
@@ -229,10 +232,10 @@ class LLMaterialEditor : public LLPreview, public LLVOInventoryListener
     static bool capabilitiesAvailable();
 
 private:
-    static void saveMaterialAs(const LLGLTFMaterial *render_material, const LLLocalGLTFMaterial *local_material);
+    static void saveObjectsMaterialAs(const LLGLTFMaterial *render_material, const LLLocalGLTFMaterial *local_material, const LLPermissions& permissions, const LLUUID& object_id /* = LLUUID::null */, const LLUUID& item /* = LLUUID::null */);
 
     static bool updateInventoryItem(const std::string &buffer, const LLUUID &item_id, const LLUUID &task_id);
-    static void createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc);
+    static void createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc, const LLPermissions& permissions);
 
     void setFromGLTFMaterial(LLGLTFMaterial* mat);
     bool setFromSelection();
diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp
index 83a330af37f2e3b84f29b6e36a8ddcf88cfac9e6..b502fa3546d9a73935472525abc6a3ca6c153f61 100644
--- a/indra/newview/llpanelface.cpp
+++ b/indra/newview/llpanelface.cpp
@@ -51,6 +51,7 @@
 #include "llinventorymodelbackgroundfetch.h"
 #include "llfloatermediasettings.h"
 #include "llfloaterreg.h"
+#include "llfloatertools.h"
 #include "lllineeditor.h"
 #include "llmaterialmgr.h"
 #include "llmaterialeditor.h"
@@ -77,6 +78,7 @@
 #include "llviewerregion.h"
 #include "llviewerstats.h"
 #include "llvovolume.h"
+#include "llvoinventorylistener.h"
 #include "lluictrlfactory.h"
 #include "llpluginclassmedia.h"
 #include "llviewertexturelist.h"// Update sel manager as to which channel we're editing so it can reflect the correct overlay UI
@@ -490,6 +492,15 @@ LLPanelFace::~LLPanelFace()
     unloadMedia();
 }
 
+void LLPanelFace::onVisibilityChange(BOOL new_visibility)
+{
+    if (new_visibility)
+    {
+        gAgent.showLatestFeatureNotification("gltf");
+    }
+    LLPanel::onVisibilityChange(new_visibility);
+}
+
 void LLPanelFace::draw()
 {
     updateCopyTexButton();
@@ -991,7 +1002,8 @@ void LLPanelFace::getState()
 
 void LLPanelFace::updateUI(bool force_set_values /*false*/)
 { //set state of UI to match state of texture entry(ies)  (calls setEnabled, setValue, etc, but NOT setVisible)
-	LLViewerObject* objectp = LLSelectMgr::getInstance()->getSelection()->getFirstObject();
+    LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstNode();
+	LLViewerObject* objectp = node ? node->getObject() : NULL;
 
 	if (objectp
 		&& objectp->getPCode() == LL_PCODE_VOLUME
@@ -1022,6 +1034,62 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
             }
         }
 
+        // *NOTE: The "identical" variable is currently only used to decide if
+        // the texgen control should be tentative - this is not used by GLTF
+        // materials. -Cosmic;2022-11-09
+        bool identical         = true;  // true because it is anded below
+        bool identical_diffuse = false;
+        bool identical_norm    = false;
+        bool identical_spec    = false;
+
+        LLTextureCtrl *texture_ctrl      = getChild<LLTextureCtrl>("texture control");
+        LLTextureCtrl *shinytexture_ctrl = getChild<LLTextureCtrl>("shinytexture control");
+        LLTextureCtrl *bumpytexture_ctrl = getChild<LLTextureCtrl>("bumpytexture control");
+
+        LLUUID id;
+        LLUUID normmap_id;
+        LLUUID specmap_id;
+
+        LLSelectedTE::getTexId(id, identical_diffuse);
+        LLSelectedTEMaterial::getNormalID(normmap_id, identical_norm);
+        LLSelectedTEMaterial::getSpecularID(specmap_id, identical_spec);
+
+        static S32 selected_te = -1;
+        if ((LLToolFace::getInstance() == LLToolMgr::getInstance()->getCurrentTool()) && 
+            !LLSelectMgr::getInstance()->getSelection()->isMultipleTESelected()) 
+        {
+            S32 new_selection = -1; // Don't use getLastSelectedTE, it could have been deselected
+            S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces());
+            for (S32 te = 0; te < num_tes; ++te)
+            {
+                if (node->isTESelected(te))
+                {
+                    new_selection = te;
+                    break;
+                }
+            }
+
+            if (new_selection != selected_te)
+            {
+                bool te_has_media = objectp->getTE(new_selection) && objectp->getTE(new_selection)->hasMedia();
+                bool te_has_pbr = objectp->getRenderMaterialID(new_selection).notNull();
+
+                if (te_has_pbr && !((mComboMatMedia->getCurrentIndex() == MATMEDIA_MEDIA) && te_has_media))
+                {
+                    mComboMatMedia->selectNthItem(MATMEDIA_PBR);
+                }
+                else if (te_has_media) 
+                {
+                    mComboMatMedia->selectNthItem(MATMEDIA_MEDIA);
+                }
+                else if (id.notNull() || normmap_id.notNull() || specmap_id.notNull()) 
+                {
+                    mComboMatMedia->selectNthItem(MATMEDIA_MATERIAL);
+                }
+                selected_te = new_selection;
+            }
+        }
+
         mComboMatMedia->setEnabled(editable);
 
         LLRadioGroup* radio_mat_type = getChild<LLRadioGroup>("radio_material_type");
@@ -1043,24 +1111,8 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
 		getChildView("checkbox_sync_settings")->setEnabled(editable);
 		childSetValue("checkbox_sync_settings", gSavedSettings.getBOOL("SyncMaterialSettings"));
 
-		updateVisibility();
+		updateVisibility(objectp);
 
-        // *NOTE: The "identical" variable is currently only used to decide if
-        // the texgen control should be tentative - this is not used by GLTF
-        // materials. -Cosmic;2022-11-09
-		bool identical			= true;	// true because it is anded below
-        bool identical_diffuse	= false;
-        bool identical_norm		= false;
-        bool identical_spec		= false;
-
-		LLTextureCtrl*	texture_ctrl = getChild<LLTextureCtrl>("texture control");
-		LLTextureCtrl*	shinytexture_ctrl = getChild<LLTextureCtrl>("shinytexture control");
-		LLTextureCtrl*	bumpytexture_ctrl = getChild<LLTextureCtrl>("bumpytexture control");
-		
-		LLUUID id;
-		LLUUID normmap_id;
-		LLUUID specmap_id;
-		
 		// Color swatch
 		{
 			getChildView("color label")->setEnabled(editable);
@@ -1090,9 +1142,6 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
 		getChild<LLUICtrl>("ColorTrans")->setValue(editable ? transparency : 0);
 		getChildView("ColorTrans")->setEnabled(editable && has_material);
 
-		// Specular map
-		LLSelectedTEMaterial::getSpecularID(specmap_id, identical_spec);
-		
 		U8 shiny = 0;
 		bool identical_shiny = false;
 
@@ -1158,11 +1207,6 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
 
 		// Texture
 		{
-			LLSelectedTE::getTexId(id,identical_diffuse);
-
-			// Normal map
-			LLSelectedTEMaterial::getNormalID(normmap_id, identical_norm);
-
 			mIsAlpha = FALSE;
 			LLGLenum image_format = GL_RGB;
 			bool identical_image_format = false;
@@ -1801,29 +1845,82 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
 	}
 }
 
+// One-off listener that updates the build floater UI when the prim inventory updates
+class PBRPickerItemListener : public LLVOInventoryListener
+{
+protected:
+    LLViewerObject* mObjectp;
+    bool mChangePending = true;
+public:
+
+    PBRPickerItemListener(LLViewerObject* object)
+    : mObjectp(object)
+    {
+        registerVOInventoryListener(mObjectp, nullptr);
+    }
+
+    const bool isListeningFor(const LLViewerObject* objectp) const
+    {
+        return mChangePending && (objectp == mObjectp);
+    }
+
+    void inventoryChanged(LLViewerObject* object,
+        LLInventoryObject::object_list_t* inventory,
+        S32 serial_num,
+        void* user_data) override
+    {
+        if (gFloaterTools)
+        {
+            gFloaterTools->dirty();
+        }
+        removeVOInventoryListener();
+        mChangePending = false;
+    }
+
+    ~PBRPickerItemListener()
+    {
+        removeVOInventoryListener();
+        mChangePending = false;
+    }
+};
+
 void LLPanelFace::updateUIGLTF(LLViewerObject* objectp, bool& has_pbr_material, bool& has_faces_without_pbr, bool force_set_values)
 {
     has_pbr_material = false;
 
-    const bool editable = objectp->permModify() && !objectp->isPermanentEnforced();
     bool has_pbr_capabilities = LLMaterialEditor::capabilitiesAvailable();
+    bool identical_pbr = true;
+    const bool settable = has_pbr_capabilities && objectp->permModify() && !objectp->isPermanentEnforced();
+    const bool editable = LLMaterialEditor::canModifyObjectsMaterial();
+    const bool saveable = LLMaterialEditor::canSaveObjectsMaterial();
 
     // pbr material
     LLTextureCtrl* pbr_ctrl = findChild<LLTextureCtrl>("pbr_control");
     if (pbr_ctrl)
     {
         LLUUID pbr_id;
-        bool identical_pbr;
         LLSelectedTE::getPbrMaterialId(pbr_id, identical_pbr, has_pbr_material, has_faces_without_pbr);
 
         pbr_ctrl->setTentative(identical_pbr ? FALSE : TRUE);
-        pbr_ctrl->setEnabled(editable && has_pbr_capabilities);
+        pbr_ctrl->setEnabled(settable);
         pbr_ctrl->setImageAssetID(pbr_id);
     }
 
-    getChildView("pbr_from_inventory")->setEnabled(editable && has_pbr_capabilities);
-    getChildView("edit_selected_pbr")->setEnabled(editable && has_pbr_material && !has_faces_without_pbr && has_pbr_capabilities);
-    getChildView("save_selected_pbr")->setEnabled(objectp->permCopy() && has_pbr_material && !has_faces_without_pbr && has_pbr_capabilities);
+    getChildView("pbr_from_inventory")->setEnabled(settable);
+    getChildView("edit_selected_pbr")->setEnabled(editable && !has_faces_without_pbr);
+    getChildView("save_selected_pbr")->setEnabled(saveable && identical_pbr);
+    if (objectp->isInventoryPending())
+    {
+        // Reuse the same listener when possible
+        if (!mInventoryListener || !mInventoryListener->isListeningFor(objectp))
+        {
+            mInventoryListener = std::make_unique<PBRPickerItemListener>(objectp);
+        }
+    }
+    else
+    {
+        mInventoryListener = nullptr;
+    }
 
     const bool show_pbr = mComboMatMedia->getCurrentIndex() == MATMEDIA_PBR && mComboMatMedia->getEnabled();
     if (show_pbr)
@@ -1848,9 +1945,10 @@ void LLPanelFace::updateUIGLTF(LLViewerObject* objectp, bool& has_pbr_material,
     }
 }
 
-void LLPanelFace::updateVisibilityGLTF()
+void LLPanelFace::updateVisibilityGLTF(LLViewerObject* objectp /*= nullptr */)
 {
     const bool show_pbr = mComboMatMedia->getCurrentIndex() == MATMEDIA_PBR && mComboMatMedia->getEnabled();
+    const bool inventory_pending = objectp && objectp->isInventoryPending();
 
     LLRadioGroup* radio_pbr_type = findChild<LLRadioGroup>("radio_pbr_type");
     radio_pbr_type->setVisible(show_pbr);
@@ -1861,8 +1959,9 @@ void LLPanelFace::updateVisibilityGLTF()
     getChildView("pbr_control")->setVisible(show_pbr_render_material_id);
 
     getChildView("pbr_from_inventory")->setVisible(show_pbr_render_material_id);
-    getChildView("edit_selected_pbr")->setVisible(show_pbr_render_material_id);
-    getChildView("save_selected_pbr")->setVisible(show_pbr_render_material_id);
+    getChildView("edit_selected_pbr")->setVisible(show_pbr_render_material_id && !inventory_pending);
+    getChildView("save_selected_pbr")->setVisible(show_pbr_render_material_id && !inventory_pending);
+    getChildView("material_permissions_loading_label")->setVisible(show_pbr_render_material_id && inventory_pending);
 
     getChildView("gltfTextureScaleU")->setVisible(show_pbr);
     getChildView("gltfTextureScaleV")->setVisible(show_pbr);
@@ -2703,7 +2802,7 @@ void LLPanelFace::onCommitMaterialsMedia(LLUICtrl* ctrl, void* userdata)
 	self->refreshMedia();
 }
 
-void LLPanelFace::updateVisibility()
+void LLPanelFace::updateVisibility(LLViewerObject* objectp /* = nullptr */)
 {
     LLRadioGroup* radio_mat_type = findChild<LLRadioGroup>("radio_material_type");
     LLRadioGroup* radio_pbr_type = findChild<LLRadioGroup>("radio_pbr_type");
@@ -2794,7 +2893,7 @@ void LLPanelFace::updateVisibility()
     getChild<LLSpinCtrl>("rptctrl")->setVisible(show_material || show_media);
 
     // PBR controls
-    updateVisibilityGLTF();
+    updateVisibilityGLTF(objectp);
 }
 
 // static
@@ -3060,11 +3159,7 @@ void LLPanelFace::onSelectPbr(const LLSD& data)
         {
             id = pbr_ctrl->getImageAssetID();
         }
-        if (LLSelectMgr::getInstance()->selectionSetGLTFMaterial(id))
-        {
-            LLSelectedTEMaterial::setMaterialID(this, id);
-        }
-        else
+        if (!LLSelectMgr::getInstance()->selectionSetGLTFMaterial(id))
         {
             refresh();
         }
@@ -4676,13 +4771,6 @@ void LLPanelFace::updateGLTFTextureTransform(float value, U32 pbr_type, std::fun
             edit(&new_transform);
         }
     });
-
-    LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstNode();
-    if (node)
-    {
-        LLViewerObject* object = node->getObject();
-        sMaterialOverrideSelection.setObjectUpdatePending(object->getID(), node->getLastSelectedTE());
-    }
 }
 
 void LLPanelFace::setMaterialOverridesFromSelection()
@@ -4795,17 +4883,22 @@ bool LLPanelFace::Selection::update()
     return changed;
 }
 
-void LLPanelFace::Selection::setObjectUpdatePending(const LLUUID &object_id, S32 side)
-{
-    mPendingObjectID = object_id;
-    mPendingSide = side;
-}
-
 void LLPanelFace::Selection::onSelectedObjectUpdated(const LLUUID& object_id, S32 side)
 {
-    if (object_id == mSelectedObjectID && side == mSelectedSide)
+    if (object_id == mSelectedObjectID)
     {
-        mChanged = true;
+        if (side == mLastSelectedSide)
+        {
+            mChanged = true;
+        }
+        else if (mLastSelectedSide == -1) // if last selected face was deselected
+        {
+            LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstNode();
+            if (node && node->isTESelected(side))
+            {
+                mChanged = true;
+            }
+        }
     }
 }
 
@@ -4818,8 +4911,9 @@ bool LLPanelFace::Selection::compareSelection()
     mNeedsSelectionCheck = false;
 
     const S32 old_object_count = mSelectedObjectCount;
+    const S32 old_te_count = mSelectedTECount;
     const LLUUID old_object_id = mSelectedObjectID;
-    const S32 old_side = mSelectedSide;
+    const S32 old_side = mLastSelectedSide;
 
     LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection();
     LLSelectNode* node = selection->getFirstNode();
@@ -4827,17 +4921,23 @@ bool LLPanelFace::Selection::compareSelection()
     {
         LLViewerObject* object = node->getObject();
         mSelectedObjectCount = selection->getObjectCount();
+        mSelectedTECount = selection->getTECount();
         mSelectedObjectID = object->getID();
-        mSelectedSide = node->getLastSelectedTE();
+        mLastSelectedSide = node->getLastSelectedTE();
     }
     else
     {
         mSelectedObjectCount = 0;
+        mSelectedTECount = 0;
         mSelectedObjectID = LLUUID::null;
-        mSelectedSide = -1;
+        mLastSelectedSide = -1;
     }
 
-    const bool selection_changed = old_object_count != mSelectedObjectCount || old_object_id != mSelectedObjectID || old_side != mSelectedSide;
+    const bool selection_changed =
+        old_object_count != mSelectedObjectCount
+        || old_te_count != mSelectedTECount
+        || old_object_id != mSelectedObjectID
+        || old_side != mLastSelectedSide;
     mChanged = mChanged || selection_changed;
     return selection_changed;
 }
@@ -4956,23 +5056,24 @@ void LLPanelFace::onPbrSelectionChanged(LLInventoryItem* itemp)
         LLSaleInfo sale_info;
         LLSelectMgr::instance().selectGetSaleInfo(sale_info);
 
-        bool can_copy = itemp->getPermissions().allowCopyBy(gAgentID); // do we have perm to copy this texture?
-        bool can_transfer = itemp->getPermissions().allowOperationBy(PERM_TRANSFER, gAgentID); // do we have perm to transfer this texture?
-        bool is_object_owner = gAgentID == obj_owner_id; // does object for which we are going to apply texture belong to the agent?
-        bool not_for_sale = !sale_info.isForSale(); // is object for which we are going to apply texture not for sale?
+        bool can_copy = itemp->getPermissions().allowCopyBy(gAgentID); // do we have perm to copy this material?
+        bool can_transfer = itemp->getPermissions().allowOperationBy(PERM_TRANSFER, gAgentID); // do we have perm to transfer this material?
+        bool can_modify = itemp->getPermissions().allowOperationBy(PERM_MODIFY, gAgentID); // do we have perm to transfer this material?
+        bool is_object_owner = gAgentID == obj_owner_id; // does object for which we are going to apply material belong to the agent?
+        bool not_for_sale = !sale_info.isForSale(); // is object for which we are going to apply material not for sale?
 
-        if (can_copy && can_transfer)
+        if (can_copy && can_transfer && can_modify)
         {
             pbr_ctrl->setCanApply(true, true);
             return;
         }
 
-        // if texture has (no-transfer) attribute it can be applied only for object which we own and is not for sale
+        // if material has (no-transfer) attribute it can be applied only for object which we own and is not for sale
         pbr_ctrl->setCanApply(false, can_transfer ? true : is_object_owner && not_for_sale);
 
         if (gSavedSettings.getBOOL("TextureLivePreview"))
         {
-            LLNotificationsUtil::add("LivePreviewUnavailable");
+            LLNotificationsUtil::add("LivePreviewUnavailablePBR");
         }
     }
 }
@@ -5058,14 +5159,18 @@ void LLPanelFace::LLSelectedTE::getTexId(LLUUID& id, bool& identical)
 
 void LLPanelFace::LLSelectedTE::getPbrMaterialId(LLUUID& id, bool& identical, bool& has_faces_with_pbr, bool& has_faces_without_pbr)
 {
-    struct LLSelectedTEGetmatId : public LLSelectedTEGetFunctor<LLUUID>
+    struct LLSelectedTEGetmatId : public LLSelectedTEFunctor
     {
         LLSelectedTEGetmatId()
             : mHasFacesWithoutPBR(false)
             , mHasFacesWithPBR(false)
+            , mIdenticalId(true)
+            , mIdenticalOverride(true)
+            , mInitialized(false)
+            , mMaterialOverride(LLGLTFMaterial::sDefault)
         {
         }
-        LLUUID get(LLViewerObject* object, S32 te_index)
+        bool apply(LLViewerObject* object, S32 te_index) override
         {
             LLUUID pbr_id = object->getRenderMaterialID(te_index);
             if (pbr_id.isNull())
@@ -5076,12 +5181,49 @@ void LLPanelFace::LLSelectedTE::getPbrMaterialId(LLUUID& id, bool& identical, bo
             {
                 mHasFacesWithPBR = true;
             }
-            return pbr_id;
+            if (mInitialized)
+            {
+                if (mPBRId != pbr_id)
+                {
+                    mIdenticalId = false;
+                }
+                
+                LLGLTFMaterial* te_override = object->getTE(te_index)->getGLTFMaterialOverride();
+                if (te_override)
+                {
+                    LLGLTFMaterial override = *te_override;
+                    override.sanitizeAssetMaterial();
+                    mIdenticalOverride &= (override == mMaterialOverride);
+                }
+                else
+                {
+                    mIdenticalOverride &= (mMaterialOverride == LLGLTFMaterial::sDefault);
+                }
+            }
+            else
+            {
+                mInitialized = true;
+                mPBRId = pbr_id;
+                LLGLTFMaterial* override = object->getTE(te_index)->getGLTFMaterialOverride();
+                if (override)
+                {
+                    mMaterialOverride = *override;
+                    mMaterialOverride.sanitizeAssetMaterial();
+                }
+            }
+            return true;
         }
         bool mHasFacesWithoutPBR;
         bool mHasFacesWithPBR;
+        bool mIdenticalId;
+        bool mIdenticalOverride;
+        bool mInitialized;
+        LLGLTFMaterial mMaterialOverride;
+        LLUUID mPBRId;
     } func;
-    identical = LLSelectMgr::getInstance()->getSelection()->getSelectedTEValue(&func, id);
+    LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func);
+    id = func.mPBRId;
+    identical = func.mIdenticalId && func.mIdenticalOverride;
     has_faces_with_pbr = func.mHasFacesWithPBR;
     has_faces_without_pbr = func.mHasFacesWithoutPBR;
 }
diff --git a/indra/newview/llpanelface.h b/indra/newview/llpanelface.h
index 7d567c7ce830a5e82be7b862719debb018dccb40..d36662c11bb46d969f6daa524342416d17c46881 100644
--- a/indra/newview/llpanelface.h
+++ b/indra/newview/llpanelface.h
@@ -35,6 +35,8 @@
 #include "lltextureentry.h"
 #include "llselectmgr.h"
 
+#include <memory>
+
 class LLButton;
 class LLCheckBoxCtrl;
 class LLColorSwatchCtrl;
@@ -51,6 +53,8 @@ class LLMaterialID;
 class LLMediaCtrl;
 class LLMenuButton;
 
+class PBRPickerItemListener;
+
 // Represents an edit for use in replicating the op across one or more materials in the selection set.
 //
 // The apply function optionally performs the edit which it implements
@@ -105,6 +109,7 @@ class LLPanelFace : public LLPanel
 
     static void onMaterialOverrideReceived(const LLUUID& object_id, S32 side);
 
+    /*virtual*/ void onVisibilityChange(BOOL new_visibility);
     /*virtual*/ void draw();
 
 	LLMaterialPtr createDefaultMaterial(LLMaterialPtr current_material)
@@ -292,7 +297,7 @@ class LLPanelFace : public LLPanel
 	//
 	// Do NOT call updateUI from within this function.
 	//
-	void updateVisibility();
+	void updateVisibility(LLViewerObject* objectp = nullptr);
 
 	// Hey look everyone, a type-safe alternative to copy and paste! :)
 	//
@@ -453,7 +458,7 @@ class LLPanelFace : public LLPanel
     void onPbrSelectionChanged(LLInventoryItem* itemp);
 
     void updateUIGLTF(LLViewerObject* objectp, bool& has_pbr_material, bool& has_faces_without_pbr, bool force_set_values);
-    void updateVisibilityGLTF();
+    void updateVisibilityGLTF(LLViewerObject* objectp = nullptr);
 
     void updateSelectedGLTFMaterials(std::function<void(LLGLTFMaterial*)> func);
     void updateGLTFTextureTransform(float value, U32 pbr_type, std::function<void(LLGLTFMaterial::TextureTransform*)> edit);
@@ -482,7 +487,6 @@ class LLPanelFace : public LLPanel
         // Prevents update() returning true until the provided object is
         // updated. Necessary to prevent controls updating when the mouse is
         // held down.
-        void setObjectUpdatePending(const LLUUID &object_id, S32 side);
         void setDirty() { mChanged = true; };
 
         // Callbacks
@@ -497,15 +501,15 @@ class LLPanelFace : public LLPanel
         boost::signals2::scoped_connection mSelectConnection;
         bool mNeedsSelectionCheck = true;
         S32 mSelectedObjectCount = 0;
+        S32 mSelectedTECount = 0;
         LLUUID mSelectedObjectID;
-        S32 mSelectedSide = -1;
-
-        LLUUID mPendingObjectID;
-        S32 mPendingSide = -1;
+        S32 mLastSelectedSide = -1;
     };
 
     static Selection sMaterialOverrideSelection;
 
+    std::unique_ptr<PBRPickerItemListener> mInventoryListener;
+
 public:
 	#if defined(DEF_GET_MAT_STATE)
 		#undef DEF_GET_MAT_STATE
@@ -586,7 +590,6 @@ class LLPanelFace : public LLPanel
 		DEF_EDIT_MAT_STATE(LLUUID,const LLUUID&,setNormalID);
 		DEF_EDIT_MAT_STATE(LLUUID,const LLUUID&,setSpecularID);
 		DEF_EDIT_MAT_STATE(LLColor4U,	const LLColor4U&,setSpecularLightColor);
-		DEF_EDIT_MAT_STATE(LLUUID, const LLUUID&, setMaterialID);
 	};
 
 	class LLSelectedTE
diff --git a/indra/newview/llpreview.cpp b/indra/newview/llpreview.cpp
index b9b2279e771dcddfd320308e8e738cdfb62d3a26..0117db86e82f66b86d712ea074352b208ca58cdf 100644
--- a/indra/newview/llpreview.cpp
+++ b/indra/newview/llpreview.cpp
@@ -241,14 +241,22 @@ void LLPreview::refreshFromItem()
 // static
 BOOL LLPreview::canModify(const LLUUID taskUUID, const LLInventoryItem* item)
 {
+    const LLViewerObject* object = nullptr;
 	if (taskUUID.notNull())
 	{
-		LLViewerObject* object = gObjectList.findObject(taskUUID);
-		if(object && !object->permModify())
-		{
-			// No permission to edit in-world inventory
-			return FALSE;
-		}
+		object = gObjectList.findObject(taskUUID);
+	}
+
+	return canModify(object, item);
+}
+
+// static
+BOOL LLPreview::canModify(const LLViewerObject* object, const LLInventoryItem* item)
+{
+	if (object && !object->permModify())
+	{
+        // No permission to edit in-world inventory
+        return FALSE;
 	}
 
 	return item && gAgent.allowOperation(PERM_MODIFY, item->getPermissions(), GP_OBJECT_MANIPULATE);
diff --git a/indra/newview/llpreview.h b/indra/newview/llpreview.h
index ab60f4c008653fa5d06b13e4b509e2dfd2a1960e..3688ee019268de83d179d3bf45e9d6a68abd7ae4 100644
--- a/indra/newview/llpreview.h
+++ b/indra/newview/llpreview.h
@@ -36,6 +36,7 @@
 #include <map>
 
 class LLInventoryItem;
+class LLViewerObject;
 class LLLineEditor;
 class LLRadioGroup;
 class LLPreview;
@@ -109,6 +110,7 @@ class LLPreview : public LLFloater, LLInventoryObserver
 	// We can't modify Item or description in preview if either in-world Object
 	// or Item  itself is unmodifiable
 	static BOOL canModify(const LLUUID taskUUID, const LLInventoryItem* item);
+	static BOOL canModify(const LLViewerObject* object, const LLInventoryItem* item);
 
 protected:
 	virtual void onCommit();
diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp
index 5c1a339570004177ae43bbde6920a1a54e3c6d55..6692d124d8cc2cac7dbb563e3a4888bed16067bd 100644
--- a/indra/newview/llselectmgr.cpp
+++ b/indra/newview/llselectmgr.cpp
@@ -1773,15 +1773,17 @@ void LLObjectSelection::applyNoCopyTextureToTEs(LLViewerInventoryItem* item)
 	}
 }
 
-void LLObjectSelection::applyNoCopyPbrMaterialToTEs(LLViewerInventoryItem* item)
+bool LLObjectSelection::applyRestrictedPbrMaterialToTEs(LLViewerInventoryItem* item)
 {
     if (!item)
     {
-        return;
+        return false;
     }
 
     LLUUID asset_id = item->getAssetUUID();
 
+    bool material_copied_all_faces = true;
+
     for (iterator iter = begin(); iter != end(); ++iter)
     {
         LLSelectNode* node = *iter;
@@ -1797,12 +1799,17 @@ void LLObjectSelection::applyNoCopyPbrMaterialToTEs(LLViewerInventoryItem* item)
         {
             if (node->isTESelected(te))
             {
-                //(no-copy) materials must be moved to the object's inventory only once
+                //(no-copy), (no-modify), and (no-transfer) materials must be moved to the object's inventory only once
                 // without making any copies
                 if (!material_copied && asset_id.notNull())
                 {
-                    LLToolDragAndDrop::handleDropMaterialProtections(object, item, LLToolDragAndDrop::SOURCE_AGENT, LLUUID::null);
-                    material_copied = true;
+                    material_copied = (bool)LLToolDragAndDrop::handleDropMaterialProtections(object, item, LLToolDragAndDrop::SOURCE_AGENT, LLUUID::null);
+                }
+                if (!material_copied)
+                {
+                    // Applying the material is not possible for this object given the current inventory
+					material_copied_all_faces = false;
+                    break;
                 }
 
                 // apply texture for the selected faces
@@ -1814,6 +1821,8 @@ void LLObjectSelection::applyNoCopyPbrMaterialToTEs(LLViewerInventoryItem* item)
     }
 
     LLGLTFMaterialList::flushUpdates();
+
+    return material_copied_all_faces;
 }
 
 
@@ -1924,6 +1933,8 @@ bool LLSelectMgr::selectionSetGLTFMaterial(const LLUUID& mat_id)
     {
         LLViewerInventoryItem* mItem;
         LLUUID mMatId;
+        bool material_copied_any_face = false;
+        bool material_copied_all_faces = true;
         f(LLViewerInventoryItem* item, const LLUUID& id) : mItem(item), mMatId(id) {}
         bool apply(LLViewerObject* objectp, S32 te)
         {
@@ -1950,14 +1961,19 @@ bool LLSelectMgr::selectionSetGLTFMaterial(const LLUUID& mat_id)
         }
     };
 
-    if (item && !item->getPermissions().allowOperationBy(PERM_COPY, gAgent.getID()))
+    bool success = true;
+    if (item &&
+            (!item->getPermissions().allowOperationBy(PERM_COPY, gAgent.getID()) ||
+             !item->getPermissions().allowOperationBy(PERM_TRANSFER, gAgent.getID()) ||
+             !item->getPermissions().allowOperationBy(PERM_MODIFY, gAgent.getID())
+            ))
     {
-        getSelection()->applyNoCopyPbrMaterialToTEs(item);
+        success = success && getSelection()->applyRestrictedPbrMaterialToTEs(item);
     }
     else
     {
         f setfunc(item, mat_id);
-        getSelection()->applyToTEs(&setfunc);
+        success = success && getSelection()->applyToTEs(&setfunc);
     }
 
     struct g : public LLSelectedObjectFunctor
@@ -1986,11 +2002,11 @@ bool LLSelectMgr::selectionSetGLTFMaterial(const LLUUID& mat_id)
             return true;
         }
     } sendfunc(item);
-    getSelection()->applyToObjects(&sendfunc);
+    success = success && getSelection()->applyToObjects(&sendfunc);
 
     LLGLTFMaterialList::flushUpdates();
 
-    return true;
+    return success;
 }
 
 //-----------------------------------------------------------------------------
diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h
index 327134a4875110868870d3af071173053723d44c..f89209b4375af84da350a600567726efee03445a 100644
--- a/indra/newview/llselectmgr.h
+++ b/indra/newview/llselectmgr.h
@@ -378,7 +378,17 @@ class LLObjectSelection : public LLRefCount
 	 * Then this only texture is used for all selected faces.
 	 */
 	void applyNoCopyTextureToTEs(LLViewerInventoryItem* item);
-    void applyNoCopyPbrMaterialToTEs(LLViewerInventoryItem* item);
+    /*
+     * Multi-purpose function for applying PBR materials to the
+     * selected object or faces, any combination of copy/mod/transfer
+     * permission restrictions. This method moves the restricted
+     * material to the object's inventory and doesn't make a copy of the
+     * material for each face. Then this only material is used for
+     * all selected faces.
+     * Returns false if applying the material failed on one or more selected
+     * faces.
+     */
+    bool applyRestrictedPbrMaterialToTEs(LLViewerInventoryItem* item);
 
 	ESelectType getSelectType() const { return mSelectType; }
 
@@ -635,7 +645,7 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton<LLSelectMgr>
 	void selectionSetRestitution(F32 restitution);
 	void selectionSetMaterial(U8 material);
 	bool selectionSetImage(const LLUUID& imageid); // could be item or asset id
-    bool selectionSetGLTFMaterial(const LLUUID& mat_id); // could be item or asset id
+    bool selectionSetGLTFMaterial(const LLUUID& mat_id); // material id only
 	void selectionSetColor(const LLColor4 &color);
 	void selectionSetColorOnly(const LLColor4 &color); // Set only the RGB channels
 	void selectionSetAlphaOnly(const F32 alpha); // Set only the alpha channel
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index e84894b395f44d80fd474a1d29e9830d7913c8a5..8ffab761f4dad3cfda2e7ecc13e9677f1eba2dd6 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -3617,7 +3617,7 @@ bool process_login_success_response()
 		std::string flag = login_flags["ever_logged_in"];
 		if(!flag.empty())
 		{
-			gAgent.setFirstLogin((flag == "N") ? TRUE : FALSE);
+			gAgent.setFirstLogin(flag == "N");
 		}
 
 		/*  Flag is currently ignored by the viewer.
@@ -3708,7 +3708,7 @@ bool process_login_success_response()
 	std::string fake_initial_outfit_name = gSavedSettings.getString("FakeInitialOutfitName");
 	if (!fake_initial_outfit_name.empty())
 	{
-		gAgent.setFirstLogin(TRUE);
+		gAgent.setFirstLogin(true);
 		sInitialOutfit = fake_initial_outfit_name;
 		if (sInitialOutfitGender.empty())
 		{
diff --git a/indra/newview/lltooldraganddrop.cpp b/indra/newview/lltooldraganddrop.cpp
index 1918d1196457ecfa1d22c678f9e45eda898fc801..afcdb26f1a4d50f30c1c6dae7627b4b784a3e76a 100644
--- a/indra/newview/lltooldraganddrop.cpp
+++ b/indra/newview/lltooldraganddrop.cpp
@@ -930,6 +930,8 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 													 LLToolDragAndDrop::ESource source,
 													 const LLUUID& src_id)
 {
+	if (!item) return FALSE;
+
 	// Always succeed if....
 	// material is from the library 
 	// or already in the contents of the object
@@ -948,7 +950,14 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 	{
 		hit_obj->requestInventory();
 		LLSD args;
-		args["ERROR_MESSAGE"] = "Unable to add texture.\nPlease wait a few seconds and try again.";
+        if (LLAssetType::AT_MATERIAL == item->getType())
+        {
+            args["ERROR_MESSAGE"] = "Unable to add material.\nPlease wait a few seconds and try again.";
+        }
+        else
+        {
+            args["ERROR_MESSAGE"] = "Unable to add texture.\nPlease wait a few seconds and try again.";
+        }
 		LLNotificationsUtil::add("ErrorMessage", args);
 		return FALSE;
 	}
@@ -957,11 +966,9 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 		// if the asset is already in the object's inventory 
 		// then it can always be added to a side.
 		// This saves some work if the task's inventory is already loaded
-		// and ensures that the texture item is only added once.
+		// and ensures that the asset item is only added once.
 		return TRUE;
 	}
-
-	if (!item) return FALSE;
 	
 	LLPointer<LLViewerInventoryItem> new_item = new LLViewerInventoryItem(item);
 	if (!item->getPermissions().allowOperationBy(PERM_COPY, gAgent.getID()))
@@ -995,7 +1002,7 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 				return FALSE;
 			}
 		}
-		// Add the texture item to the target object's inventory.
+		// Add the asset item to the target object's inventory.
         if (LLAssetType::AT_TEXTURE == new_item->getType()
             || LLAssetType::AT_MATERIAL == new_item->getType())
         {
@@ -1005,20 +1012,24 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 		{
 			hit_obj->updateInventory(new_item, TASK_INVENTORY_ITEM_KEY, true);
 		}
- 		// TODO: Check to see if adding the item was successful; if not, then
-		// we should return false here.
+		// Force the object to update and refetch its inventory so it has this asset.
+		hit_obj->dirtyInventory();
+		hit_obj->requestInventory();
+		// TODO: Check to see if adding the item was successful; if not, then
+		// we should return false here. This will requre a separate listener
+		// since without listener, we have no way to receive update
 	}
 	else if (!item->getPermissions().allowOperationBy(PERM_TRANSFER,
 													 gAgent.getID()))
 	{
-		// Check that we can add the texture as inventory to the object
+		// Check that we can add the asset as inventory to the object
 		if (willObjectAcceptInventory(hit_obj,item) < ACCEPT_YES_COPY_SINGLE )
 		{
 			return FALSE;
 		}
 		// *FIX: may want to make sure agent can paint hit_obj.
 
-		// Add the texture item to the target object's inventory.
+		// Add the asset item to the target object's inventory.
 		if (LLAssetType::AT_TEXTURE == new_item->getType()
             || LLAssetType::AT_MATERIAL == new_item->getType())
 		{
@@ -1028,7 +1039,27 @@ BOOL LLToolDragAndDrop::handleDropMaterialProtections(LLViewerObject* hit_obj,
 		{
 			hit_obj->updateInventory(new_item, TASK_INVENTORY_ITEM_KEY, true);
 		}
-		// Force the object to update and refetch its inventory so it has this texture.
+		// Force the object to update and refetch its inventory so it has this asset.
+		hit_obj->dirtyInventory();
+		hit_obj->requestInventory();
+		// TODO: Check to see if adding the item was successful; if not, then
+		// we should return false here. This will requre a separate listener
+		// since without listener, we have no way to receive update
+	}
+	else if (LLAssetType::AT_MATERIAL == new_item->getType() &&
+             !item->getPermissions().allowOperationBy(PERM_MODIFY, gAgent.getID()))
+	{
+		// Check that we can add the material as inventory to the object
+		if (willObjectAcceptInventory(hit_obj,item) < ACCEPT_YES_COPY_SINGLE )
+		{
+			return FALSE;
+		}
+		// *FIX: may want to make sure agent can paint hit_obj.
+
+		// Add the material item to the target object's inventory.
+        hit_obj->updateMaterialInventory(new_item, TASK_INVENTORY_ITEM_KEY, true);
+
+		// Force the object to update and refetch its inventory so it has this material.
 		hit_obj->dirtyInventory();
 		hit_obj->requestInventory();
 		// TODO: Check to see if adding the item was successful; if not, then
diff --git a/indra/newview/lltoolpie.cpp b/indra/newview/lltoolpie.cpp
index 84cc3f03c27aedc775eceb4b836b29cb3b3a8516..935d61f7eabf5f64fab7254534438b83a5b8df64 100644
--- a/indra/newview/lltoolpie.cpp
+++ b/indra/newview/lltoolpie.cpp
@@ -183,11 +183,15 @@ BOOL LLToolPie::handleMouseDown(S32 x, S32 y, MASK mask)
 // an item.
 BOOL LLToolPie::handleRightMouseDown(S32 x, S32 y, MASK mask)
 {
+    BOOL pick_reflection_probe = gSavedSettings.getBOOL("SelectReflectionProbes");
+
 	// don't pick transparent so users can't "pay" transparent objects
 	mPick = gViewerWindow->pickImmediate(x, y,
                                          /*BOOL pick_transparent*/ FALSE,
                                          /*BOOL pick_rigged*/ TRUE,
-                                         /*BOOL pick_particle*/ TRUE);
+                                         /*BOOL pick_particle*/ TRUE,
+                                         /*BOOL pick_unselectable*/ TRUE, 
+                                         pick_reflection_probe);
 	mPick.mKeyMask = mask;
 
 	// claim not handled so UI focus stays same
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index ca87ff0092fbc84ef19411d4e8898aeee799f06f..0d78bfd753e27bfa2c4004393e80b1ab13654e0f 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -98,6 +98,7 @@
 #include "llfloatermyscripts.h"
 #include "llfloatermyenvironment.h"
 #include "llfloaternamedesc.h"
+#include "llfloaternewfeaturenotification.h"
 #include "llfloaternotificationsconsole.h"
 #include "llfloaternotificationstabbed.h"
 #include "llfloaterobjectweights.h"
@@ -229,6 +230,7 @@ class LLFloaterOpenHandler : public LLCommandHandler
                 "avatar_picker",
                 "camera",
                 "camera_presets",
+                "change_item_thumbnail"
                 "classified",
                 "add_landmark",
                 "delete_pref_preset",
@@ -247,6 +249,7 @@ class LLFloaterOpenHandler : public LLCommandHandler
                 "message_critical", // Modal!!! Login specific. If this is in use elsewhere, better to create a non modal variant
                 "message_tos", // Modal!!! Login specific.
                 "mute_object_by_name",
+                "new_feature_notification",
                 "publish_classified",
                 "save_pref_preset",
                 "save_camera_preset",
@@ -395,6 +398,7 @@ void LLViewerFloaterReg::registerFloaters()
 	LLFloaterReg::add("moveview", "floater_moveview.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMove>);
 	LLFloaterReg::add("mute_object_by_name", "floater_mute_object.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGetBlockedObjectName>);
 	LLFloaterReg::add("mini_map", "floater_map.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMap>);
+    LLFloaterReg::add("new_feature_notification", "floater_new_feature_notification.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterNewFeatureNotification>);
 
 	LLFloaterReg::add("notifications_console", "floater_notifications_console.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterNotificationConsole>);
 	
diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp
index 518967709d6ee57dcf703569e812d134097139f0..aa1827a8e2d4d5e692613376730360f9554d9482 100644
--- a/indra/newview/llviewerinventory.cpp
+++ b/indra/newview/llviewerinventory.cpp
@@ -1604,6 +1604,67 @@ void copy_inventory_from_notecard(const LLUUID& destination_id,
     }
 }
 
+void move_or_copy_inventory_from_object(const LLUUID& destination_id,
+                                        const LLUUID& object_id,
+                                        const LLUUID& item_id,
+                                        LLPointer<LLInventoryCallback> cb)
+{
+    LLViewerObject* object = gObjectList.findObject(object_id);
+    if (!object)
+    {
+        return;
+    }
+    const LLInventoryItem* item = object->getInventoryItem(item_id);
+    if (!item)
+    {
+        return;
+    }
+
+    class LLItemAddedObserver : public LLInventoryObserver
+    {
+    public:
+        LLItemAddedObserver(const LLUUID& copied_asset_id, LLPointer<LLInventoryCallback> cb)
+        : LLInventoryObserver(),
+          mAssetId(copied_asset_id),
+          mCallback(cb)
+        {
+        }
+
+        void changed(U32 mask) override
+        {
+            if((mask & (LLInventoryObserver::ADD)) == 0)
+            {
+                return;
+            }
+            for (const LLUUID& changed_id : gInventory.getChangedIDs())
+            {
+                LLViewerInventoryItem* changed_item = gInventory.getItem(changed_id);
+                if (changed_item->getAssetUUID() == mAssetId)
+                {
+                    changeComplete(changed_item->getUUID());
+                    return;
+                }
+            }
+        }
+
+    private:
+        void changeComplete(const LLUUID& item_id)
+        {
+			mCallback->fire(item_id);
+            gInventory.removeObserver(this);
+            delete this;
+        }
+
+        LLUUID mAssetId;
+        LLPointer<LLInventoryCallback> mCallback;
+    };
+
+	const LLUUID& asset_id = item->getAssetUUID();
+    LLItemAddedObserver* observer = new LLItemAddedObserver(asset_id, cb);
+    gInventory.addObserver(observer);
+    object->moveInventory(destination_id, item_id);
+}
+
 void create_new_item(const std::string& name,
 				   const LLUUID& parent_id,
 				   LLAssetType::EType asset_type,
diff --git a/indra/newview/llviewerinventory.h b/indra/newview/llviewerinventory.h
index 24b632632b911a0d734ce942ec72510906fe7b0f..6c0f1b8d07fbe44c2d60f45cbf25a3aad28d2f9f 100644
--- a/indra/newview/llviewerinventory.h
+++ b/indra/newview/llviewerinventory.h
@@ -439,6 +439,10 @@ void copy_inventory_from_notecard(const LLUUID& destination_id,
 								  const LLInventoryItem *src,
 								  U32 callback_id = 0);
 
+void move_or_copy_inventory_from_object(const LLUUID& destination_id,
+                                        const LLUUID& object_id,
+                                        const LLUUID& item_id,
+                                        LLPointer<LLInventoryCallback> cb);
 
 void menu_create_inventory_item(LLInventoryPanel* root,
 								LLFolderBridge* bridge,
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index d21d6f702758f2e42ed96488273b106e80cae0d1..69e62ace9760da34956c00ad936fb60ae7af9e57 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -3610,6 +3610,17 @@ LLInventoryObject* LLViewerObject::getInventoryObject(const LLUUID& item_id)
 	return rv;
 }
 
+LLInventoryItem* LLViewerObject::getInventoryItem(const LLUUID& item_id)
+{
+	LLInventoryObject* iobj = getInventoryObject(item_id);
+	if (!iobj || iobj->getType() == LLAssetType::AT_CATEGORY)
+	{
+		return NULL;
+	}
+	LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(iobj);
+	return item;
+}
+
 void LLViewerObject::getInventoryContents(LLInventoryObject::object_list_t& objects)
 {
 	if(mInventory)
diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h
index 3665c64965408acc8755e2d03171deae9c74b990..898b21e1aee02590cff56530173c707f50dde285 100644
--- a/indra/newview/llviewerobject.h
+++ b/indra/newview/llviewerobject.h
@@ -494,6 +494,7 @@ class LLViewerObject
 	void updateInventoryLocal(LLInventoryItem* item, U8 key); // Update without messaging.
 	void updateMaterialInventory(LLViewerInventoryItem* item, U8 key, bool is_new);
 	LLInventoryObject* getInventoryObject(const LLUUID& item_id);
+	LLInventoryItem* getInventoryItem(const LLUUID& item_id);
 
 	// Get content except for root category
 	void getInventoryContents(LLInventoryObject::object_list_t& objects);
diff --git a/indra/newview/skins/default/xui/en/floater_bulk_perms.xml b/indra/newview/skins/default/xui/en/floater_bulk_perms.xml
index 431c33a3393279299cc9c02a33d18edaad5f6f9a..7aa31bed714344876833cac61c600c6a63156825 100644
--- a/indra/newview/skins/default/xui/en/floater_bulk_perms.xml
+++ b/indra/newview/skins/default/xui/en/floater_bulk_perms.xml
@@ -170,6 +170,20 @@
     name="icon_setting"
     tool_tip="Environment settings"
     left_pad="2" />
+  <check_box
+    control_name="BulkChangeIncludeMaterials"
+    height="16"
+    name="check_materials"
+    top_pad="5"
+    left="245"
+    width="16" />
+  <icon
+    height="16"
+    image_name="Inv_Material"
+    mouse_opaque="true"
+    name="icon_materials"
+    tool_tip="Materials"
+    left_pad="2" />
     <button
       height="23"
     layout="topleft"
diff --git a/indra/newview/skins/default/xui/en/floater_new_feature_notification.xml b/indra/newview/skins/default/xui/en/floater_new_feature_notification.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c8726d36b44d2b8e81590561ba4bf31bdfd4594d
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/floater_new_feature_notification.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ height="130"
+ width="300"
+ layout="topleft"
+ name="floater_new_feature_notification"
+ title="NEW FEATURE"
+ show_title="false"
+ header_height="0"
+ bg_opaque_image="Window_NoTitle_Foreground"
+ bg_alpha_image="Window_NoTitle_Background"
+ can_resize="false"
+ can_drag_on_left="false"
+ can_minimize="false"
+ can_close="false">
+    <floater.string name="title_txt_inventory">
+New inventory features
+    </floater.string>
+    <floater.string name="description_txt_inventory">
+You can now add preview images to inventory items and view a folder in its own window.
+Learn more in this [https://community.secondlife.com/blogs/entry/13637-new-features-inventory-item-preview-and-single-folder-view/ blogpost]
+    </floater.string>
+    <floater.string name="title_txt_gltf">
+New GLTF PBR materials support
+    </floater.string>
+    <floater.string name="description_txt_gltf">
+You can now use expanded material support with the ability to import and edit GLTF Physically Based Rendering (PBR) Materials.
+In order to support the addition of the GLTF format, some areas in the viewer may appear darker than usual.
+
+Learn more about [https://wiki.secondlife.com/wiki/PBR_Materials Physically Based Rendering (PBR)]
+    </floater.string>
+  <text
+   type="string"
+   length="1"
+   follows="top|left|right"
+   font="SansSerifLargeBold"
+   text_color="White"
+   layout="topleft"
+   left="10"
+   height="14"
+   top="10"
+   right="-10"
+   name="title_txt">
+New feature
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="top|left|right|bottom"
+   text_color="White"
+   layout="topleft"
+   left="10"
+   height="40"
+   top_pad="14"
+   right="-10"
+   word_wrap="true"
+   name="description_txt">
+Feature description
+  </text>
+  <button
+   follows="bottom|left|right"
+   layout="topleft"
+   height="24"
+   label="Got it!"
+   left="104"
+   bottom="-10"
+   name="close_btn"
+   width="90"/>
+</floater>
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index ca07aacb210846ef92686a8dcdc158a46086e408..bbc379803405188140a8d31d566a48cdbe785da5 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -9209,6 +9209,18 @@ We cannot display a preview of this texture because it is no-copy and/or no-tran
     yestext="OK"/>
   </notification>
 
+  <notification
+   icon="alertmodal.tga"
+   name="LivePreviewUnavailablePBR"
+   type="alert">
+   
+We cannot display a preview of this material because it is no-copy, no-transfer, and/or no-modify.
+  <usetemplate
+    ignoretext="Warn me that Live Preview mode is not available for no-copy, no-transfer, and/or no-modify materials"
+    name="okignore"
+    yestext="OK"/>
+  </notification>
+
   <notification
    icon="alertmodal.tga"
    name="FacePasteFailed"
diff --git a/indra/newview/skins/default/xui/en/panel_tools_texture.xml b/indra/newview/skins/default/xui/en/panel_tools_texture.xml
index 51e6099cebd30d35ec81a540d0d36632dbbe7a0f..5b15752eb721709d70fa13d148996bb462a4772e 100644
--- a/indra/newview/skins/default/xui/en/panel_tools_texture.xml
+++ b/indra/newview/skins/default/xui/en/panel_tools_texture.xml
@@ -284,12 +284,26 @@
              name="pbr_from_inventory"
              label="Choose from inventory"
              width="140"/>
+            <text
+             visible="false"
+             type="string"
+             length="1"
+             follows="left|top"
+             height="10"
+             layout="topleft"
+             top_pad="4"
+             left_delta="0"
+             name="material_permissions_loading_label"
+             text_readonly_color="LabelDisabledColor"
+             width="160">
+                Loading contents...
+            </text>
             <button
              follows="left|top"
              height="23"
              layout="topleft"
              left_delta="0"
-             top_pad="4"
+             top_delta="0"
              name="edit_selected_pbr"
              label="Edit Selected"
              width="140"/>