diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index ca8b5e946f4ad17cc91e946b100b49344d691878..4dbf1282c4acef3bad30c7f9f2e4f94627d3f366 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -29,6 +29,7 @@ include_directories(
 #     ${LLCOMMON_LIBRARIES})
 
 set(llcommon_SOURCE_FILES
+    commoncontrol.cpp
     indra_constants.cpp
     llallocator.cpp
     llallocator_heap_profile.cpp
@@ -129,6 +130,7 @@ set(llcommon_HEADER_FILES
     CMakeLists.txt
 
     chrono.h
+    commoncontrol.h
     ctype_workaround.h
     fix_macros.h
     indra_constants.h
diff --git a/indra/llcommon/commoncontrol.cpp b/indra/llcommon/commoncontrol.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81e66baf8c2f026928b809e3d4ffc805d86aad92
--- /dev/null
+++ b/indra/llcommon/commoncontrol.cpp
@@ -0,0 +1,106 @@
+/**
+ * @file   commoncontrol.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-08
+ * @brief  Implementation for commoncontrol.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "commoncontrol.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llevents.h"
+#include "llsdutil.h"
+
+LLSD LL::CommonControl::access(const LLSD& params)
+{
+    // We can't actually introduce a link-time dependency on llxml, or on any
+    // global LLControlGroup (*koff* gSavedSettings *koff*) but we can issue a
+    // runtime query. If we're running as part of a viewer with
+    // LLViewerControlListener, we can use that to interact with any
+    // instantiated LLControGroup.
+    LLSD response;
+    {
+        LLEventStream reply("reply");
+        LLTempBoundListener connection = reply.listen("listener",
+                     [&response] (const LLSD& event)
+                     {
+                         response = event;
+                         return false;
+                     });
+        LLSD rparams{ params };
+        rparams["reply"] = reply.getName();
+        LLEventPumps::instance().obtain("LLViewerControl").post(rparams);
+    }
+    // LLViewerControlListener responds immediately. If it's listening at all,
+    // it will already have set response.
+    if (! response.isDefined())
+    {
+        LLTHROW(NoListener("No LLViewerControl listener instantiated"));
+    }
+    LLSD error{ response["error"] };
+    if (error.isDefined())
+    {
+        LLTHROW(ParamError(error));
+    }
+    response.erase("error");
+    response.erase("reqid");
+    return response;
+}
+
+/// set control group.key to defined default value
+LLSD LL::CommonControl::set_default(const std::string& group, const std::string& key)
+{
+    return access(llsd::map("op", "set",
+                            "group", group, "key", key))["value"];
+}
+
+/// set control group.key to specified value
+LLSD LL::CommonControl::set(const std::string& group, const std::string& key, const LLSD& value)
+{
+    return access(llsd::map("op", "set",
+                            "group", group, "key", key, "value", value))["value"];
+}
+
+/// toggle boolean control group.key
+LLSD LL::CommonControl::toggle(const std::string& group, const std::string& key)
+{
+    return access(llsd::map("op", "toggle",
+                            "group", group, "key", key))["value"];
+}
+
+/// get the definition for control group.key, (! isDefined()) if bad
+/// ["name"], ["type"], ["value"], ["comment"]
+LLSD LL::CommonControl::get_def(const std::string& group, const std::string& key)
+{
+    return access(llsd::map("op", "get",
+                            "group", group, "key", key));
+}
+
+/// get the value of control group.key
+LLSD LL::CommonControl::get(const std::string& group, const std::string& key)
+{
+    return access(llsd::map("op", "get",
+                            "group", group, "key", key))["value"];
+}
+
+/// get defined groups
+std::vector<std::string> LL::CommonControl::get_groups()
+{
+    auto groups{ access(llsd::map("op", "groups"))["groups"] };
+    return { groups.beginArray(), groups.endArray() };
+}
+
+/// get definitions for all variables in group
+LLSD LL::CommonControl::get_vars(const std::string& group)
+{
+    return access(llsd::map("op", "vars", "group", group))["vars"];
+}
diff --git a/indra/llcommon/commoncontrol.h b/indra/llcommon/commoncontrol.h
new file mode 100644
index 0000000000000000000000000000000000000000..07d4a45ac5adc15351a847398b5711d7c119e359
--- /dev/null
+++ b/indra/llcommon/commoncontrol.h
@@ -0,0 +1,75 @@
+/**
+ * @file   commoncontrol.h
+ * @author Nat Goodspeed
+ * @date   2022-06-08
+ * @brief  Access LLViewerControl LLEventAPI, if process has one.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_COMMONCONTROL_H)
+#define LL_COMMONCONTROL_H
+
+#include <vector>
+#include "llexception.h"
+#include "llsd.h"
+
+namespace LL
+{
+    class CommonControl
+    {
+    public:
+        struct Error: public LLException
+        {
+            Error(const std::string& what): LLException(what) {}
+        };
+
+        /// Exception thrown if there's no LLViewerControl LLEventAPI
+        struct NoListener: public Error
+        {
+            NoListener(const std::string& what): Error(what) {}
+        };
+
+        struct ParamError: public Error
+        {
+            ParamError(const std::string& what): Error(what) {}
+        };
+
+        /// set control group.key to defined default value
+        static
+        LLSD set_default(const std::string& group, const std::string& key);
+
+        /// set control group.key to specified value
+        static
+        LLSD set(const std::string& group, const std::string& key, const LLSD& value);
+
+        /// toggle boolean control group.key
+        static
+        LLSD toggle(const std::string& group, const std::string& key);
+
+        /// get the definition for control group.key, (! isDefined()) if bad
+        /// ["name"], ["type"], ["value"], ["comment"]
+        static
+        LLSD get_def(const std::string& group, const std::string& key);
+
+        /// get the value of control group.key
+        static
+        LLSD get(const std::string& group, const std::string& key);
+
+        /// get defined groups
+        static
+        std::vector<std::string> get_groups();
+
+        /// get definitions for all variables in group
+        static
+        LLSD get_vars(const std::string& group);
+
+    private:
+        static
+        LLSD access(const LLSD& params);
+    };
+} // namespace LL
+
+#endif /* ! defined(LL_COMMONCONTROL_H) */
diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp
index ba914035e200a9ecfaee13c010e2f6fe32863af1..f49dd40a8b3aff3eac28b55ed66f481d35e4d668 100644
--- a/indra/llcommon/threadpool.cpp
+++ b/indra/llcommon/threadpool.cpp
@@ -17,14 +17,17 @@
 // std headers
 // external library headers
 // other Linden headers
+#include "commoncontrol.h"
 #include "llerror.h"
 #include "llevents.h"
+#include "llsd.h"
 #include "stringize.h"
 
 LL::ThreadPool::ThreadPool(const std::string& name, size_t threads, size_t capacity):
+    super(name),
     mQueue(name, capacity),
     mName("ThreadPool:" + name),
-    mThreadCount(threads)
+    mThreadCount(getConfiguredWidth(name, threads))
 {}
 
 void LL::ThreadPool::start()
@@ -86,3 +89,58 @@ void LL::ThreadPool::run()
 {
     mQueue.runUntilClose();
 }
+
+//static
+size_t LL::ThreadPool::getConfiguredWidth(const std::string& name, size_t dft)
+{
+    LLSD poolSizes;
+    try
+    {
+        poolSizes = LL::CommonControl::get("Global", "ThreadPoolSizes");
+        // "ThreadPoolSizes" is actually a map containing the sizes of
+        // interest -- or should be, if this process has an
+        // LLViewerControlListener instance and its settings include
+        // "ThreadPoolSizes". If we failed to retrieve it, perhaps we're in a
+        // program that doesn't define that, or perhaps there's no such
+        // setting, or perhaps we're asking too early, before the LLEventAPI
+        // itself has been instantiated. In any of those cases, it seems worth
+        // warning.
+        if (! poolSizes.isDefined())
+        {
+            // Note: we don't warn about absence of an override key for a
+            // particular ThreadPool name, that's fine. This warning is about
+            // complete absence of a ThreadPoolSizes setting, which we expect
+            // in a normal viewer session.
+            LL_WARNS("ThreadPool") << "No 'ThreadPoolSizes' setting for ThreadPool '"
+                                   << name << "'" << LL_ENDL;
+        }
+    }
+    catch (const LL::CommonControl::Error& exc)
+    {
+        // We don't want ThreadPool to *require* LLViewerControlListener.
+        // Just log it and carry on.
+        LL_WARNS("ThreadPool") << "Can't check 'ThreadPoolSizes': " << exc.what() << LL_ENDL;
+    }
+
+    LL_DEBUGS("ThreadPool") << "ThreadPoolSizes = " << poolSizes << LL_ENDL;
+    // LLSD treats an undefined value as an empty map when asked to retrieve a
+    // key, so we don't need this to be conditional.
+    LLSD sizeSpec{ poolSizes[name] };
+    // We retrieve sizeSpec as LLSD, rather than immediately as LLSD::Integer,
+    // so we can distinguish the case when it's undefined.
+    return sizeSpec.isInteger() ? sizeSpec.asInteger() : dft;
+}
+
+//static
+size_t LL::ThreadPool::getWidth(const std::string& name, size_t dft)
+{
+    auto instance{ getInstance(name) };
+    if (instance)
+    {
+        return instance->getWidth();
+    }
+    else
+    {
+        return getConfiguredWidth(name, dft);
+    }
+}
diff --git a/indra/llcommon/threadpool.h b/indra/llcommon/threadpool.h
index b79c9b90903e6cec030608f5dc097bf5cba00575..b49d511257b627fd4176f49564d10399bb4d3544 100644
--- a/indra/llcommon/threadpool.h
+++ b/indra/llcommon/threadpool.h
@@ -22,14 +22,25 @@
 namespace LL
 {
 
-    class ThreadPool
+    class ThreadPool: public LLInstanceTracker<ThreadPool, std::string>
     {
+    private:
+        using super = LLInstanceTracker<ThreadPool, std::string>;
     public:
         /**
          * Pass ThreadPool a string name. This can be used to look up the
          * relevant WorkQueue.
+         *
+         * The number of threads you pass sets the compile-time default. But
+         * if the user has overridden the LLSD map in the "ThreadPoolSizes"
+         * setting with a key matching this ThreadPool name, that setting
+         * overrides this parameter.
+         *
+         * Pass an explicit capacity to limit the size of the queue.
+         * Constraining the queue can cause a submitter to block. Do not
+         * constrain any ThreadPool accepting work from the main thread.
          */
-        ThreadPool(const std::string& name, size_t threads=1, size_t capacity=1024);
+        ThreadPool(const std::string& name, size_t threads=1, size_t capacity=1024*1024);
         virtual ~ThreadPool();
 
         /**
@@ -57,6 +68,25 @@ namespace LL
          */
         virtual void run();
 
+        /**
+         * getConfiguredWidth() returns the setting, if any, for the specified
+         * ThreadPool name. Returns dft if the "ThreadPoolSizes" map does not
+         * contain the specified name.
+         */
+        static
+        size_t getConfiguredWidth(const std::string& name, size_t dft=0);
+
+        /**
+         * This getWidth() returns the width of the instantiated ThreadPool
+         * with the specified name, if any. If no instance exists, returns its
+         * getConfiguredWidth() if any. If there's no instance and no relevant
+         * override, return dft. Presumably dft should match the threads
+         * parameter passed to the ThreadPool constructor call that will
+         * eventually instantiate the ThreadPool with that name.
+         */
+        static
+        size_t getWidth(const std::string& name, size_t dft);
+
     private:
         void run(const std::string& name);
 
diff --git a/indra/llimage/llimageworker.cpp b/indra/llimage/llimageworker.cpp
index d8503396d78aefee51c86ced0b8cbc32bff830af..0093958e6ddf6d0365a0fbeb9ff48d367b417b78 100644
--- a/indra/llimage/llimageworker.cpp
+++ b/indra/llimage/llimageworker.cpp
@@ -28,44 +28,88 @@
 
 #include "llimageworker.h"
 #include "llimagedxt.h"
+#include "threadpool.h"
+
+/*--------------------------------------------------------------------------*/
+class ImageRequest
+{
+public:
+	ImageRequest(const LLPointer<LLImageFormatted>& image,
+				 S32 discard, BOOL needs_aux,
+				 const LLPointer<LLImageDecodeThread::Responder>& responder);
+	virtual ~ImageRequest();
+
+	/*virtual*/ bool processRequest();
+	/*virtual*/ void finishRequest(bool completed);
+
+private:
+	// LLPointers stored in ImageRequest MUST be LLPointer instances rather
+	// than references: we need to increment the refcount when storing these.
+	// input
+	LLPointer<LLImageFormatted> mFormattedImage;
+	S32 mDiscardLevel;
+	BOOL mNeedsAux;
+	// output
+	LLPointer<LLImageRaw> mDecodedImageRaw;
+	LLPointer<LLImageRaw> mDecodedImageAux;
+	BOOL mDecodedRaw;
+	BOOL mDecodedAux;
+	LLPointer<LLImageDecodeThread::Responder> mResponder;
+};
+
 
 //----------------------------------------------------------------------------
 
 // MAIN THREAD
-LLImageDecodeThread::LLImageDecodeThread(bool threaded)
-	: LLQueuedThread("imagedecode", threaded)
+LLImageDecodeThread::LLImageDecodeThread(bool /*threaded*/)
 {
-	mCreationMutex = new LLMutex();
+    mThreadPool.reset(new LL::ThreadPool("ImageDecode", 8));
+    mThreadPool->start();
 }
 
 //virtual 
 LLImageDecodeThread::~LLImageDecodeThread()
-{
-	delete mCreationMutex ;
-}
+{}
 
 // MAIN THREAD
 // virtual
 S32 LLImageDecodeThread::update(F32 max_time_ms)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
-	S32 res = LLQueuedThread::update(max_time_ms);
-	return res;
+    return getPending();
 }
 
-LLImageDecodeThread::handle_t LLImageDecodeThread::decodeImage(LLImageFormatted* image, 
-	S32 discard, BOOL needs_aux, Responder* responder)
+S32 LLImageDecodeThread::getPending()
 {
-    LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
-	handle_t handle = generateHandle();
+    return mThreadPool->getQueue().size();
+}
 
-    ImageRequest* req = new ImageRequest(handle, image,
-        discard, needs_aux,
-        responder);
+LLImageDecodeThread::handle_t LLImageDecodeThread::decodeImage(
+    const LLPointer<LLImageFormatted>& image, 
+    S32 discard,
+    BOOL needs_aux,
+    const LLPointer<LLImageDecodeThread::Responder>& responder)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
 
-    addRequest(req);
+    // Instantiate the ImageRequest right in the lambda, why not?
+    mThreadPool->getQueue().post(
+        [req = ImageRequest(image, discard, needs_aux, responder)]
+        () mutable
+        {
+            auto done = req.processRequest();
+            req.finishRequest(done);
+        });
+
+    // It's important to our consumer (LLTextureFetchWorker) that we return a
+    // nonzero handle. It is NOT important that the nonzero handle be unique:
+    // nothing is ever done with it except to compare it to zero, or zero it.
+    return 17;
+}
 
-	return handle;
+void LLImageDecodeThread::shutdown()
+{
+    mThreadPool->close();
 }
 
 LLImageDecodeThread::Responder::~Responder()
@@ -74,11 +118,10 @@ LLImageDecodeThread::Responder::~Responder()
 
 //----------------------------------------------------------------------------
 
-LLImageDecodeThread::ImageRequest::ImageRequest(handle_t handle, LLImageFormatted* image, 
-												S32 discard, BOOL needs_aux,
-												LLImageDecodeThread::Responder* responder)
-	: LLQueuedThread::QueuedRequest(handle, FLAG_AUTO_COMPLETE),
-	  mFormattedImage(image),
+ImageRequest::ImageRequest(const LLPointer<LLImageFormatted>& image, 
+							S32 discard, BOOL needs_aux,
+							const LLPointer<LLImageDecodeThread::Responder>& responder)
+	: mFormattedImage(image),
 	  mDiscardLevel(discard),
 	  mNeedsAux(needs_aux),
 	  mDecodedRaw(FALSE),
@@ -87,7 +130,7 @@ LLImageDecodeThread::ImageRequest::ImageRequest(handle_t handle, LLImageFormatte
 {
 }
 
-LLImageDecodeThread::ImageRequest::~ImageRequest()
+ImageRequest::~ImageRequest()
 {
 	mDecodedImageRaw = NULL;
 	mDecodedImageAux = NULL;
@@ -98,7 +141,7 @@ LLImageDecodeThread::ImageRequest::~ImageRequest()
 
 
 // Returns true when done, whether or not decode was successful.
-bool LLImageDecodeThread::ImageRequest::processRequest()
+bool ImageRequest::processRequest()
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
 	const F32 decode_time_slice = 0.f; //disable time slicing
@@ -125,7 +168,7 @@ bool LLImageDecodeThread::ImageRequest::processRequest()
 											  mFormattedImage->getHeight(),
 											  mFormattedImage->getComponents());
 		}
-		done = mFormattedImage->decode(mDecodedImageRaw, decode_time_slice); // 1ms
+		done = mFormattedImage->decode(mDecodedImageRaw, decode_time_slice);
 		// some decoders are removing data when task is complete and there were errors
 		mDecodedRaw = done && mDecodedImageRaw->getData();
 	}
@@ -138,14 +181,14 @@ bool LLImageDecodeThread::ImageRequest::processRequest()
 											  mFormattedImage->getHeight(),
 											  1);
 		}
-		done = mFormattedImage->decodeChannels(mDecodedImageAux, decode_time_slice, 4, 4); // 1ms
+		done = mFormattedImage->decodeChannels(mDecodedImageAux, decode_time_slice, 4, 4);
 		mDecodedAux = done && mDecodedImageAux->getData();
 	}
 
 	return done;
 }
 
-void LLImageDecodeThread::ImageRequest::finishRequest(bool completed)
+void ImageRequest::finishRequest(bool completed)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
 	if (mResponder.notNull())
@@ -155,10 +198,3 @@ void LLImageDecodeThread::ImageRequest::finishRequest(bool completed)
 	}
 	// Will automatically be deleted
 }
-
-// Used by unit test only
-// Checks that a responder exists for this instance so that something can happen when completion is reached
-bool LLImageDecodeThread::ImageRequest::tut_isOK()
-{
-	return mResponder.notNull();
-}
diff --git a/indra/llimage/llimageworker.h b/indra/llimage/llimageworker.h
index e0a94d2841a58fa8c7b3c02217b1aa97eb79d71f..18398d9ae2b3a6d5319847c7c39822866d617732 100644
--- a/indra/llimage/llimageworker.h
+++ b/indra/llimage/llimageworker.h
@@ -29,9 +29,13 @@
 
 #include "llimage.h"
 #include "llpointer.h"
-#include "llworkerthread.h"
 
-class LLImageDecodeThread : public LLQueuedThread
+namespace LL
+{
+    class ThreadPool;
+} // namespace LL
+
+class LLImageDecodeThread
 {
 public:
 	class Responder : public LLThreadSafeRefCount
@@ -42,57 +46,24 @@ class LLImageDecodeThread : public LLQueuedThread
 		virtual void completed(bool success, LLImageRaw* raw, LLImageRaw* aux) = 0;
 	};
 
-	class ImageRequest : public LLQueuedThread::QueuedRequest
-	{
-	protected:
-		virtual ~ImageRequest(); // use deleteRequest()
-		
-	public:
-		ImageRequest(handle_t handle, LLImageFormatted* image,
-					 S32 discard, BOOL needs_aux,
-					 LLImageDecodeThread::Responder* responder);
-
-		/*virtual*/ bool processRequest();
-		/*virtual*/ void finishRequest(bool completed);
-
-		// Used by unit tests to check the consitency of the request instance
-		bool tut_isOK();
-		
-	private:
-		// input
-		LLPointer<LLImageFormatted> mFormattedImage;
-		S32 mDiscardLevel;
-		BOOL mNeedsAux;
-		// output
-		LLPointer<LLImageRaw> mDecodedImageRaw;
-		LLPointer<LLImageRaw> mDecodedImageAux;
-		BOOL mDecodedRaw;
-		BOOL mDecodedAux;
-		LLPointer<LLImageDecodeThread::Responder> mResponder;
-	};
-	
 public:
 	LLImageDecodeThread(bool threaded = true);
 	virtual ~LLImageDecodeThread();
 
-	handle_t decodeImage(LLImageFormatted* image,
+	// meant to resemble LLQueuedThread::handle_t
+	typedef U32 handle_t;
+	handle_t decodeImage(const LLPointer<LLImageFormatted>& image,
 						 S32 discard, BOOL needs_aux,
-						 Responder* responder);
+						 const LLPointer<Responder>& responder);
+	S32 getPending();
 	S32 update(F32 max_time_ms);
+	void shutdown();
 
 private:
-	struct creation_info
-	{
-		handle_t handle;
-		LLPointer<LLImageFormatted> image;
-		S32 discard;
-		BOOL needs_aux;
-		LLPointer<Responder> responder;
-		creation_info(handle_t h, LLImageFormatted* i, U32 p, S32 d, BOOL aux, Responder* r)
-			: handle(h), image(i), discard(d), needs_aux(aux), responder(r)
-		{}
-	};
-	LLMutex* mCreationMutex;
+	// As of SL-17483, LLImageDecodeThread is no longer itself an
+	// LLQueuedThread - instead this is the API by which we submit work to the
+	// "ImageDecode" ThreadPool.
+	std::unique_ptr<LL::ThreadPool> mThreadPool;
 };
 
 #endif
diff --git a/indra/llimage/tests/llimageworker_test.cpp b/indra/llimage/tests/llimageworker_test.cpp
index d36d35aba4d3099f5323b6756b2cab69efcd302b..0a97b739b093333c35e50b53225c061cf2b8d407 100644
--- a/indra/llimage/tests/llimageworker_test.cpp
+++ b/indra/llimage/tests/llimageworker_test.cpp
@@ -125,42 +125,11 @@ namespace tut
 		}
 	};
 
-	// Test wrapper declaration : image worker
-	// Note: this class is not meant to be instantiated outside an LLImageDecodeThread instance
-	// but it's not a bad idea to get its public API a good shake as part of a thorough unit test set.
-	// Some gotcha with the destructor though (see below).
-	struct imagerequest_test
-	{
-		// Instance to be tested
-		LLImageDecodeThread::ImageRequest* mRequest;
-		bool done;
-
-		// Constructor and destructor of the test wrapper
-		imagerequest_test()
-		{
-			done = false;
-
-			mRequest = new LLImageDecodeThread::ImageRequest(0, 0,
-											 0, FALSE,
-											 new responder_test(&done));
-		}
-		~imagerequest_test()
-		{
-			// We should delete the object *but*, because its destructor is protected, that cannot be
-			// done from outside an LLImageDecodeThread instance... So we leak memory here... It's fine...
-			//delete mRequest;
-		}
-	};
-
 	// Tut templating thingamagic: test group, object and test instance
 	typedef test_group<imagedecodethread_test> imagedecodethread_t;
 	typedef imagedecodethread_t::object imagedecodethread_object_t;
 	tut::imagedecodethread_t tut_imagedecodethread("LLImageDecodeThread");
 
-	typedef test_group<imagerequest_test> imagerequest_t;
-	typedef imagerequest_t::object imagerequest_object_t;
-	tut::imagerequest_t tut_imagerequest("LLImageRequest");
-
 	// ---------------------------------------------------------------------------------------
 	// Test functions
 	// Notes:
@@ -172,21 +141,6 @@ namespace tut
 	// ---------------------------------------------------------------------------------------
 	// Test the LLImageDecodeThread interface
 	// ---------------------------------------------------------------------------------------
-	//
-	// Note on Unit Testing Queued Thread Classes
-	//
-	// Since methods on such a class are called on a separate loop and that we can't insert tut
-	// ensure() calls in there, we exercise the class with 2 sets of tests:
-	// - 1: Test as a single threaded instance: We declare the class but ask for no thread
-	//   to be spawned (easy with LLThreads since there's a boolean argument on the constructor
-	//   just for that). We can then unit test each public method like we do on a normal class.
-	// - 2: Test as a threaded instance: We let the thread launch and check that its external 
-	//   behavior is as expected (i.e. it runs, can accept a work order and processes
-	//   it). Typically though there's no guarantee that this exercises all the methods of the
-	//   class which is why we also need the previous "non threaded" set of unit tests for
-	//   complete coverage.
-	//
-	// ---------------------------------------------------------------------------------------
 
 	template<> template<>
 	void imagedecodethread_object_t::test<1>()
@@ -211,24 +165,4 @@ namespace tut
 		// Verifies that the responder has now been called
 		ensure("LLImageDecodeThread: threaded work unit not processed", done == true);
 	}
-
-	// ---------------------------------------------------------------------------------------
-	// Test the LLImageDecodeThread::ImageRequest interface
-	// ---------------------------------------------------------------------------------------
-	
-	template<> template<>
-	void imagerequest_object_t::test<1>()
-	{
-		// Test that we start with a correct request at creation
-		ensure("LLImageDecodeThread::ImageRequest::ImageRequest() constructor test failed", mRequest->tut_isOK());
-		bool res = mRequest->processRequest();
-		// Verifies that we processed the request successfully
-		ensure("LLImageDecodeThread::ImageRequest::processRequest() processing request test failed", res == true);
-		// Check that we can call the finishing call safely
-		try {
-			mRequest->finishRequest(false);
-		} catch (...) {
-			fail("LLImageDecodeThread::ImageRequest::finishRequest() test failed");
-		}
-	}
 }
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 46f0e1d69ed438383dbcc2e471802270d1f447a0..ae10142e7a9a081a7cbd584317a3a3825ce70e36 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -2444,10 +2444,8 @@ void LLImageGL::checkActiveThread()
 */  
 
 LLImageGLThread::LLImageGLThread(LLWindow* window)
-    // We want exactly one thread, but a very large capacity: we never want
-    // anyone, especially inner-loop render code, to have to block on post()
-    // because we're full.
-    : ThreadPool("LLImageGL", 1, 1024*1024)
+    // We want exactly one thread.
+    : ThreadPool("LLImageGL", 1)
     , mWindow(window)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp
index da7966023937b326075fe02d10d4d7f2b37889c1..20443988abed3cfb9891aabb3d7b945552508659 100644
--- a/indra/llwindow/llwindowwin32.cpp
+++ b/indra/llwindow/llwindowwin32.cpp
@@ -65,7 +65,6 @@
 #include <d3d9.h>
 #include <dxgi1_4.h>
 
-
 // Require DirectInput version 8
 #define DIRECTINPUT_VERSION 0x0800
 
@@ -4649,23 +4648,34 @@ void LLWindowWin32::LLWindowWin32Thread::updateVRAMUsage()
         mDXGIAdapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &info);
 
         // try to use no more than the available reserve minus 10%
-        U32 target = info.AvailableForReservation / 1024 / 1024;
-        target -= target / 10;
+        U32 target = info.Budget / 1024 / 1024;
+
+        // EXPERIMENTAL
+        // Trying to zero in on a good target usage, code here should be tuned against observed behavior
+        // of various hardware.
+        if (target > 4096)  // if 4GB are installed, try to leave 2GB free 
+        {
+            target -= 2048;
+        }
+        else // if less than 4GB are installed, try not to use more than half of it
+        {
+            target /= 2;
+        }
 
         U32 used_vram = info.CurrentUsage / 1024 / 1024;
 
         mAvailableVRAM = used_vram < target ? target - used_vram : 0;
 
-        /*LL_INFOS() << "\nLocal\nAFR: " << info.AvailableForReservation / 1024 / 1024
+        LL_INFOS("Window") << "\nLocal\nAFR: " << info.AvailableForReservation / 1024 / 1024
             << "\nBudget: " << info.Budget / 1024 / 1024
             << "\nCR: " << info.CurrentReservation / 1024 / 1024
             << "\nCU: " << info.CurrentUsage / 1024 / 1024 << LL_ENDL;
 
         mDXGIAdapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_NON_LOCAL, &info);
-        LL_INFOS() << "\nNon-Local\nAFR: " << info.AvailableForReservation / 1024 / 1024
+        LL_INFOS("Window") << "\nNon-Local\nAFR: " << info.AvailableForReservation / 1024 / 1024
             << "\nBudget: " << info.Budget / 1024 / 1024
             << "\nCR: " << info.CurrentReservation / 1024 / 1024
-            << "\nCU: " << info.CurrentUsage / 1024 / 1024 << LL_ENDL;*/
+            << "\nCU: " << info.CurrentUsage / 1024 / 1024 << LL_ENDL;
     }
     else if (mD3DDevice != NULL)
     { // fallback to D3D9
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index c253eca94e972f32af4c49b1ba137d27c86b19ea..f13ce85495a65a864647a863cd2d76598ad8b19e 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -2506,6 +2506,19 @@ if (LL_TESTS)
     "${test_libs}"
     )
 
+  set(llviewercontrollistener_test_sources
+    llviewercontrollistener.cpp
+    ../llxml/llcontrol.cpp
+    ../llxml/llxmltree.cpp
+    ../llxml/llxmlparser.cpp
+    ../llcommon/commoncontrol.cpp
+    )
+
+  LL_ADD_INTEGRATION_TEST(llviewercontrollistener
+    "${llviewercontrollistener_test_sources}"
+    "${test_libs}"
+    )
+
   LL_ADD_INTEGRATION_TEST(llviewernetwork
      llviewernetwork.cpp
     "${test_libs}"
diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt
index 826f5ce030e080c48cef80aae0c10216f954a285..09a7391e4e373ff475cb5093c88cbbbc7210dc27 100644
--- a/indra/newview/VIEWER_VERSION.txt
+++ b/indra/newview/VIEWER_VERSION.txt
@@ -1 +1 @@
-6.6.0
+6.6.1
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 6426964190975c52476eb063b9239f3514e290bc..bc4945eca5a1940296023dbc9d40003daffc51d8 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -12645,7 +12645,9 @@
       <key>Value</key>
       <map>
         <key>General</key>
-        <integer>4</integer>
+        <integer>1</integer>
+        <key>ImageDecode</key>
+        <integer>9</integer>
       </map>
     </map>
     <key>ThrottleBandwidthKBPS</key>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 0b80d32ac3db5eb3a022590f99d5202309586af9..55c0b31bf6fca3461cadfaf7b6dfc1212edbeec6 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -1548,7 +1548,6 @@ bool LLAppViewer::doFrame()
 			{
 				S32 non_interactive_ms_sleep_time = 100;
 				LLAppViewer::getTextureCache()->pause();
-				LLAppViewer::getImageDecodeThread()->pause();
 				ms_sleep(non_interactive_ms_sleep_time);
 			}
 
@@ -1568,7 +1567,6 @@ bool LLAppViewer::doFrame()
 					ms_sleep(milliseconds_to_sleep);
 					// also pause worker threads during this wait period
 					LLAppViewer::getTextureCache()->pause();
-					LLAppViewer::getImageDecodeThread()->pause();
 				}
 			}
 
@@ -1617,7 +1615,6 @@ bool LLAppViewer::doFrame()
 			{
 				LL_PROFILE_ZONE_NAMED_CATEGORY_APP( "df getTextureCache" )
 				LLAppViewer::getTextureCache()->pause();
-				LLAppViewer::getImageDecodeThread()->pause();
 				LLAppViewer::getTextureFetch()->pause();
 			}
 			if(!total_io_pending) //pause file threads if nothing to process.
@@ -2049,10 +2046,10 @@ bool LLAppViewer::cleanup()
 	sTextureCache->shutdown();
 	sImageDecodeThread->shutdown();
 	sPurgeDiskCacheThread->shutdown();
-    if (mGeneralThreadPool)
-    {
-        mGeneralThreadPool->close();
-    }
+	if (mGeneralThreadPool)
+	{
+		mGeneralThreadPool->close();
+	}
 
 	sTextureFetch->shutDownTextureCacheThread() ;
 	sTextureFetch->shutDownImageDecodeThread() ;
@@ -2173,14 +2170,7 @@ void LLAppViewer::initGeneralThread()
         return;
     }
 
-    LLSD poolSizes{ gSavedSettings.getLLSD("ThreadPoolSizes") };
-    LLSD sizeSpec{ poolSizes["General"] };
-    LLSD::Integer poolSize{ sizeSpec.isInteger() ? sizeSpec.asInteger() : 3 };
-    LL_DEBUGS("ThreadPool") << "Instantiating General pool with "
-        << poolSize << " threads" << LL_ENDL;
-    // We don't want anyone, especially the main thread, to have to block
-    // due to this ThreadPool being full.
-    mGeneralThreadPool = new LL::ThreadPool("General", poolSize, 1024 * 1024);
+    mGeneralThreadPool = new LL::ThreadPool("General", 3);
     mGeneralThreadPool->start();
 }
 
diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp
index 0451bae3c90d3cd2be886fa6e067ba416ae53695..604444b64a191a6cdb2cf3641001c2c30c30e49d 100644
--- a/indra/newview/lltexturefetch.cpp
+++ b/indra/newview/lltexturefetch.cpp
@@ -2113,10 +2113,10 @@ void LLTextureFetchWorker::onCompleted(LLCore::HttpHandle handle, LLCore::HttpRe
 // Threads:  Tmain
 void LLTextureFetchWorker::endWork(S32 param, bool aborted)
 {
-    LL_PROFILE_ZONE_SCOPED;
+	LL_PROFILE_ZONE_SCOPED;
 	if (mDecodeHandle != 0)
 	{
-		mFetcher->mImageDecodeThread->abortRequest(mDecodeHandle, false);
+		// LL::ThreadPool has no operation to cancel a particular work item
 		mDecodeHandle = 0;
 	}
 	mFormattedImage = NULL;
@@ -3176,7 +3176,7 @@ void LLTextureFetch::shutDownImageDecodeThread()
 {
 	if(mImageDecodeThread)
 	{
-		llassert_always(mImageDecodeThread->isQuitting() || mImageDecodeThread->isStopped()) ;
+		delete mImageDecodeThread;
 		mImageDecodeThread = NULL ;
 	}
 }
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index a6e7b01df7ab017748b2854c49a0ea27b46ba4e0..3584fffd449645eaffc7d4c953397f03cb29d6b8 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -803,14 +803,8 @@ void LLViewerTexture::addTextureStats(F32 virtual_size, BOOL needs_gltexture) co
 	}
 
 	virtual_size *= sTexelPixelRatio;
-	/*if (!mMaxVirtualSizeResetCounter)
-	{
-		//flag to reset the values because the old values are used.
-		resetMaxVirtualSizeResetCounter();
-		mMaxVirtualSize = virtual_size;
-		mNeedsGLTexture = needs_gltexture;
-	}
-	else*/ if (virtual_size > mMaxVirtualSize)
+
+	if (virtual_size > mMaxVirtualSize)
 	{
 		mMaxVirtualSize = virtual_size;
 	}
@@ -1801,6 +1795,12 @@ void LLViewerFetchedTexture::updateVirtualSize()
         return;
     }
 
+    if (sDesiredDiscardBias > 0.f)
+    {
+        // running out of video memory, don't hold onto high res textures in the background
+        mMaxVirtualSize = 0.f;
+    }
+
 	for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch)
 	{				
 		llassert(mNumFaces[ch] <= mFaceList[ch].size());
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index e27b5caab7797d62543dbed03892691edac23dca..15f20d1d3483116079e9c47d7143ac87cf4fc943 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -5476,7 +5476,6 @@ void LLViewerWindow::stopGL(BOOL save_state)
 
 		// Pause texture decode threads (will get unpaused during main loop)
 		LLAppViewer::getTextureCache()->pause();
-		LLAppViewer::getImageDecodeThread()->pause();
 		LLAppViewer::getTextureFetch()->pause();
 				
 		gSky.destroyGL();
diff --git a/indra/newview/tests/llviewercontrollistener_test.cpp b/indra/newview/tests/llviewercontrollistener_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d100ef984b51aab980cff5e58043c51d04afc02
--- /dev/null
+++ b/indra/newview/tests/llviewercontrollistener_test.cpp
@@ -0,0 +1,174 @@
+/**
+ * @file   llviewercontrollistener_test.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-09
+ * @brief  Test for llviewercontrollistener.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "llviewerprecompiledheaders.h"
+// associated header
+#include "llviewercontrollistener.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "../test/catch_and_store_what_in.h" // catch_what()
+#include "commoncontrol.h"
+#include "llcontrol.h"              // LLControlGroup
+#include "llviewercontrollistener.h"
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    void ensure_contains(const std::string& msg, const std::string& substr)
+    {
+        ensure_contains("Exception does not contain " + substr, msg, substr);
+    }
+
+    struct llviewercontrollistener_data
+    {
+        LLControlGroup Global{"FakeGlobal"};
+
+        llviewercontrollistener_data()
+        {
+            Global.declareString("strvar", "woof", "string variable");
+            // together we will stroll the boolvar, ma cherie
+            Global.declareBOOL("boolvar",  TRUE, "bool variable");
+        }
+    };
+    typedef test_group<llviewercontrollistener_data> llviewercontrollistener_group;
+    typedef llviewercontrollistener_group::object object;
+    llviewercontrollistener_group llviewercontrollistenergrp("llviewercontrollistener");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("CommonControl no listener");
+        // Not implemented: the linker drags in LLViewerControlListener when
+        // we bring in LLViewerControl.
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("CommonControl bad group");
+        std::string threw{ catch_what<LL::CommonControl::ParamError>(
+                [](){ LL::CommonControl::get("Nonexistent", "Variable"); }) };
+        ensure_contains(threw, "group");
+        ensure_contains(threw, "Nonexistent");
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("CommonControl bad variable");
+        std::string threw{ catch_what<LL::CommonControl::ParamError>(
+                [](){ LL::CommonControl::get("FakeGlobal", "Nonexistent"); }) };
+        ensure_contains(threw, "key");
+        ensure_contains(threw, "Nonexistent");
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("CommonControl toggle string");
+        std::string threw{ catch_what<LL::CommonControl::ParamError>(
+                [](){ LL::CommonControl::toggle("FakeGlobal", "strvar"); }) };
+        ensure_contains(threw, "non-boolean");
+        ensure_contains(threw, "strvar");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("CommonControl list bad group");
+        std::string threw{ catch_what<LL::CommonControl::ParamError>(
+                [](){ LL::CommonControl::get_vars("Nonexistent"); }) };
+        ensure_contains(threw, "group");
+        ensure_contains(threw, "Nonexistent");
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("CommonControl get");
+        auto strvar{ LL::CommonControl::get("FakeGlobal", "strvar") };
+        ensure_equals(strvar, "woof");
+        auto boolvar{ LL::CommonControl::get("FakeGlobal", "boolvar") };
+        ensure(boolvar);
+    }
+
+    template<> template<>
+    void object::test<7>()
+    {
+        set_test_name("CommonControl set, set_default, toggle");
+
+        std::string newstr{ LL::CommonControl::set("FakeGlobal", "strvar", "mouse").asString() };
+        ensure_equals(newstr, "mouse");
+        ensure_equals(LL::CommonControl::get("FakeGlobal", "strvar").asString(), "mouse");
+        ensure_equals(LL::CommonControl::set_default("FakeGlobal", "strvar").asString(), "woof");
+
+        bool newbool{ LL::CommonControl::set("FakeGlobal", "boolvar", false) };
+        ensure(! newbool);
+        ensure(! LL::CommonControl::get("FakeGlobal", "boolvar").asBoolean());
+        ensure(LL::CommonControl::set_default("FakeGlobal", "boolvar").asBoolean());
+        ensure(! LL::CommonControl::toggle("FakeGlobal", "boolvar").asBoolean());
+    }
+
+    template<> template<>
+    void object::test<8>()
+    {
+        set_test_name("CommonControl get_def");
+        LLSD def{ LL::CommonControl::get_def("FakeGlobal", "strvar") };
+        ensure_equals(
+            def,
+            llsd::map("name", "strvar",
+                      "type", "String",
+                      "value", "woof",
+                      "comment", "string variable"));
+    }
+
+    template<> template<>
+    void object::test<9>()
+    {
+        set_test_name("CommonControl get_groups");
+        std::vector<std::string> groups{ LL::CommonControl::get_groups() };
+        ensure_equals(groups.size(), 1);
+        ensure_equals(groups[0], "FakeGlobal");
+    }
+
+    template<> template<>
+    void object::test<10>()
+    {
+        set_test_name("CommonControl get_vars");
+        LLSD vars{ LL::CommonControl::get_vars("FakeGlobal") };
+        // convert from array (unpredictable order) to map
+        LLSD varsmap{ LLSD::emptyMap() };
+        for (auto& var : llsd::inArray(vars))
+        {
+            varsmap[var["name"].asString()] = var;
+        }
+        // comparing maps is order-insensitive
+        ensure_equals(
+            varsmap,
+            llsd::map(
+                "strvar",
+                llsd::map("name", "strvar",
+                          "type", "String",
+                          "value", "woof",
+                          "comment", "string variable"),
+                "boolvar",
+                llsd::map("name", "boolvar",
+                          "type", "Boolean",
+                          "value", TRUE,
+                          "comment", "bool variable")));
+    }
+} // namespace tut
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index bb48216b2b388499dad917d1e2d4eb8bf3853532..28f25087ac5476644dafab2c003efe2a5dc3a06e 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -401,7 +401,7 @@ class LLTCTestCallback : public LLTestCallback
 	{
 		// Per http://confluence.jetbrains.net/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ServiceMessages
 		std::string result;
-		BOOST_FOREACH(char c, str)
+		for (char c : str)
 		{
 			switch (c)
 			{