diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt
index b24fa5fbfe02c14be4b877d8e4283bb969668ed3..7971068fa22466a2bca41f90dfda883c0658b0f8 100644
--- a/indra/llmessage/CMakeLists.txt
+++ b/indra/llmessage/CMakeLists.txt
@@ -29,6 +29,7 @@ set(llmessage_SOURCE_FILES
     lldispatcher.cpp
     llexperiencecache.cpp
     llfiltersd2xmlrpc.cpp
+    llgenericstreamingmessage.cpp
     llhost.cpp
     llhttpnode.cpp
     llhttpsdhandler.cpp
@@ -113,6 +114,7 @@ set(llmessage_HEADER_FILES
     llextendedstatus.h
     llfiltersd2xmlrpc.h
     llfollowcamparams.h
+    llgenericstreamingmessage.h
     llhost.h
     llhttpnode.h
     llhttpnodeadapter.h
diff --git a/indra/llmessage/llgenericstreamingmessage.cpp b/indra/llmessage/llgenericstreamingmessage.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8627675c54edd74d8ef3f8ed8c88bf962c7754bc
--- /dev/null
+++ b/indra/llmessage/llgenericstreamingmessage.cpp
@@ -0,0 +1,72 @@
+/**
+ * @file llgenericstreamingmessage.cpp
+ * @brief Generic Streaming Message helpers.  Shared between viewer and simulator.
+ *
+ * $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 "linden_common.h"
+
+#include "llgenericstreamingmessage.h"
+
+#include "message.h"
+
+void LLGenericStreamingMessage::send(LLMessageSystem* msg)
+{
+#if 0 // viewer cannot send GenericStreamingMessage
+    msg->newMessageFast(_PREHASH_GenericStreamingMessage);
+
+    if (mData.size() < 1024 * 7)
+    { // disable warning about big messages unless we're sending a REALLY big message
+        msg->tempDisableWarnAboutBigMessage();
+    }
+    else
+    {
+        LL_WARNS("Messaging") << "Attempted to send too large GenericStreamingMessage, dropping." << LL_ENDL;
+        return;
+    }
+
+    msg->nextBlockFast(_PREHASH_MethodData);
+    msg->addU16Fast(_PREHASH_Method, mMethod);
+    msg->nextBlockFast(_PREHASH_DataBlock);
+    msg->addStringFast(_PREHASH_Data, mData.c_str());
+#endif
+}
+
+void LLGenericStreamingMessage::unpack(LLMessageSystem* msg)
+{
+    U16* m = (U16*)&mMethod; // squirrely pass enum as U16 by reference
+    msg->getU16Fast(_PREHASH_MethodData, _PREHASH_Method, *m);
+
+    constexpr int MAX_SIZE = 7 * 1024;
+
+    char buffer[MAX_SIZE];
+
+    // NOTE: don't use getStringFast to avoid 1200 byte truncation
+    U32 size = msg->getSizeFast(_PREHASH_DataBlock, _PREHASH_Data);
+    msg->getBinaryDataFast(_PREHASH_DataBlock, _PREHASH_Data, buffer, size, 0, MAX_SIZE);
+
+    mData.assign(buffer, size);
+}
+
+
+
diff --git a/indra/llmessage/llgenericstreamingmessage.h b/indra/llmessage/llgenericstreamingmessage.h
new file mode 100644
index 0000000000000000000000000000000000000000..9ac9719ea1402a815147a67dd1db38b90c511108
--- /dev/null
+++ b/indra/llmessage/llgenericstreamingmessage.h
@@ -0,0 +1,50 @@
+/**
+ * @file llgenericstreamingmessage.h
+ * @brief Generic Streaming Message helpers.  Shared between viewer and simulator.
+ *
+ * $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 <string>
+#include "stdtypes.h"
+
+class LLMessageSystem;
+
+class LLGenericStreamingMessage
+{
+public:
+    enum Method : U16
+    {
+        METHOD_GLTF_MATERIAL_OVERRIDE = 0x4175,
+        METHOD_UNKNOWN = 0xFFFF,
+    };
+
+    void send(LLMessageSystem* msg);
+    void unpack(LLMessageSystem* msg);
+
+    Method mMethod = METHOD_UNKNOWN;
+    std::string mData;
+};
+
+
diff --git a/indra/llmessage/message_prehash.cpp b/indra/llmessage/message_prehash.cpp
index e82b8e61c9189b11688e8474dfa5a29a51fd5ca9..346082cf68bedd5a223f816a495f735692e2472f 100644
--- a/indra/llmessage/message_prehash.cpp
+++ b/indra/llmessage/message_prehash.cpp
@@ -1432,6 +1432,7 @@ char const* const _PREHASH_IMViaEMail = LLMessageStringTable::getInstance()->get
 char const* const _PREHASH_RentPrice = LLMessageStringTable::getInstance()->getString("RentPrice");
 char const* const _PREHASH_GenericMessage = LLMessageStringTable::getInstance()->getString("GenericMessage");
 char const* const _PREHASH_LargeGenericMessage = LLMessageStringTable::getInstance()->getString("LargeGenericMessage");
+char const* const _PREHASH_GenericStreamingMessage = LLMessageStringTable::getInstance()->getString("GenericStreamingMessage");
 char const* const _PREHASH_ChildAgentAlive = LLMessageStringTable::getInstance()->getString("ChildAgentAlive");
 char const* const _PREHASH_AssetType = LLMessageStringTable::getInstance()->getString("AssetType");
 char const* const _PREHASH_SpawnPointBlock = LLMessageStringTable::getInstance()->getString("SpawnPointBlock");
diff --git a/indra/llmessage/message_prehash.h b/indra/llmessage/message_prehash.h
index 209633c3c91107ec1934736f30e9b29b4dd5a8b6..c58436a1498d285e5e5c3d5c93848773bfb2faad 100644
--- a/indra/llmessage/message_prehash.h
+++ b/indra/llmessage/message_prehash.h
@@ -1433,6 +1433,7 @@ extern char const* const _PREHASH_IMViaEMail;
 extern char const* const _PREHASH_RentPrice;
 extern char const* const _PREHASH_GenericMessage;
 extern char const* const _PREHASH_LargeGenericMessage;
+extern char const* const _PREHASH_GenericStreamingMessage;
 extern char const* const _PREHASH_ChildAgentAlive;
 extern char const* const _PREHASH_AssetType;
 extern char const* const _PREHASH_SpawnPointBlock;
diff --git a/indra/newview/llgltfmateriallist.cpp b/indra/newview/llgltfmateriallist.cpp
index 25368f633a3db9c28d3cfcabd32414e5d13df888..337091f5e13c40d37e37fc1caeb68ba94040dbd4 100644
--- a/indra/newview/llgltfmateriallist.cpp
+++ b/indra/newview/llgltfmateriallist.cpp
@@ -165,9 +165,9 @@ class LLGLTFMaterialOverrideDispatchHandler : public LLDispatchHandler
         //  sides - array of S32 indices of texture entries
         //  gltf_json - array of corresponding Strings of GLTF json for override data
 
-
         LLSD message;
         bool success = true;
+#if 0 //deprecated
         for(const std::string& llsdRaw : strings)
         {
             std::istringstream llsdData(llsdRaw);
@@ -203,6 +203,7 @@ class LLGLTFMaterialOverrideDispatchHandler : public LLDispatchHandler
             applyData(object_override);
         }
 
+#endif
         return success;
     }
 
@@ -218,6 +219,7 @@ class LLGLTFMaterialOverrideDispatchHandler : public LLDispatchHandler
     {
         // Parse the data
 
+#if 0 // DEPRECATED
         LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop");
         LL::WorkQueue::ptr_t general_queue = LL::WorkQueue::getInstance("General");
 
@@ -231,98 +233,92 @@ class LLGLTFMaterialOverrideDispatchHandler : public LLDispatchHandler
 
         if (!object_override.mSides.empty())
         {
-	        // fromJson() is performance heavy offload to a thread.
-	        main_queue->postTo(
-	            general_queue,
-	            [sides=object_override.mSides]() // Work done on general queue
-	        {
-	            std::vector<ReturnData> results;
-
-	            results.reserve(sides.size());
-	            // parse json
-	            std::unordered_map<S32, std::string>::const_iterator iter = sides.begin();
-	            std::unordered_map<S32, std::string>::const_iterator end = sides.end();
-	            while (iter != end)
-	            {
-	                std::string warn_msg, error_msg;
-
-	                ReturnData result;
-
-	                bool success = result.mMaterial.fromJSON(iter->second, warn_msg, error_msg);
-
-	                result.mSuccess = success;
-	                result.mSide = iter->first;
-
-	                if (!success)
-	                {
-	                    LL_WARNS("GLTF") << "failed to parse GLTF override data.  errors: " << error_msg << " | warnings: " << warn_msg << LL_ENDL;
-	                }
-
-	                results.push_back(result);
-	                iter++;
-	            }
-	            return results;
-	        },
-	        [object_id=object_override.mObjectId, this](std::vector<ReturnData> results) // Callback to main thread
-	        {
-	            LLViewerObject * obj = gObjectList.findObject(object_id);
-
-	            if (results.size() > 0 )
-	            {
-	                std::unordered_set<S32> side_set;
-
-	                for (auto const & result : results)
-	                {
-	                    S32 side = result.mSide;
-	                    if (result.mSuccess)
-	                    {
-	                        // copy to heap here because LLTextureEntry is going to take ownership with an LLPointer
-	                        LLGLTFMaterial * material = new LLGLTFMaterial(result.mMaterial);
-
-	                        // flag this side to not be nulled out later
-	                        side_set.insert(side);
-
-	                        if (obj)
-	                        {
-	                            obj->setTEGLTFMaterialOverride(side, material);
-	                        }
-	                    }
-
-	                    // unblock material editor
-	                    if (obj && obj->getTE(side) && obj->getTE(side)->isSelected())
-	                    {
-	                        doSelectionCallbacks(object_id, side);
-	                    }
-	                }
-
-	                if (obj && side_set.size() != obj->getNumTEs())
-	                { // object exists and at least one texture entry needs to have its override data nulled out
-	                    for (int i = 0; i < obj->getNumTEs(); ++i)
-	                    {
-	                        if (side_set.find(i) == side_set.end())
-	                        {
-	                            obj->setTEGLTFMaterialOverride(i, nullptr);
-	                            if (obj->getTE(i) && obj->getTE(i)->isSelected())
-	                            {
-	                                doSelectionCallbacks(object_id, i);
-	                            }
-	                        }
-	                    }
-	                }
-	            }
-	            else if (obj)
-	            { // override list was empty or an error occurred, null out all overrides for this object
-	                for (int i = 0; i < obj->getNumTEs(); ++i)
-	                {
-	                    obj->setTEGLTFMaterialOverride(i, nullptr);
-	                    if (obj->getTE(i) && obj->getTE(i)->isSelected())
-	                    {
-	                        doSelectionCallbacks(obj->getID(), i);
-	                    }
-	                }
-	            }
-	        });
+            // fromJson() is performance heavy offload to a thread.
+            main_queue->postTo(
+                general_queue,
+                [sides=object_override.mSides]() // Work done on general queue
+            {
+                std::vector<ReturnData> results;
+
+                results.reserve(sides.size());
+                // parse json
+                std::unordered_map<S32, LLSD>::const_iterator iter = sides.begin();
+                std::unordered_map<S32, LLSD>::const_iterator end = sides.end();
+                while (iter != end)
+                {
+                    ReturnData result;
+
+                    result.mMaterial.applyOverrideLLSD(iter->second);
+                    
+                    result.mSuccess = true;
+                    result.mSide = iter->first;
+
+                    results.push_back(result);
+                    iter++;
+                }
+                return results;
+            },
+            [object_id=object_override.mObjectId, this](std::vector<ReturnData> results) // Callback to main thread
+            {
+                LLViewerObject * obj = gObjectList.findObject(object_id);
+
+                if (results.size() > 0 )
+                {
+                    std::unordered_set<S32> side_set;
+
+                    for (auto const & result : results)
+                    {
+                        S32 side = result.mSide;
+                        if (result.mSuccess)
+                        {
+                            // copy to heap here because LLTextureEntry is going to take ownership with an LLPointer
+                            LLGLTFMaterial * material = new LLGLTFMaterial(result.mMaterial);
+
+                            // flag this side to not be nulled out later
+                            side_set.insert(side);
+
+                            if (obj)
+                            {
+                                obj->setTEGLTFMaterialOverride(side, material);
+                            }
+                        }
+
+                        // unblock material editor
+                        if (obj && obj->getTE(side) && obj->getTE(side)->isSelected())
+                        {
+                            doSelectionCallbacks(object_id, side);
+                        }
+                    }
+
+                    if (obj && side_set.size() != obj->getNumTEs())
+                    { // object exists and at least one texture entry needs to have its override data nulled out
+                        for (int i = 0; i < obj->getNumTEs(); ++i)
+                        {
+                            if (side_set.find(i) == side_set.end())
+                            {
+                                obj->setTEGLTFMaterialOverride(i, nullptr);
+                                if (obj->getTE(i) && obj->getTE(i)->isSelected())
+                                {
+                                    doSelectionCallbacks(object_id, i);
+                                }
+                            }
+                        }
+                    }
+                }
+                else if (obj)
+                { // override list was empty or an error occurred, null out all overrides for this object
+                    for (int i = 0; i < obj->getNumTEs(); ++i)
+                    {
+                        obj->setTEGLTFMaterialOverride(i, nullptr);
+                        if (obj->getTE(i) && obj->getTE(i)->isSelected())
+                        {
+                            doSelectionCallbacks(obj->getID(), i);
+                        }
+                    }
+                }
+            });
         }
+#endif
     }
 
 private:
@@ -335,6 +331,70 @@ namespace
     LLGLTFMaterialOverrideDispatchHandler handle_gltf_override_message;
 }
 
+void LLGLTFMaterialList::applyOverrideMessage(LLMessageSystem* msg, const std::string& data_in)
+{
+    std::istringstream str(data_in);
+
+    LLSD data;
+
+    LLSDSerialize::fromNotation(data, str, data_in.length());
+
+    const LLHost& host = msg->getSender();
+    
+    LLViewerRegion* region = LLWorld::instance().getRegion(host);
+    llassert(region);
+
+    if (region)
+    {
+        U32 local_id = data.get("id").asInteger();
+        LLUUID id;
+        gObjectList.getUUIDFromLocal(id, local_id, host.getAddress(), host.getPort());
+        LLViewerObject* obj = gObjectList.findObject(id);
+
+        // NOTE: obj may be null if the viewer hasn't heard about the object yet, cache update in any case
+
+        if (obj && gShowObjectUpdates)
+        { // display a cyan blip for override updates when "Show Updates to Objects" enabled
+            LLColor4 color(0.f, 1.f, 1.f, 1.f);
+            gPipeline.addDebugBlip(obj->getPositionAgent(), color);
+        }
+
+        const LLSD& tes = data["te"];
+        const LLSD& od = data["od"];
+
+        if (tes.isArray())
+        {
+            LLGLTFOverrideCacheEntry cache;
+            cache.mLocalId = local_id;
+            cache.mObjectId = id;
+            cache.mRegionHandle = region->getHandle();
+
+            for (int i = 0; i < tes.size(); ++i)
+            {
+                LLGLTFMaterial* mat = new LLGLTFMaterial(); // setTEGLTFMaterialOverride and cache will take ownership
+                mat->applyOverrideLLSD(od[i]);
+
+                S32 te = tes[i].asInteger();
+
+                cache.mSides[te] = od[i];
+                cache.mGLTFMaterial[te] = mat;
+
+                if (obj)
+                {
+                    obj->setTEGLTFMaterialOverride(te, mat);
+                    if (obj->getTE(te) && obj->getTE(te)->isSelected())
+                    {
+                        handle_gltf_override_message.doSelectionCallbacks(id, te);
+                    }
+                }
+            }
+
+            region->cacheFullUpdateGLTFOverride(cache);
+        }
+
+    }
+}
+
 void LLGLTFMaterialList::queueOverrideUpdate(const LLUUID& id, S32 side, LLGLTFMaterial* override_data)
 {
 #if 0
diff --git a/indra/newview/llgltfmateriallist.h b/indra/newview/llgltfmateriallist.h
index 3669ed955005dc1004273f17579d9f8cb6122354..830c35d1f2843babcb93116c1795f6b6392f7ce6 100644
--- a/indra/newview/llgltfmateriallist.h
+++ b/indra/newview/llgltfmateriallist.h
@@ -101,6 +101,9 @@ class LLGLTFMaterialList
 
     static void loadCacheOverrides(const LLGLTFOverrideCacheEntry& override);
 
+    // Apply an override update with the given data
+    void applyOverrideMessage(LLMessageSystem* msg, const std::string& data);
+
 private:
     friend class LLGLTFMaterialOverrideDispatchHandler;
     // save an override update that we got from the simulator for later (for example, if an override arrived for an unknown object)
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 535b3b0047e4ac1445a333ca81e06995a843a797..c5b4bd3ceb4284548013fbb8d17dc70fd7efa412 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -2810,6 +2810,7 @@ void register_viewer_callbacks(LLMessageSystem* msg)
 	msg->setHandlerFuncFast(_PREHASH_InitiateDownload, process_initiate_download);
 	msg->setHandlerFuncFast(_PREHASH_LandStatReply, LLFloaterTopObjects::handle_land_reply);
     msg->setHandlerFuncFast(_PREHASH_GenericMessage, process_generic_message);
+    msg->setHandlerFunc("GenericStreamingMessage", process_generic_streaming_message);
     msg->setHandlerFuncFast(_PREHASH_LargeGenericMessage, process_large_generic_message);
 
 	msg->setHandlerFuncFast(_PREHASH_FeatureDisabled, process_feature_disabled_message);
diff --git a/indra/newview/llviewergenericmessage.cpp b/indra/newview/llviewergenericmessage.cpp
index 53f3bf558118cdb74e87e4a30d69d4a649b8e290..f4a47cdbb7ef2698cc874f9ff8a77f2fdb764044 100644
--- a/indra/newview/llviewergenericmessage.cpp
+++ b/indra/newview/llviewergenericmessage.cpp
@@ -32,9 +32,10 @@
 #include "lldispatcher.h"
 #include "lluuid.h"
 #include "message.h"
+#include "llgenericstreamingmessage.h"
 
 #include "llagent.h"
-
+#include "llgltfmateriallist.h"
 
 LLDispatcher gGenericDispatcher;
 
@@ -92,6 +93,21 @@ void process_generic_message(LLMessageSystem* msg, void**)
 	}
 }
 
+void process_generic_streaming_message(LLMessageSystem* msg, void**)
+{
+    LLGenericStreamingMessage data;
+    data.unpack(msg);
+    switch (data.mMethod)
+    {
+    case LLGenericStreamingMessage::METHOD_GLTF_MATERIAL_OVERRIDE:
+        gGLTFMaterialList.applyOverrideMessage(msg, data.mData);
+        break;
+    default:
+        LL_WARNS_ONCE() << "Received unknown method" << LL_ENDL;
+        break;
+    }
+}
+
 void process_large_generic_message(LLMessageSystem* msg, void**)
 {
     LLUUID agent_id;
diff --git a/indra/newview/llviewergenericmessage.h b/indra/newview/llviewergenericmessage.h
index 170f38a48552931faed09f1bea03b5e350c3092f..96a73a3d5f0a14fd15e2f633d052fd989dac00ba 100644
--- a/indra/newview/llviewergenericmessage.h
+++ b/indra/newview/llviewergenericmessage.h
@@ -38,6 +38,7 @@ void send_generic_message(const std::string& method,
 						  const LLUUID& invoice = LLUUID::null);
 
 void process_generic_message(LLMessageSystem* msg, void**);
+void process_generic_streaming_message(LLMessageSystem* msg, void**);
 void process_large_generic_message(LLMessageSystem* msg, void**);
 
 
diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp
index 1863ae0c2e015e8476548d08dd247ca06893ee2a..ed0734f58fb04d233ee8fea197485135a1ffec53 100755
--- a/indra/newview/llviewerregion.cpp
+++ b/indra/newview/llviewerregion.cpp
@@ -228,7 +228,7 @@ class LLViewerRegionImpl
 	LLVOCacheEntry::vocache_entry_set_t   mVisibleEntries; //must-be-created visible entries wait for objects creation.	
 	LLVOCacheEntry::vocache_entry_priority_list_t mWaitingList; //transient list storing sorted visible entries waiting for object creation.
 	std::set<U32>                          mNonCacheableCreatedList; //list of local ids of all non-cacheable objects
-    LLVOCacheEntry::vocache_gltf_overrides_map_t mGLTFOverridesJson; // for materials
+    LLVOCacheEntry::vocache_gltf_overrides_map_t mGLTFOverridesLLSD; // for materials
 
 	// time?
 	// LRU info?
@@ -833,7 +833,7 @@ void LLViewerRegion::loadObjectCache()
 	{
         LLVOCache & vocache = LLVOCache::instance();
 		vocache.readFromCache(mHandle, mImpl->mCacheID, mImpl->mCacheMap);
-        vocache.readGenericExtrasFromCache(mHandle, mImpl->mCacheID, mImpl->mGLTFOverridesJson);
+        vocache.readGenericExtrasFromCache(mHandle, mImpl->mCacheID, mImpl->mGLTFOverridesLLSD);
 
 		if (mImpl->mCacheMap.empty())
 		{
@@ -863,7 +863,7 @@ void LLViewerRegion::saveObjectCache()
         LLVOCache & instance = LLVOCache::instance();
 
         instance.writeToCache(mHandle, mImpl->mCacheID, mImpl->mCacheMap, mCacheDirty, removal_enabled);
-        instance.writeGenericExtrasToCache(mHandle, mImpl->mCacheID, mImpl->mGLTFOverridesJson, mCacheDirty, removal_enabled);
+        instance.writeGenericExtrasToCache(mHandle, mImpl->mCacheID, mImpl->mGLTFOverridesLLSD, mCacheDirty, removal_enabled);
 		mCacheDirty = FALSE;
 	}
 
@@ -2898,7 +2898,7 @@ LLViewerRegion::eCacheUpdateResult LLViewerRegion::cacheFullUpdate(LLViewerObjec
 void LLViewerRegion::cacheFullUpdateGLTFOverride(const LLGLTFOverrideCacheEntry &override_data)
 {
     U32 local_id = override_data.mLocalId;
-    mImpl->mGLTFOverridesJson[local_id] = override_data;
+    mImpl->mGLTFOverridesLLSD[local_id] = override_data;
 }
 
 LLVOCacheEntry* LLViewerRegion::getCacheEntryForOctree(U32 local_id)
@@ -3986,8 +3986,8 @@ std::string LLViewerRegion::getSimHostName()
 
 void LLViewerRegion::loadCacheMiscExtras(U32 local_id)
 {
-    auto iter = mImpl->mGLTFOverridesJson.find(local_id);
-    if (iter != mImpl->mGLTFOverridesJson.end())
+    auto iter = mImpl->mGLTFOverridesLLSD.find(local_id);
+    if (iter != mImpl->mGLTFOverridesLLSD.end())
     {
         LLGLTFMaterialList::loadCacheOverrides(iter->second);
     }
@@ -3999,8 +3999,8 @@ void LLViewerRegion::applyCacheMiscExtras(LLViewerObject* obj)
     llassert(obj);
 
     U32 local_id = obj->getLocalID();
-    auto iter = mImpl->mGLTFOverridesJson.find(local_id);
-    if (iter != mImpl->mGLTFOverridesJson.end())
+    auto iter = mImpl->mGLTFOverridesLLSD.find(local_id);
+    if (iter != mImpl->mGLTFOverridesLLSD.end())
     {
         llassert(iter->second.mGLTFMaterial.size() == iter->second.mSides.size());
 
diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp
index e1fd2b206ceabac16e84021d03733ba95a076e4c..094e05974f5db61cfa9981c01e2b22b86dabfd7c 100644
--- a/indra/newview/llvocache.cpp
+++ b/indra/newview/llvocache.cpp
@@ -85,40 +85,24 @@ bool LLGLTFOverrideCacheEntry::fromLLSD(const LLSD& data)
 
     // message should be interpreted thusly:
     ///  sides is a list of face indices
-    //   gltf_json is a list of corresponding json
+    //   gltf_llsd is a list of corresponding GLTF override LLSD
     //   any side not represented in "sides" has no override
-    if (data.has("sides") && data.has("gltf_json"))
+    if (data.has("sides") && data.has("gltf_llsd"))
     {
         LLSD const& sides = data.get("sides");
-        LLSD const& gltf_json = data.get("gltf_json");
+        LLSD const& gltf_llsd = data.get("gltf_llsd");
 
-        if (sides.isArray() && gltf_json.isArray() &&
+        if (sides.isArray() && gltf_llsd.isArray() &&
             sides.size() != 0 &&
-            sides.size() == gltf_json.size())
+            sides.size() == gltf_llsd.size())
         {
             for (int i = 0; i < sides.size(); ++i)
             {
                 S32 side_idx = sides[i].asInteger();
-                std::string gltf_json_str = gltf_json[i].asString();
-                mSides[side_idx] = gltf_json_str;
+                mSides[side_idx] = gltf_llsd[i];
                 LLGLTFMaterial* override_mat = new LLGLTFMaterial();
-                std::string error, warn;
-                if (override_mat->fromJSON(gltf_json_str, warn, error))
-                {
-                    mGLTFMaterial[side_idx] = override_mat;
-                }
-                else
-                {
-                    LL_WARNS() << "Invalid GLTF string: \n" << gltf_json_str << LL_ENDL;
-                    if (!error.empty())
-                    {
-                        LL_WARNS() << "Error: " << error << LL_ENDL;
-                    }
-                    if (!warn.empty())
-                    {
-                        LL_WARNS() << "Warning: " << warn << LL_ENDL;
-                    }
-                }
+                override_mat->applyOverrideLLSD(gltf_llsd[i]);
+                mGLTFMaterial[side_idx] = override_mat;
             }
         }
         else
@@ -157,7 +141,7 @@ LLSD LLGLTFOverrideCacheEntry::toLLSD() const
         // check that mSides and mGLTFMaterial have exactly the same keys present
         llassert(mGLTFMaterial.count(side.first) == 1);
         data["sides"].append(LLSD::Integer(side.first));
-        data["gltf_json"].append(side.second);
+        data["gltf_llsd"].append(side.second);
     }
 
     return data;
diff --git a/indra/newview/llvocache.h b/indra/newview/llvocache.h
index c0f4b3ae6a5d817e0ac07af7ea3c219a9fe72795..888026c4ce3c068a8de1d327fdd217d6fc56ddd3 100644
--- a/indra/newview/llvocache.h
+++ b/indra/newview/llvocache.h
@@ -48,7 +48,7 @@ class LLGLTFOverrideCacheEntry
 
     LLUUID mObjectId;
     U32    mLocalId = 0;
-    std::unordered_map<S32, std::string> mSides; //json per side
+    std::unordered_map<S32, LLSD> mSides; //override LLSD per side
     std::unordered_map<S32, LLPointer<LLGLTFMaterial> > mGLTFMaterial; //GLTF material per side
     U64 mRegionHandle = 0;
 };
diff --git a/scripts/messages/message_template.msg b/scripts/messages/message_template.msg
index 714da02fe6e3940b025b4908dddb9b12f9d70292..7f317dae426dd37fa0c9d6d76e8611a0944319aa 100755
--- a/scripts/messages/message_template.msg
+++ b/scripts/messages/message_template.msg
@@ -5807,6 +5807,25 @@ version 2.0
 	}
 }
 
+// GenericStreamingMessage
+// Optimized generic message for streaming arbitrary data to viewer
+// Avoid payloads over 7KB (8KB ceiling)
+// Method -- magic number indicating method to use to decode payload:
+//      0x4175 - GLTF material override data
+// Payload -- data to be decoded
+{
+    GenericStreamingMessage High 31 Trusted Unencoded
+    {
+        MethodData Single
+        { Method    U16 }
+    }
+
+    {
+        DataBlock Single
+        { Data Variable 2 }
+    }
+}
+
 // LargeGenericMessage
 // Similar to the above messages, but can handle larger payloads and serialized 
 // LLSD.  Uses HTTP transport