From 678817626b844fe7a635ca97315ba524487252a6 Mon Sep 17 00:00:00 2001
From: Rye Mutt <rye@alchemyviewer.org>
Date: Mon, 22 Jan 2024 18:10:40 -0500
Subject: [PATCH] Merge

---
 indra/llmath/llcamera.h                    |   1 -
 indra/llrender/llgltexture.h               |   2 +-
 indra/newview/CMakeLists.txt               |   2 +
 indra/newview/app_settings/settings.xml    |   4 +-
 indra/newview/lldrawpool.cpp               |  17 +-
 indra/newview/lldrawpool.h                 |  12 +-
 indra/newview/lldrawpoolterrain.cpp        |  19 +-
 indra/newview/lldrawpooltree.cpp           |  13 +-
 indra/newview/lldynamictexture.cpp         |  29 +-
 indra/newview/llfetchedgltfmaterial.cpp    |  52 --
 indra/newview/llfetchedgltfmaterial.h      |   5 -
 indra/newview/llgltfmaterialpreviewmgr.cpp | 553 +++++++++++++++++++++
 indra/newview/llgltfmaterialpreviewmgr.h   |  82 +++
 indra/newview/llreflectionmapmanager.cpp   |  39 ++
 indra/newview/llreflectionmapmanager.h     |   5 +
 indra/newview/lltexturectrl.cpp            |  93 +++-
 indra/newview/lltexturectrl.h              |   3 +
 indra/newview/llviewertexture.cpp          |   8 +-
 indra/newview/pipeline.cpp                 |  28 +-
 indra/newview/pipeline.h                   |   4 +-
 20 files changed, 819 insertions(+), 152 deletions(-)
 create mode 100644 indra/newview/llgltfmaterialpreviewmgr.cpp
 create mode 100644 indra/newview/llgltfmaterialpreviewmgr.h

diff --git a/indra/llmath/llcamera.h b/indra/llmath/llcamera.h
index ce271dd99f7..96416708431 100644
--- a/indra/llmath/llcamera.h
+++ b/indra/llmath/llcamera.h
@@ -65,7 +65,6 @@ class LLCamera
 : 	public LLCoordFrame
 {
 public:
-	
 	LLCamera(const LLCamera& rhs)
 	{
 		*this = rhs;
diff --git a/indra/llrender/llgltexture.h b/indra/llrender/llgltexture.h
index 32427b79ae8..1ece18e7f4b 100644
--- a/indra/llrender/llgltexture.h
+++ b/indra/llrender/llgltexture.h
@@ -42,7 +42,7 @@ class LLGLTexture : public LLTexture
 public:
 	enum
 	{
-		MAX_IMAGE_SIZE_DEFAULT = 1024,
+		MAX_IMAGE_SIZE_DEFAULT = 2048,
 		INVALID_DISCARD_LEVEL = 0x7fff
 	};
 
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 7976f75778b..0f671d861c1 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -372,6 +372,7 @@ set(viewer_SOURCE_FILES
     llgiveinventory.cpp
     llglsandbox.cpp
     llgltfmateriallist.cpp
+    llgltfmaterialpreviewmgr.cpp
     llgroupactions.cpp
     llgroupiconctrl.cpp
     llgrouplist.cpp
@@ -1104,6 +1105,7 @@ set(viewer_HEADER_FILES
     llgesturemgr.h
     llgiveinventory.h
     llgltfmateriallist.h
+    llgltfmaterialpreviewmgr.h
     llgroupactions.h
     llgroupiconctrl.h
     llgrouplist.h
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 1f25805d753..db0f26905fd 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -16308,7 +16308,7 @@
       <key>Type</key>
       <string>S32</string>
       <key>Value</key>
-      <integer>2048</integer>
+      <integer>1024</integer>
     </map>
     <key>max_texture_dimension_Y</key>
     <map>
@@ -16319,7 +16319,7 @@
       <key>Type</key>
       <string>S32</string>
       <key>Value</key>
-      <integer>2048</integer>
+      <integer>1024</integer>
     </map>
     <!-- End of back compatibility settings -->
     <key>teleport_offer_invitation_max_length</key>
diff --git a/indra/newview/lldrawpool.cpp b/indra/newview/lldrawpool.cpp
index 8899410dace..eb4595a1965 100644
--- a/indra/newview/lldrawpool.cpp
+++ b/indra/newview/lldrawpool.cpp
@@ -566,14 +566,19 @@ void LLRenderPass::pushRiggedMaskBatches(U32 type, bool texture, bool batch_text
 
 void LLRenderPass::applyModelMatrix(const LLDrawInfo& params)
 {
-	if (params.mModelMatrix != gGLLastMatrix)
+	applyModelMatrix(params.mModelMatrix);
+}
+
+void LLRenderPass::applyModelMatrix(const LLMatrix4a* model_matrix)
+{
+	if (model_matrix != gGLLastMatrix)
 	{
-		gGLLastMatrix = params.mModelMatrix;
+		gGLLastMatrix = model_matrix;
 		gGL.matrixMode(LLRender::MM_MODELVIEW);
 		gGL.loadMatrix(gGLModelView);
-		if (params.mModelMatrix)
+		if (model_matrix)
 		{
-			gGL.multMatrix(*params.mModelMatrix);
+			gGL.multMatrix(*model_matrix);
 		}
 		gPipeline.mMatrixOpCount++;
 	}
@@ -747,6 +752,7 @@ void LLRenderPass::pushUntexturedGLTFBatches(U32 type)
     }
 }
 
+// static
 void LLRenderPass::pushGLTFBatch(LLDrawInfo& params)
 {
     auto& mat = params.mGLTFMaterial;
@@ -765,6 +771,7 @@ void LLRenderPass::pushGLTFBatch(LLDrawInfo& params)
     teardown_texture_matrix(params);
 }
 
+// static
 void LLRenderPass::pushUntexturedGLTFBatch(LLDrawInfo& params)
 {
     auto& mat = params.mGLTFMaterial;
@@ -826,6 +833,7 @@ void LLRenderPass::pushUntexturedRiggedGLTFBatches(U32 type)
 }
 
 
+// static
 void LLRenderPass::pushRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId)
 {
     if (params.mAvatar.notNull() && (lastAvatar != params.mAvatar.get() || lastMeshId != params.mSkinInfo->mHash))
@@ -838,6 +846,7 @@ void LLRenderPass::pushRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvat
     pushGLTFBatch(params);
 }
 
+// static
 void LLRenderPass::pushUntexturedRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId)
 {
     if (params.mAvatar.notNull() && (lastAvatar != params.mAvatar.get() || lastMeshId != params.mSkinInfo->mHash))
diff --git a/indra/newview/lldrawpool.h b/indra/newview/lldrawpool.h
index 0925a01439a..b4d30368a50 100644
--- a/indra/newview/lldrawpool.h
+++ b/indra/newview/lldrawpool.h
@@ -349,8 +349,8 @@ class LLRenderPass : public LLDrawPool
 	void resetDrawOrders() { }
 
 	static void applyModelMatrix(const LLDrawInfo& params);
-    // Use before a non-GLTF batch if it is interleaved with GLTF batches that share the same shader
-    static void resetGLTFTextureTransform();
+    // For rendering that doesn't use LLDrawInfo for some reason
+	static void applyModelMatrix(const LLMatrix4a* model_matrix);
 	void pushBatches(U32 type, bool texture = true, bool batch_textures = false);
     void pushUntexturedBatches(U32 type);
 
@@ -374,10 +374,10 @@ class LLRenderPass : public LLDrawPool
     void pushUntexturedRiggedGLTFBatches(U32 type);
 
     // push a single GLTF draw call
-    void pushGLTFBatch(LLDrawInfo& params);
-    void pushRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId);
-    void pushUntexturedGLTFBatch(LLDrawInfo& params);
-    void pushUntexturedRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId);
+    static void pushGLTFBatch(LLDrawInfo& params);
+    static void pushRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId);
+    static void pushUntexturedGLTFBatch(LLDrawInfo& params);
+    static void pushUntexturedRiggedGLTFBatch(LLDrawInfo& params, LLVOAvatar*& lastAvatar, U64& lastMeshId);
 
 	void pushMaskBatches(U32 type, bool texture = true, bool batch_textures = false);
     void pushRiggedMaskBatches(U32 type, bool texture = true, bool batch_textures = false);
diff --git a/indra/newview/lldrawpoolterrain.cpp b/indra/newview/lldrawpoolterrain.cpp
index 1e2962f93bb..974974d2c1b 100644
--- a/indra/newview/lldrawpoolterrain.cpp
+++ b/indra/newview/lldrawpoolterrain.cpp
@@ -201,25 +201,12 @@ void LLDrawPoolTerrain::drawLoop()
 	{
 		for (LLFace* facep : mDrawFace)
 		{
+
 			if (!facep || !facep->getDrawable() || !facep->getDrawable()->getRegion())
 				continue;
 
-			LLMatrix4a* model_matrix = &(facep->getDrawable()->getRegion()->mRenderMatrix);
-			if(model_matrix && model_matrix->isIdentity())
-			{
-				model_matrix = NULL;
-			}
-			if (model_matrix != gGLLastMatrix)
-			{
-				llassert(gGL.getMatrixMode() == LLRender::MM_MODELVIEW);
-				gGLLastMatrix = model_matrix;
-				gGL.loadMatrix(gGLModelView);
-				if (model_matrix)
-				{
-					gGL.multMatrix(*model_matrix);
-				}
-				gPipeline.mMatrixOpCount++;
-			}
+            llassert(gGL.getMatrixMode() == LLRender::MM_MODELVIEW);
+            LLRenderPass::applyModelMatrix(&facep->getDrawable()->getRegion()->mRenderMatrix);
 
 			facep->renderIndexed();
 		}
diff --git a/indra/newview/lldrawpooltree.cpp b/indra/newview/lldrawpooltree.cpp
index 335d0949fab..dc76be91725 100644
--- a/indra/newview/lldrawpooltree.cpp
+++ b/indra/newview/lldrawpooltree.cpp
@@ -93,17 +93,8 @@ void LLDrawPoolTree::renderDeferred(S32 pass)
 			{
 				model_matrix = NULL;
 			}
-            if (model_matrix != gGLLastMatrix)
-            {
-                gGLLastMatrix = model_matrix;
-                gGL.loadMatrix(gGLModelView);
-                if (model_matrix)
-                {
-                    llassert(gGL.getMatrixMode() == LLRender::MM_MODELVIEW);
-					gGL.multMatrix(*model_matrix);
-                }
-                gPipeline.mMatrixOpCount++;
-            }
+            llassert(gGL.getMatrixMode() == LLRender::MM_MODELVIEW);
+            LLRenderPass::applyModelMatrix(model_matrix);
 
             buff->setBuffer();
             buff->drawRange(LLRender::TRIANGLES, 0, buff->getNumVerts() - 1, buff->getNumIndices(), 0);
diff --git a/indra/newview/lldynamictexture.cpp b/indra/newview/lldynamictexture.cpp
index 84c03f5b014..0232514ef54 100644
--- a/indra/newview/lldynamictexture.cpp
+++ b/indra/newview/lldynamictexture.cpp
@@ -181,19 +181,26 @@ void LLViewerDynamicTexture::postRender(BOOL success)
 //-----------------------------------------------------------------------------
 BOOL LLViewerDynamicTexture::updateAllInstances()
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+
 	sNumRenders = 0;
 	if (gGLManager.mIsDisabled)
 	{
 		return TRUE;
 	}
 
-	bool use_fbo = gPipeline.mBake.isComplete();
+    LLRenderTarget& bake_target = gPipeline.mAuxillaryRT.deferredScreen;
 
-	if (use_fbo)
-	{
-		gPipeline.mBake.bindTarget();
-        gPipeline.mBake.clear();
-	}
+	if (!bake_target.isComplete())
+    {
+        llassert(false);
+		return FALSE;
+    }
+    llassert(bake_target.getWidth() >= LLPipeline::MAX_BAKE_WIDTH);
+    llassert(bake_target.getHeight() >= LLPipeline::MAX_BAKE_WIDTH);
+
+    bake_target.bindTarget();
+    bake_target.clear();
 
 	LLGLSLShader::unbind();
 	LLVertexBuffer::unbind();
@@ -206,11 +213,14 @@ BOOL LLViewerDynamicTexture::updateAllInstances()
 		{
 			if (dynamicTexture->needsRender())
 			{				
+                llassert(dynamicTexture->getFullWidth() <= LLPipeline::MAX_BAKE_WIDTH);
+                llassert(dynamicTexture->getFullHeight() <= LLPipeline::MAX_BAKE_WIDTH);
+
 				glClear(GL_DEPTH_BUFFER_BIT);
 				gDepthDirty = TRUE;
 								
 				gGL.color4f(1,1,1,1);
-                dynamicTexture->setBoundTarget(use_fbo ? &gPipeline.mBake : nullptr);
+                dynamicTexture->setBoundTarget(&bake_target);
 				dynamicTexture->preRender();	// Must be called outside of startRender()
 				result = FALSE;
 				if (dynamicTexture->render())
@@ -227,10 +237,7 @@ BOOL LLViewerDynamicTexture::updateAllInstances()
 		}
 	}
 
-	if (use_fbo)
-	{
-		gPipeline.mBake.flush();
-	}
+	bake_target.flush();
 
     gGL.flush();
 
diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp
index 46b9dffae94..90ec08391d6 100644
--- a/indra/newview/llfetchedgltfmaterial.cpp
+++ b/indra/newview/llfetchedgltfmaterial.cpp
@@ -254,55 +254,3 @@ void LLFetchedGLTFMaterial::materialComplete()
     materialCompleteCallbacks.clear();
     materialCompleteCallbacks.shrink_to_fit();
 }
-
-LLPointer<LLViewerFetchedTexture> LLFetchedGLTFMaterial::getUITexture()
-{
-    if (mFetching)
-    {
-        return nullptr;
-    }
-
-    auto fetch_texture_for_ui = [](LLPointer<LLViewerFetchedTexture>& img, const LLUUID& id)
-    {
-        if (id.notNull())
-        {
-            if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(id))
-            {
-                LLViewerObject* obj = LLSelectMgr::getInstance()->getSelection()->getFirstObject();
-                if (obj)
-                {
-                    LLViewerTexture* viewerTexture = obj->getBakedTextureForMagicId(id);
-                    img = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
-                }
-
-            }
-            else
-            {
-                img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, TRUE, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
-            }
-        }
-        if (img)
-        {
-            img->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
-            img->forceToSaveRawImage(0);
-        }
-    };
-
-    fetch_texture_for_ui(mBaseColorTexture, mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
-    fetch_texture_for_ui(mNormalTexture, mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);
-    fetch_texture_for_ui(mMetallicRoughnessTexture, mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
-    fetch_texture_for_ui(mEmissiveTexture, mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);
-
-    if ((mBaseColorTexture && (mBaseColorTexture->getRawImageLevel() != 0)) ||
-        (mNormalTexture && (mNormalTexture->getRawImageLevel() != 0)) ||
-        (mMetallicRoughnessTexture && (mMetallicRoughnessTexture->getRawImageLevel() != 0)) ||
-        (mEmissiveTexture && (mEmissiveTexture->getRawImageLevel() != 0)))
-    {
-        return nullptr;
-    }
-
-    // *HACK: Use one of the PBR texture components as the preview texture for now
-    mPreviewTexture = mBaseColorTexture;
-
-    return mPreviewTexture;
-}
diff --git a/indra/newview/llfetchedgltfmaterial.h b/indra/newview/llfetchedgltfmaterial.h
index a9e539633d1..2559aa46cc3 100644
--- a/indra/newview/llfetchedgltfmaterial.h
+++ b/indra/newview/llfetchedgltfmaterial.h
@@ -50,8 +50,6 @@ class LLFetchedGLTFMaterial: public LLGLTFMaterial
 
     bool isFetching() const { return mFetching; }
 
-    LLPointer<LLViewerFetchedTexture> getUITexture();
-
     void addTextureEntry(LLTextureEntry* te) override;
     void removeTextureEntry(LLTextureEntry* te) override;
     virtual bool replaceLocalTexture(const LLUUID& tracking_id, const LLUUID& old_id, const LLUUID& new_id) override;
@@ -65,9 +63,6 @@ class LLFetchedGLTFMaterial: public LLGLTFMaterial
 
     std::set<LLTextureEntry*> mTextureEntires;
 
-    // Texture used for previewing the material in the UI
-    LLPointer<LLViewerFetchedTexture> mPreviewTexture;
-
 protected:
     // Lifetime management
     
diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp
new file mode 100644
index 00000000000..d020f5a2ff8
--- /dev/null
+++ b/indra/newview/llgltfmaterialpreviewmgr.cpp
@@ -0,0 +1,553 @@
+/**
+ * @file llgltfmaterialpreviewmgr.cpp
+ *
+ * $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 "llgltfmaterialpreviewmgr.h"
+
+#include <memory>
+#include <vector>
+
+#include "llavatarappearancedefines.h"
+#include "llenvironment.h"
+#include "llselectmgr.h"
+#include "llviewercamera.h"
+#include "llviewerobject.h"
+#include "llviewershadermgr.h"
+#include "llviewertexturelist.h"
+#include "llviewerwindow.h"
+#include "llvolumemgr.h"
+#include "pipeline.h"
+
+LLGLTFMaterialPreviewMgr gGLTFMaterialPreviewMgr;
+
+namespace
+{
+    constexpr S32 FULLY_LOADED = 0;
+    constexpr S32 NOT_LOADED = 99;
+};
+
+LLGLTFPreviewTexture::MaterialLoadLevels::MaterialLoadLevels()
+{
+    for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i)
+    {
+        levels[i] = NOT_LOADED;
+    }
+}
+
+S32& LLGLTFPreviewTexture::MaterialLoadLevels::operator[](size_t i)
+{
+    llassert(i >= 0 && i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT);
+    return levels[i];
+}
+
+const S32& LLGLTFPreviewTexture::MaterialLoadLevels::operator[](size_t i) const
+{
+    llassert(i >= 0 && i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT);
+    return levels[i];
+}
+
+bool LLGLTFPreviewTexture::MaterialLoadLevels::operator<(const MaterialLoadLevels& other) const
+{
+    bool less = false;
+    for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i)
+    {
+        if (((*this)[i] > other[i])) { return false; }
+        less = less || ((*this)[i] < other[i]);
+    }
+    return less;
+}
+
+bool LLGLTFPreviewTexture::MaterialLoadLevels::operator>(const MaterialLoadLevels& other) const
+{
+    bool great = false;
+    for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i)
+    {
+        if (((*this)[i] < other[i])) { return false; }
+        great = great || ((*this)[i] > other[i]);
+    }
+    return great;
+}
+
+namespace
+{
+    void fetch_texture_for_ui(LLPointer<LLViewerFetchedTexture>& img, const LLUUID& id)
+    {
+        if (id.notNull())
+        {
+            if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(id))
+            {
+                LLViewerObject* obj = LLSelectMgr::getInstance()->getSelection()->getFirstObject();
+                if (obj)
+                {
+                    LLViewerTexture* viewerTexture = obj->getBakedTextureForMagicId(id);
+                    img = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
+                }
+            }
+            else
+            {
+                img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, TRUE, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+            }
+        }
+        if (img)
+        {
+            img->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
+            img->forceToSaveRawImage(0);
+        }
+    };
+
+    // *NOTE: Does not use the same conventions as texture discard level. Lower is better.
+    S32 get_texture_load_level(const LLPointer<LLViewerFetchedTexture>& texture)
+    {
+        if (!texture) { return FULLY_LOADED; }
+        const S32 raw_level = texture->getDiscardLevel();
+        if (raw_level < 0) { return NOT_LOADED; }
+        return raw_level;
+    }
+
+    LLGLTFPreviewTexture::MaterialLoadLevels get_material_load_levels(LLFetchedGLTFMaterial& material)
+    {
+        using MaterialTextures = LLPointer<LLViewerFetchedTexture>*[LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT];
+
+        MaterialTextures textures;
+
+        textures[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] = &material.mBaseColorTexture;
+        textures[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] = &material.mNormalTexture;
+        textures[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] = &material.mMetallicRoughnessTexture;
+        textures[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] = &material.mEmissiveTexture;
+
+        LLGLTFPreviewTexture::MaterialLoadLevels levels;
+
+        for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i)
+        {
+            fetch_texture_for_ui(*textures[i], material.mTextureId[i]);
+            levels[i] = get_texture_load_level(*textures[i]);
+        }
+
+        return levels;
+    }
+
+    // Is the material loaded enough to start rendering a preview?
+    bool is_material_loaded_enough_for_ui(LLFetchedGLTFMaterial& material)
+    {
+        if (material.isFetching())
+        {
+            return false;
+        }
+
+        LLGLTFPreviewTexture::MaterialLoadLevels levels = get_material_load_levels(material);
+
+        for (U32 i = 0; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++i)
+        {
+            if (levels[i] == NOT_LOADED)
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+};  // namespace
+
+LLGLTFPreviewTexture::LLGLTFPreviewTexture(LLPointer<LLFetchedGLTFMaterial> material, S32 width)
+    : LLViewerDynamicTexture(width, width, 4, EOrder::ORDER_MIDDLE, FALSE)
+    , mGLTFMaterial(material)
+{
+}
+
+// static
+LLPointer<LLGLTFPreviewTexture> LLGLTFPreviewTexture::create(LLPointer<LLFetchedGLTFMaterial> material)
+{
+    return new LLGLTFPreviewTexture(material, LLPipeline::MAX_BAKE_WIDTH);
+}
+
+void LLGLTFPreviewTexture::preRender(BOOL clear_depth)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+
+    MaterialLoadLevels current_load = get_material_load_levels(*mGLTFMaterial.get());
+    if (current_load < mBestLoad)
+    {
+        mShouldRender = true;
+        mBestLoad = current_load;
+    }
+
+    if (!mShouldRender) { return; }
+
+    LLViewerDynamicTexture::preRender(clear_depth);
+}
+
+
+namespace {
+
+struct GLTFPreviewModel
+{
+    GLTFPreviewModel(LLPointer<LLDrawInfo>& info, const LLMatrix4& mat)
+    : mDrawInfo(info)
+    , mModelMatrix(mat)
+    {
+        mDrawInfo->mModelMatrix = &mModelMatrix;
+    }
+    GLTFPreviewModel(GLTFPreviewModel&) = delete;
+    ~GLTFPreviewModel()
+    {
+        // No model matrix necromancy
+        llassert(gGLLastMatrix != &mModelMatrix);
+        gGLLastMatrix = nullptr;
+    }
+    LLPointer<LLDrawInfo> mDrawInfo;
+    LLMatrix4a mModelMatrix; // Referenced by mDrawInfo
+};
+
+using PreviewSpherePart = std::unique_ptr<GLTFPreviewModel>;
+using PreviewSphere = std::vector<PreviewSpherePart>;
+
+// Like LLVolumeGeometryManager::registerFace but without batching or too-many-indices/vertices checking.
+PreviewSphere create_preview_sphere(LLPointer<LLFetchedGLTFMaterial>& material, const LLMatrix4& model_matrix)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+
+    const LLColor4U vertex_color(material->mBaseColor);
+
+    LLPrimitive prim;
+    prim.init_primitive(LL_PCODE_VOLUME);
+    LLVolumeParams params;
+    params.setType(LL_PCODE_PROFILE_CIRCLE_HALF, LL_PCODE_PATH_CIRCLE);
+    params.setBeginAndEndS(0.f, 1.f);
+    params.setBeginAndEndT(0.f, 1.f);
+    params.setRatio(1, 1);
+    params.setShear(0, 0);
+    constexpr auto MAX_LOD = LLVolumeLODGroup::NUM_LODS - 1;
+    prim.setVolume(params, MAX_LOD);
+
+    LLVolume* volume = prim.getVolume();
+    llassert(volume);
+    for (LLVolumeFace& face : volume->getVolumeFaces())
+    {
+        face.createTangents();
+    }
+
+    PreviewSphere preview_sphere;
+    preview_sphere.reserve(volume->getNumFaces());
+
+    LLPointer<LLVertexBuffer> buf = new LLVertexBuffer(
+        LLVertexBuffer::MAP_VERTEX |
+        LLVertexBuffer::MAP_NORMAL |
+        LLVertexBuffer::MAP_TEXCOORD0 |
+        LLVertexBuffer::MAP_COLOR |
+        LLVertexBuffer::MAP_TANGENT
+    );
+    U32 nv = 0;
+    U32 ni = 0;
+    for (LLVolumeFace& face : volume->getVolumeFaces())
+    {
+        nv += face.mNumVertices;
+        ni += face.mNumIndices;
+    }
+    buf->allocateBuffer(nv, ni);
+
+    // UV hacks
+    // Higher factor helps to see more details on the preview sphere
+    const LLVector2 uv_factor(2.0f, 2.0f);
+    // Offset places center of material in center of view
+    const LLVector2 uv_offset(-0.5f, -0.5f);
+
+    LLStrider<U16> indices;
+    LLStrider<LLVector4a> positions;
+    LLStrider<LLVector4a> normals;
+    LLStrider<LLVector2> texcoords;
+    LLStrider<LLColor4U> colors;
+    LLStrider<LLVector4a> tangents;
+	buf->getIndexStrider(indices);
+	buf->getVertexStrider(positions);
+	buf->getNormalStrider(normals);
+	buf->getTexCoord0Strider(texcoords);
+	buf->getColorStrider(colors);
+	buf->getTangentStrider(tangents);
+    U32 index_offset = 0;
+    U32 vertex_offset = 0;
+    for (const LLVolumeFace& face : volume->getVolumeFaces())
+    {
+        for (S32 i = 0; i < face.mNumIndices; ++i)
+        {
+            *indices++ = face.mIndices[i] + vertex_offset;
+        }
+        for (S32 v = 0; v < face.mNumVertices; ++v)
+        {
+            *positions++ = face.mPositions[v];
+            *normals++ = face.mNormals[v];
+            LLVector2 uv(face.mTexCoords[v]);
+            uv.scaleVec(uv_factor);
+            uv += uv_offset;
+            *texcoords++ = uv;
+            *colors++ = vertex_color;
+            *tangents++ = face.mTangents[v];
+        }
+
+        constexpr LLViewerTexture* no_media = nullptr;
+        LLPointer<LLDrawInfo> info = new LLDrawInfo(U16(vertex_offset), U16(vertex_offset + face.mNumVertices - 1), face.mNumIndices, index_offset, no_media, buf.get());
+        info->mGLTFMaterial = material;
+        preview_sphere.emplace_back(std::make_unique<GLTFPreviewModel>(info, model_matrix));
+        index_offset += face.mNumIndices;
+        vertex_offset += face.mNumVertices;
+    }
+
+    buf->unmapBuffer();
+
+    return preview_sphere;
+}
+
+void set_preview_sphere_material(PreviewSphere& preview_sphere, LLPointer<LLFetchedGLTFMaterial>& material)
+{
+    llassert(!preview_sphere.empty());
+    if (preview_sphere.empty()) { return; }
+
+    const LLColor4U vertex_color(material->mBaseColor);
+
+    // See comments about unmapBuffer in llvertexbuffer.h
+    for (PreviewSpherePart& part : preview_sphere)
+    {
+        LLDrawInfo* info = part->mDrawInfo.get();
+        info->mGLTFMaterial = material;
+        LLVertexBuffer* buf = info->mVertexBuffer.get();
+        LLStrider<LLColor4U> colors;
+        const S32 count = info->mEnd - info->mStart;
+        buf->getColorStrider(colors, info->mStart, count);
+        for (S32 i = 0; i < count; ++i)
+        {
+            *colors++ = vertex_color;
+        }
+        buf->unmapBuffer();
+    }
+}
+
+PreviewSphere& get_preview_sphere(LLPointer<LLFetchedGLTFMaterial>& material, const LLMatrix4& model_matrix)
+{
+    static PreviewSphere preview_sphere;
+    if (preview_sphere.empty())
+    {
+        preview_sphere = create_preview_sphere(material, model_matrix);
+    }
+    else
+    {
+        set_preview_sphere_material(preview_sphere, material);
+    }
+    return preview_sphere;
+}
+
+// Final, direct modifications to shader constants, just before render
+void fixup_shader_constants(LLGLSLShader& shader)
+{
+    // Sunlight intensity of 0 no matter what
+    shader.uniform1i(LLShaderMgr::SUN_UP_FACTOR, 1);
+    shader.uniform3fv(LLShaderMgr::SUNLIGHT_COLOR, 1, LLColor3::white.mV);
+    shader.uniform1f(LLShaderMgr::DENSITY_MULTIPLIER, 0.0f);
+
+    // Ignore sun shadow (if enabled)
+    for (U32 i = 0; i < 6; i++)
+    {
+        const S32 channel = shader.getTextureChannel(LLShaderMgr::DEFERRED_SHADOW0+i);
+        if (channel != -1)
+        {
+            gGL.getTexUnit(channel)->bind(LLViewerFetchedTexture::sWhiteImagep, TRUE);
+        }
+    }
+}
+
+// Set a variable to a value temporarily, and restor the variable's old value
+// when this object leaves scope.
+template<typename T>
+struct SetTemporarily
+{
+    T* mRef;
+    T mOldVal;
+    SetTemporarily(T* var, T temp_val)
+    {
+        mRef = var;
+        mOldVal = *mRef;
+        *mRef = temp_val;
+    }
+    ~SetTemporarily()
+    {
+        *mRef = mOldVal;
+    }
+};
+
+}; // namespace
+
+BOOL LLGLTFPreviewTexture::render()
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+
+    if (!mShouldRender) { return FALSE; }
+
+    glClearColor(0, 0, 0, 0);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+    
+    LLGLDepthTest(GL_FALSE);
+    LLGLDisable stencil(GL_STENCIL_TEST);
+    LLGLDisable scissor(GL_SCISSOR_TEST);
+    SetTemporarily<bool> no_dof(&LLPipeline::RenderDepthOfField, false);
+    SetTemporarily<bool> no_glow(&LLPipeline::sRenderGlow, false);
+    SetTemporarily<bool> no_ssr(&LLPipeline::RenderScreenSpaceReflections, false);
+    SetTemporarily<U32> no_fxaa(&LLPipeline::RenderFSAASamples, U32(0));
+    SetTemporarily<LLPipeline::RenderTargetPack*> use_auxiliary_render_target(&gPipeline.mRT, &gPipeline.mAuxillaryRT);
+
+    const LLVector4a light_dir(1.0f, 1.0f, 1.0f, 0.f);
+    SetTemporarily<S32> sun_light_only(&LLPipeline::RenderLocalLightCount, 0);
+
+    gPipeline.mReflectionMapManager.forceDefaultProbeAndUpdateUniforms();
+
+    LLViewerCamera camera;
+
+    // Calculate the object distance at which the object of a given radius will
+    // span the partial width of the screen given by fill_ratio.
+    // Assume the primitive has a scale of 1 (this is the default).
+    constexpr F32 fill_ratio = 0.8f;
+    constexpr F32 object_radius = 0.5f;
+    const F32 object_distance = (object_radius / fill_ratio) * tan(camera.getDefaultFOV());
+    // Negative coordinate shows the textures on the sphere right-side up, when
+    // combined with the UV hacks in create_preview_sphere
+    const LLVector3 object_position(0.0, -object_distance, 0.0);
+    LLMatrix4 object_transform;
+    object_transform.translate(object_position);
+
+    // Set up camera and viewport
+    const LLVector3 origin(0.0, 0.0, 0.0);
+    camera.lookAt(origin, object_position);
+    camera.setAspect(mFullHeight / mFullWidth);
+    const LLRect texture_rect(0, mFullHeight, mFullWidth, 0);
+    camera.setPerspective(NOT_FOR_SELECTION, texture_rect.mLeft, texture_rect.mBottom, texture_rect.getWidth(), texture_rect.getHeight(), FALSE, camera.getNear(), MAX_FAR_CLIP*2.f);
+
+    // Generate sphere object on-the-fly. Discard afterwards. (Vertex buffer is
+    // discarded, but the sphere should be cached in LLVolumeMgr.)
+    PreviewSphere& preview_sphere = get_preview_sphere(mGLTFMaterial, object_transform);
+
+    gPipeline.setupHWLights();
+    const LLMatrix4a& mat = gGLModelView;
+    LLVector4a transformed_light_dir;
+    mat.rotate4(light_dir, transformed_light_dir);
+    SetTemporarily<LLVector4a> force_sun_direction_high_graphics(&gPipeline.mTransformedSunDir, transformed_light_dir);
+    // Override lights to ensure the sun is always shining from a certain direction (low graphics)
+    // See also force_sun_direction_high_graphics and fixup_shader_constants
+    {
+        LLLightState* light = gGL.getLight(0);
+        light->setPosition(LLVector4(light_dir.getF32ptr()));
+        constexpr bool sun_up = true;
+        light->setSunPrimary(sun_up);
+    }
+
+    LLRenderTarget& screen = gPipeline.mAuxillaryRT.screen;
+
+    // *HACK: Force reset of the model matrix
+    gGLLastMatrix = nullptr;
+
+#if 0
+    if (mGLTFMaterial->mAlphaMode == LLGLTFMaterial::ALPHA_MODE_OPAQUE || mGLTFMaterial->mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK)
+    {
+        // *TODO: Opaque/alpha mask rendering
+    }
+    else
+#endif
+    {
+        // Alpha blend rendering
+
+        screen.bindTarget();
+        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+        LLGLSLShader& shader = gDeferredPBRAlphaProgram;
+
+        gPipeline.bindDeferredShader(shader);
+        fixup_shader_constants(shader);
+
+        for (PreviewSpherePart& part : preview_sphere)
+        {
+            LLRenderPass::pushGLTFBatch(*part->mDrawInfo);
+        }
+
+        gPipeline.unbindDeferredShader(shader);
+
+        screen.flush();
+    }
+
+    gPipeline.copyScreenSpaceReflections(&screen, &gPipeline.mSceneMap);
+    gPipeline.generateLuminance(&screen, &gPipeline.mLuminanceMap);
+    gPipeline.generateExposure(&gPipeline.mLuminanceMap, &gPipeline.mExposureMap);
+    gPipeline.gammaCorrect(&screen, &gPipeline.mPostMap);
+    LLVertexBuffer::unbind();
+    gPipeline.generateGlow(&gPipeline.mPostMap);
+    gPipeline.combineGlow(&gPipeline.mPostMap, &screen);
+	gPipeline.renderDoF(&screen, &gPipeline.mPostMap);
+	gPipeline.applyFXAA(&gPipeline.mPostMap, &screen);
+
+    // Final render
+
+	gDeferredPostNoDoFProgram.bind();
+
+	// From LLPipeline::renderFinalize: "Whatever is last in the above post processing chain should _always_ be rendered directly here.  If not, expect problems."
+	gDeferredPostNoDoFProgram.bindTexture(LLShaderMgr::DEFERRED_DIFFUSE, &screen);
+	gDeferredPostNoDoFProgram.bindTexture(LLShaderMgr::DEFERRED_DEPTH, mBoundTarget, true);
+
+	{
+		LLGLDepthTest depth_test(GL_TRUE, GL_TRUE, GL_ALWAYS);
+		gPipeline.mScreenTriangleVB->setBuffer();
+		gPipeline.mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3);
+	}
+
+	gDeferredPostNoDoFProgram.unbind();
+
+    // Clean up
+    gPipeline.setupHWLights();
+    gPipeline.mReflectionMapManager.forceDefaultProbeAndUpdateUniforms(false);
+
+    return TRUE;
+}
+
+void LLGLTFPreviewTexture::postRender(BOOL success)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+
+    if (!mShouldRender) { return; }
+    mShouldRender = false;
+
+    LLViewerDynamicTexture::postRender(success);
+}
+
+// static
+LLPointer<LLViewerTexture> LLGLTFMaterialPreviewMgr::getPreview(LLPointer<LLFetchedGLTFMaterial> &material)
+{
+    if (!material)
+    {
+        return nullptr;
+    }
+
+    if (!is_material_loaded_enough_for_ui(*material))
+    {
+        return nullptr;
+    }
+
+    return LLGLTFPreviewTexture::create(material);
+}
diff --git a/indra/newview/llgltfmaterialpreviewmgr.h b/indra/newview/llgltfmaterialpreviewmgr.h
new file mode 100644
index 00000000000..cc40a6f2e2a
--- /dev/null
+++ b/indra/newview/llgltfmaterialpreviewmgr.h
@@ -0,0 +1,82 @@
+/**
+ * @file llgltfmaterialpreviewmgr.h
+ *
+ * $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$
+ */
+
+#pragma once
+
+#include "lldrawpool.h"
+#include "lldynamictexture.h"
+#include "llfetchedgltfmaterial.h"
+#include "llsingleton.h"
+#include "lltexture.h"
+
+class LLGLTFPreviewTexture : public LLViewerDynamicTexture
+{
+protected:
+    LLGLTFPreviewTexture(LLPointer<LLFetchedGLTFMaterial> material, S32 width);
+
+public:
+    // Width scales with size of material's textures
+    static LLPointer<LLGLTFPreviewTexture> create(LLPointer<LLFetchedGLTFMaterial> material);
+
+    BOOL needsRender() override { return mNeedsRender; }
+    void preRender(BOOL clear_depth = TRUE) override;
+    BOOL render() override;
+    void postRender(BOOL success) override;
+
+    struct MaterialLoadLevels
+    {
+        S32 levels[LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT];
+
+        MaterialLoadLevels();
+
+        S32& operator[](size_t i);
+
+        const S32& operator[](size_t i) const;
+
+        // Less is better
+        // Returns false if lhs is not strictly less or equal for all levels
+        bool operator<(const MaterialLoadLevels& other) const;
+
+        // Less is better
+        // Returns false if lhs is not strictly greater or equal for all levels
+        bool operator>(const MaterialLoadLevels& other) const;
+    };
+
+private:
+    LLPointer<LLFetchedGLTFMaterial> mGLTFMaterial;
+    bool mNeedsRender = true;
+    bool mShouldRender = true;
+    MaterialLoadLevels mBestLoad;
+};
+
+class LLGLTFMaterialPreviewMgr
+{
+  public:
+    // Returns null if the material is not loaded yet.
+    // *NOTE: User should cache the texture if the same material is being previewed
+    LLPointer<LLViewerTexture> getPreview(LLPointer<LLFetchedGLTFMaterial> &material);
+};
+
+extern LLGLTFMaterialPreviewMgr gGLTFMaterialPreviewMgr;
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index e8ab967bd8e..0bf3cdacef1 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -27,6 +27,9 @@
 #include "llviewerprecompiledheaders.h"
 
 #include "llreflectionmapmanager.h"
+
+#include <vector>
+
 #include "llviewercamera.h"
 #include "llspatialpartition.h"
 #include "llviewerregion.h"
@@ -1390,3 +1393,39 @@ void LLReflectionMapManager::doOcclusion()
         }
     }
 }
+
+void LLReflectionMapManager::forceDefaultProbeAndUpdateUniforms(bool force)
+{
+    static std::vector<bool> mProbeWasOccluded;
+
+    if (force)
+    {
+        llassert(mProbeWasOccluded.empty());
+
+        for (size_t i = 0; i < mProbes.size(); ++i)
+        {
+            auto& probe = mProbes[i];
+            mProbeWasOccluded.push_back(probe->mOccluded);
+            if (probe != nullptr && probe != mDefaultProbe)
+            {
+                probe->mOccluded = true;
+            }
+        }
+
+        updateUniforms();
+    }
+    else
+    {
+        llassert(mProbes.size() == mProbeWasOccluded.size());
+
+        const size_t n = llmin(mProbes.size(), mProbeWasOccluded.size());
+        for (size_t i = 0; i < n; ++i)
+        {
+            auto& probe = mProbes[i];
+            llassert(probe->mOccluded == (probe != mDefaultProbe));
+            probe->mOccluded = mProbeWasOccluded[i];
+        }
+        mProbeWasOccluded.clear();
+        mProbeWasOccluded.shrink_to_fit();
+    }
+}
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index b77a33da898..dd641452aef 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -106,6 +106,11 @@ class alignas(16) LLReflectionMapManager
     // perform occlusion culling on all active reflection probes
     void doOcclusion();
 
+    // *HACK: "cull" all reflection probes except the default one. Only call
+    // this if you don't intend to call updateUniforms directly. Call again
+    // with false when done.
+    void forceDefaultProbeAndUpdateUniforms(bool force = true);
+
 private:
     friend class LLPipeline;
 
diff --git a/indra/newview/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp
index 04a61b1a8dc..69446762543 100644
--- a/indra/newview/lltexturectrl.cpp
+++ b/indra/newview/lltexturectrl.cpp
@@ -63,6 +63,7 @@
 
 #include "llradiogroup.h"
 #include "llfloaterreg.h"
+#include "llgltfmaterialpreviewmgr.h"
 #include "lllocalgltfmaterials.h"
 #include "llerror.h"
 
@@ -660,6 +661,7 @@ void LLFloaterTexturePicker::draw()
 	if( mOwner ) 
 	{
 		mTexturep = NULL;
+        LLPointer<LLFetchedGLTFMaterial> old_material = mGLTFMaterial;
         mGLTFMaterial = NULL;
         if (mImageAssetID.notNull())
         {
@@ -667,10 +669,23 @@ void LLFloaterTexturePicker::draw()
             {
                 mGLTFMaterial = (LLFetchedGLTFMaterial*) gGLTFMaterialList.getMaterial(mImageAssetID);
                 llassert(mGLTFMaterial == nullptr || dynamic_cast<LLFetchedGLTFMaterial*>(gGLTFMaterialList.getMaterial(mImageAssetID)) != nullptr);
+                if (mGLTFPreview.isNull() || mGLTFMaterial.isNull() || (old_material.notNull() && (*old_material.get() != *mGLTFMaterial.get())))
+                {
+                    // Only update the preview if needed, since gGLTFMaterialPreviewMgr does not cache the preview.
+                    if (mGLTFMaterial.isNull())
+                    {
+                        mGLTFPreview = nullptr;
+                    }
+                    else
+                    {
+                        mGLTFPreview = gGLTFMaterialPreviewMgr.getPreview(mGLTFMaterial);
+                    }
+                }
             }
             else
             {
                 LLPointer<LLViewerFetchedTexture> texture = NULL;
+                mGLTFPreview = nullptr;
 
                 if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(mImageAssetID))
                 {
@@ -680,7 +695,7 @@ void LLFloaterTexturePicker::draw()
                     if (obj)
                     {
                         LLViewerTexture* viewerTexture = obj->getBakedTextureForMagicId(mImageAssetID);
-                        texture = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
+						texture = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
                     }
                 }
 
@@ -722,27 +737,29 @@ void LLFloaterTexturePicker::draw()
 
 		// If the floater is focused, don't apply its alpha to the texture (STORM-677).
 		const F32 alpha = getTransparencyType() == TT_ACTIVE ? 1.0f : getCurrentTransparency();
-        LLViewerTexture* texture = nullptr;
+        LLViewerTexture* preview = nullptr;
         if (mGLTFMaterial)
         {
-            texture = mGLTFMaterial->getUITexture();
+            preview = mGLTFPreview.get();
         }
         else
         {
-            texture = mTexturep.get();
+            preview = mTexturep.get();
+            if (mTexturep)
+            {
+                // Pump the priority
+				mTexturep->addTextureStats( (F32)(interior.getWidth() * interior.getHeight()) );
+            }
         }
 
-		if( texture )
+		if( preview )
 		{
-			if( texture->getComponents() == 4 )
+			if( preview->getComponents() == 4 )
 			{
 				gl_rect_2d_checkerboard( interior, alpha );
 			}
 
-			gl_draw_scaled_image( interior.mLeft, interior.mBottom, interior.getWidth(), interior.getHeight(), texture, UI_VERTEX_COLOR % alpha );
-
-			// Pump the priority
-			texture->addTextureStats( (F32)(interior.getWidth() * interior.getHeight()) );
+			gl_draw_scaled_image( interior.mLeft, interior.mBottom, interior.getWidth(), interior.getHeight(), preview, UI_VERTEX_COLOR % alpha );
 		}
 		else if (!mFallbackImage.isNull())
 		{
@@ -2193,48 +2210,69 @@ void LLTextureCtrl::draw()
 {
 	mBorder->setKeyboardFocusHighlight(hasFocus());
 
+    LLPointer<LLViewerTexture> preview = NULL;
+
 	if (!mValid)
 	{
 		mTexturep = NULL;
+        mGLTFMaterial = NULL;
+        mGLTFPreview = NULL;
 	}
 	else if (!mImageAssetID.isNull())
 	{
-		LLPointer<LLViewerFetchedTexture> texture = NULL;
-
 		if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(mImageAssetID))
 		{
 			LLViewerObject* obj = LLSelectMgr::getInstance()->getSelection()->getFirstObject();
 			if (obj)
 			{
 				LLViewerTexture* viewerTexture = obj->getBakedTextureForMagicId(mImageAssetID);
-				texture = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
+				mTexturep = viewerTexture ? dynamic_cast<LLViewerFetchedTexture*>(viewerTexture) : NULL;
+				mGLTFMaterial = NULL;
+				mGLTFPreview = NULL;
+
+                preview = mTexturep;
 			}
 			
 		}
 
-		if (texture.isNull())
+		if (preview.isNull())
 		{
+            LLPointer<LLFetchedGLTFMaterial> old_material = mGLTFMaterial;
+            mGLTFMaterial = NULL;
+            mTexturep = NULL;
             if (mInventoryPickType == PICK_MATERIAL)
             {
-                LLPointer<LLFetchedGLTFMaterial> material = gGLTFMaterialList.getMaterial(mImageAssetID);
-                if (material)
+                mGLTFMaterial = gGLTFMaterialList.getMaterial(mImageAssetID);
+                if (mGLTFPreview.isNull() || mGLTFMaterial.isNull() || (old_material.notNull() && (*old_material.get() != *mGLTFMaterial.get())))
                 {
-                    texture = material->getUITexture();
+                    // Only update the preview if needed, since gGLTFMaterialPreviewMgr does not cache the preview.
+                    if (mGLTFMaterial.isNull())
+                    {
+                        mGLTFPreview = nullptr;
+                    }
+                    else
+                    {
+                        mGLTFPreview = gGLTFMaterialPreviewMgr.getPreview(mGLTFMaterial);
+                    }
                 }
+
+                preview = mGLTFPreview;
             }
             else
             {
-                texture = LLViewerTextureManager::getFetchedTexture(mImageAssetID, FTT_DEFAULT, TRUE, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
-                texture->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
-                texture->forceToSaveRawImage(0);
+				mTexturep = LLViewerTextureManager::getFetchedTexture(mImageAssetID, FTT_DEFAULT, TRUE, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+				mTexturep->setBoostLevel(LLGLTexture::BOOST_PREVIEW);
+				mTexturep->forceToSaveRawImage(0);
+
+				preview = mTexturep;
             }
 		}
-
-		mTexturep = texture;
 	}
 	else//mImageAssetID == LLUUID::null
 	{
 		mTexturep = NULL; 
+        mGLTFMaterial = NULL;
+        mGLTFPreview = NULL;
 	}
 	
 	// Border
@@ -2247,15 +2285,18 @@ void LLTextureCtrl::draw()
 
 	// If we're in a focused floater, don't apply the floater's alpha to the texture (STORM-677).
 	const F32 alpha = getTransparencyType() == TT_ACTIVE ? 1.0f : getCurrentTransparency();
-	if( mTexturep )
+	if( preview )
 	{
-		if( mTexturep->getComponents() == 4 )
+		if( preview->getComponents() == 4 )
 		{
 			gl_rect_2d_checkerboard( interior, alpha );
 		}
 		
-		gl_draw_scaled_image( interior.mLeft, interior.mBottom, interior.getWidth(), interior.getHeight(), mTexturep, UI_VERTEX_COLOR % alpha);
-		mTexturep->addTextureStats( (F32)(interior.getWidth() * interior.getHeight()) );
+		gl_draw_scaled_image( interior.mLeft, interior.mBottom, interior.getWidth(), interior.getHeight(), preview, UI_VERTEX_COLOR % alpha);
+        if (mTexturep)
+        {
+			mTexturep->addTextureStats( (F32)(interior.getWidth() * interior.getHeight()) );
+        }
 	}
 	else if (!mFallbackImage.isNull())
 	{
diff --git a/indra/newview/lltexturectrl.h b/indra/newview/lltexturectrl.h
index 1e3792c69a0..aba1df8c9bb 100644
--- a/indra/newview/lltexturectrl.h
+++ b/indra/newview/lltexturectrl.h
@@ -252,6 +252,8 @@ class LLTextureCtrl
 	commit_callback_t		 	mOnCloseCallback;
 	texture_selected_callback	mOnTextureSelectedCallback;
 	LLPointer<LLViewerFetchedTexture> mTexturep;
+	LLPointer<LLFetchedGLTFMaterial> mGLTFMaterial;
+	LLPointer<LLViewerTexture> mGLTFPreview;
 	LLUIColor				 	mBorderColor;
 	LLUUID					 	mImageItemID;
 	LLUUID					 	mImageAssetID;
@@ -389,6 +391,7 @@ class LLFloaterTexturePicker final : public LLFloater
 
 	LLPointer<LLViewerTexture> mTexturep;
     LLPointer<LLFetchedGLTFMaterial> mGLTFMaterial;
+    LLPointer<LLViewerTexture> mGLTFPreview;
 	LLView*				mOwner;
 
 	LLUUID				mImageAssetID; // Currently selected texture
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index fdc47fa24c7..e3b481b23c5 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -1749,7 +1749,7 @@ void LLViewerFetchedTexture::processTextureStats()
         {
             if (mFullWidth > MAX_IMAGE_SIZE_DEFAULT || mFullHeight > MAX_IMAGE_SIZE_DEFAULT)
             {
-                mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 1024 and max size ever is 2048
+                mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096
             }
             else
             {
@@ -1762,7 +1762,7 @@ void LLViewerFetchedTexture::processTextureStats()
 		}
 		else
 		{
-			U32 desired_size = MAX_IMAGE_SIZE_DEFAULT; // MAX_IMAGE_SIZE_DEFAULT = 1024 and max size ever is 2048
+			U32 desired_size = MAX_IMAGE_SIZE_DEFAULT; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096
 			if(!mKnownDrawWidth || !mKnownDrawHeight || mFullWidth <= mKnownDrawWidth || mFullHeight <= mKnownDrawHeight)
 			{
 				if (mFullWidth > desired_size || mFullHeight > desired_size)
@@ -3211,7 +3211,7 @@ void LLViewerLODTexture::processTextureStats()
 	{
 		mDesiredDiscardLevel = 0;
 		if (mFullWidth > MAX_IMAGE_SIZE_DEFAULT || mFullHeight > MAX_IMAGE_SIZE_DEFAULT)
-			mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 1024 and max size ever is 2048
+			mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096
 	}
 	else if (mBoostLevel < LLGLTexture::BOOST_HIGH && mMaxVirtualSize <= 10.f)
 	{
@@ -3256,7 +3256,7 @@ void LLViewerLODTexture::processTextureStats()
         discard_level = floorf(discard_level);
 
         F32 min_discard = 0.f;
-        U32 desired_size = MAX_IMAGE_SIZE_DEFAULT; // MAX_IMAGE_SIZE_DEFAULT = 1024 and max size ever is 2048
+        U32 desired_size = MAX_IMAGE_SIZE_DEFAULT; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096
         if (mBoostLevel <= LLGLTexture::BOOST_SCULPTED)
         {
             desired_size = DESIRED_NORMAL_TEXTURE_SIZE;
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index ecfb35c08d4..df1a57193a7 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -179,6 +179,7 @@ F32 LLPipeline::CameraFocusTransitionTime;
 F32 LLPipeline::CameraFNumber;
 F32 LLPipeline::CameraFocalLength;
 F32 LLPipeline::CameraFieldOfView;
+S32 LLPipeline::RenderLocalLightCount;
 F32 LLPipeline::RenderShadowNoise;
 F32 LLPipeline::RenderShadowBlurSize;
 F32 LLPipeline::RenderSSAOScale;
@@ -217,6 +218,8 @@ S32 LLPipeline::RenderBufferVisualization;
 F32 LLPipeline::RenderNormalMapScale;
 LLTrace::EventStatHandle<S64> LLPipeline::sStatBatchSize("renderbatchsize");
 
+const U32 LLPipeline::MAX_BAKE_WIDTH = 512;
+
 const F32 BACKLIGHT_DAY_MAGNITUDE_OBJECT = 0.1f;
 const F32 BACKLIGHT_NIGHT_MAGNITUDE_OBJECT = 0.08f;
 const F32 ALPHA_BLEND_CUTOFF = 0.598f;
@@ -547,6 +550,7 @@ void LLPipeline::init()
 	connectRefreshCachedSettingsSafe("CameraFNumber");
 	connectRefreshCachedSettingsSafe("CameraFocalLength");
 	connectRefreshCachedSettingsSafe("CameraFieldOfView");
+	connectRefreshCachedSettingsSafe("RenderLocalLightCount");
 	connectRefreshCachedSettingsSafe("RenderShadowNoise");
 	connectRefreshCachedSettingsSafe("RenderShadowBlurSize");
 	connectRefreshCachedSettingsSafe("RenderSSAOScale");
@@ -822,10 +826,13 @@ LLPipeline::eFBOStatus LLPipeline::doAllocateScreenBuffer(U32 resX, U32 resY)
 bool LLPipeline::allocateScreenBuffer(U32 resX, U32 resY, U32 samples)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
-    if (mRT == &mMainRT && sReflectionProbesEnabled)
+    if (mRT == &mMainRT)
     { // hacky -- allocate auxillary buffer
-        gCubeSnapshot = TRUE;
-        mReflectionMapManager.initReflectionMaps();
+        if (sReflectionProbesEnabled)
+        {
+            gCubeSnapshot = TRUE;
+            mReflectionMapManager.initReflectionMaps();
+        }
         mRT = &mAuxillaryRT;
         U32 res = mReflectionMapManager.mProbeResolution * 4;  //multiply by 4 because probes will be 16x super sampled
         allocateScreenBuffer(res, res, samples);
@@ -1084,6 +1091,7 @@ void LLPipeline::refreshCachedSettings()
 	CameraFNumber = gSavedSettings.getF32("CameraFNumber");
 	CameraFocalLength = gSavedSettings.getF32("CameraFocalLength");
 	CameraFieldOfView = gSavedSettings.getF32("CameraFieldOfView");
+	RenderLocalLightCount = gSavedSettings.getS32("RenderLocalLightCount");
 	RenderShadowNoise = gSavedSettings.getF32("RenderShadowNoise");
 	RenderShadowBlurSize = gSavedSettings.getF32("RenderShadowBlurSize");
 	RenderSSAOScale = gSavedSettings.getF32("RenderSSAOScale");
@@ -1168,7 +1176,6 @@ void LLPipeline::releaseGLBuffers()
 	releaseLUTBuffers();
 
 	mWaterDis.release();
-    mBake.release();
 	
     mSceneMap.release();
 
@@ -1248,9 +1255,6 @@ void LLPipeline::createGLBuffers()
     stop_glerror();
 	assertInitialized();
 
-    // Use FBO for bake tex
-    mBake.allocate(1024, 1024, GL_RGBA, true); // SL-12781 Build > Upload > Model; 3D Preview
-
 	stop_glerror();
 
 	GLuint resX = gViewerWindow->getWorldViewWidthRaw();
@@ -5358,7 +5362,7 @@ void LLPipeline::calcNearbyLights(LLCamera& camera)
 		return;
 	}
 
-    static LLCachedControl<S32> local_light_count(gSavedSettings, "RenderLocalLightCount", 256);
+    const S32 local_light_count = LLPipeline::RenderLocalLightCount;
 
 	if (local_light_count >= 1)
 	{
@@ -5625,7 +5629,7 @@ void LLPipeline::setupHWLights()
 
 	mLightMovingMask = 0;
 	
-    static LLCachedControl<S32> local_light_count(gSavedSettings, "RenderLocalLightCount", 256);
+    const S32 local_light_count = LLPipeline::RenderLocalLightCount;
 
 	if (local_light_count >= 1)
 	{
@@ -6660,7 +6664,7 @@ void LLPipeline::renderAlphaObjects(bool rigged)
                 LLGLSLShader::sCurBoundShaderPtr->uniform1i(LLShaderMgr::SUN_UP_FACTOR, sun_up);
                 LLGLSLShader::sCurBoundShaderPtr->uniform1f(LLShaderMgr::DEFERRED_SHADOW_TARGET_WIDTH, (float)target_width);
                 LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(ALPHA_BLEND_CUTOFF);
-                mSimplePool->pushRiggedGLTFBatch(*pparams, lastAvatar, lastMeshId);
+                LLRenderPass::pushRiggedGLTFBatch(*pparams, lastAvatar, lastMeshId);
             }
             else
             {
@@ -6686,7 +6690,7 @@ void LLPipeline::renderAlphaObjects(bool rigged)
                 LLGLSLShader::sCurBoundShaderPtr->uniform1i(LLShaderMgr::SUN_UP_FACTOR, sun_up);
                 LLGLSLShader::sCurBoundShaderPtr->uniform1f(LLShaderMgr::DEFERRED_SHADOW_TARGET_WIDTH, (float)target_width);
                 LLGLSLShader::sCurBoundShaderPtr->setMinimumAlpha(ALPHA_BLEND_CUTOFF);
-                mSimplePool->pushGLTFBatch(*pparams);
+                LLRenderPass::pushGLTFBatch(*pparams);
             }
             else
             {
@@ -8011,7 +8015,7 @@ void LLPipeline::renderDeferredLighting()
             unbindDeferredShader(soften_shader);
         }
 
-        static LLCachedControl<S32> local_light_count(gSavedSettings, "RenderLocalLightCount", 256);
+        const S32 local_light_count = LLPipeline::RenderLocalLightCount;
 
         if (local_light_count > 0)
         {
diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h
index 8507e5d5e65..a7e4df363a8 100644
--- a/indra/newview/pipeline.h
+++ b/indra/newview/pipeline.h
@@ -675,6 +675,7 @@ class LLPipeline
     RenderTargetPack mMainRT;
 
     // auxillary 512x512 render target pack
+    // used by reflection probes and dynamic texture bakes
     RenderTargetPack mAuxillaryRT;
 
     // currently used render target pack
@@ -737,7 +738,7 @@ class LLPipeline
 	//water distortion texture (refraction)
 	LLRenderTarget				mWaterDis;
 
-    LLRenderTarget				mBake;
+    static const U32 MAX_BAKE_WIDTH;
 
 	//texture for making the glow
 	LLRenderTarget				mGlow[3];
@@ -1005,6 +1006,7 @@ class LLPipeline
 	static F32 CameraFNumber;
 	static F32 CameraFocalLength;
 	static F32 CameraFieldOfView;
+	static S32 RenderLocalLightCount;
 	static F32 RenderShadowNoise;
 	static F32 RenderShadowBlurSize;
 	static F32 RenderSSAOScale;
-- 
GitLab