From 3400e5fd302c0d9dea6386c4d5bf38876f2cc287 Mon Sep 17 00:00:00 2001
From: Dave Parks <davep@lindenlab.com>
Date: Mon, 16 May 2022 17:21:08 +0000
Subject: [PATCH] SL-17284 Reflection probe tuning and optimization take 1

---
 autobuild.xml                                 |   4 +-
 indra/llrender/CMakeLists.txt                 |   2 +
 indra/llrender/llcubemap.cpp                  |  10 +-
 indra/llrender/llcubemaparray.cpp             | 113 ++++
 indra/llrender/llcubemaparray.h               |  60 ++
 indra/llrender/llgl.cpp                       |  33 +-
 indra/llrender/llgl.h                         |   1 +
 indra/llrender/llglheaders.h                  |   9 +
 indra/llrender/llglslshader.cpp               |  61 ++-
 indra/llrender/llglslshader.h                 |   3 +
 indra/llrender/llimagegl.cpp                  |  51 +-
 indra/llrender/llrender.cpp                   |   4 +
 indra/llrender/llrender.h                     |   3 +-
 indra/llrender/llshadermgr.cpp                |  13 +-
 indra/llrender/llshadermgr.h                  |   2 +-
 indra/newview/app_settings/settings.xml       |  11 +
 .../shaders/class1/deferred/aoUtil.glsl       |  14 +-
 .../shaders/class1/deferred/blurLightF.glsl   |  29 +-
 .../class1/interface/reflectionmipF.glsl      |  75 +++
 .../shaders/class2/deferred/softenLightF.glsl | 274 ++++++++--
 indra/newview/llagentcamera.cpp               |   6 +
 indra/newview/lldrawpoolalpha.cpp             |   4 +-
 indra/newview/lldrawpoolbump.cpp              |   1 +
 indra/newview/lldrawpoolpbropaque.cpp         |   2 +-
 indra/newview/llreflectionmap.cpp             | 171 +++++-
 indra/newview/llreflectionmap.h               |  51 +-
 indra/newview/llreflectionmapmanager.cpp      | 516 +++++++++++++++++-
 indra/newview/llreflectionmapmanager.h        |  77 ++-
 indra/newview/llspatialpartition.cpp          | 117 +++-
 indra/newview/llspatialpartition.h            |  19 +
 indra/newview/llviewerdisplay.cpp             | 121 +++-
 indra/newview/llviewermenu.cpp                |  10 +-
 indra/newview/llviewershadermgr.cpp           |  20 +-
 indra/newview/llviewershadermgr.h             |   1 +
 indra/newview/llviewerwindow.cpp              |  99 ++--
 indra/newview/llviewerwindow.h                |   6 +-
 indra/newview/llvovolume.cpp                  |  10 +-
 indra/newview/llworld.cpp                     |   3 +-
 indra/newview/pipeline.cpp                    | 505 +++++++++--------
 indra/newview/pipeline.h                      |   3 +-
 .../skins/default/xui/en/menu_viewer.xml      |  16 +-
 41 files changed, 2062 insertions(+), 468 deletions(-)
 create mode 100644 indra/llrender/llcubemaparray.cpp
 create mode 100644 indra/llrender/llcubemaparray.h
 create mode 100644 indra/newview/app_settings/shaders/class1/interface/reflectionmipF.glsl

diff --git a/autobuild.xml b/autobuild.xml
index 9185b8af22a..fa3f7b2743e 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -1010,9 +1010,9 @@
             <key>archive</key>
             <map>
               <key>hash</key>
-              <string>1bd3214ac23474ea4c869e386970a1be</string>
+              <string>c5e9a59c7cf03c88a5cb4ab0a9c21091</string>
               <key>url</key>
-              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/54835/510029/glext-68-darwin64-538965.tar.bz2</string>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/99835/880141/glext-68-darwin64-571812.tar.bz2</string>
             </map>
             <key>name</key>
             <string>darwin64</string>
diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt
index baab09a1048..a30f47f1d9e 100644
--- a/indra/llrender/CMakeLists.txt
+++ b/indra/llrender/CMakeLists.txt
@@ -31,6 +31,7 @@ include_directories(SYSTEM
 set(llrender_SOURCE_FILES
     llatmosphere.cpp
     llcubemap.cpp
+    llcubemaparray.cpp
     llfontbitmapcache.cpp
     llfontfreetype.cpp
     llfontgl.cpp
@@ -58,6 +59,7 @@ set(llrender_HEADER_FILES
 
     llatmosphere.h
     llcubemap.h
+    llcubemaparray.h
     llfontgl.h
     llfontfreetype.h
     llfontbitmapcache.h
diff --git a/indra/llrender/llcubemap.cpp b/indra/llrender/llcubemap.cpp
index 6d872d0763a..473447ad727 100644
--- a/indra/llrender/llcubemap.cpp
+++ b/indra/llrender/llcubemap.cpp
@@ -219,13 +219,17 @@ void LLCubeMap::initEnvironmentMap(const std::vector<LLPointer<LLImageRaw> >& ra
 
 void LLCubeMap::generateMipMaps()
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+
     mImages[0]->setUseMipMaps(TRUE);
     mImages[0]->setHasMipMaps(TRUE);
     enableTexture(0);
     bind();
-    mImages[0]->setFilteringOption(LLTexUnit::TFO_ANISOTROPIC);
-    glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
-    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
+    mImages[0]->setFilteringOption(LLTexUnit::TFO_BILINEAR);
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("cmgmm - glGenerateMipmap");
+        glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
+    }
     gGL.getTexUnit(0)->disable();
     disable();
 }
diff --git a/indra/llrender/llcubemaparray.cpp b/indra/llrender/llcubemaparray.cpp
new file mode 100644
index 00000000000..e4081c1154f
--- /dev/null
+++ b/indra/llrender/llcubemaparray.cpp
@@ -0,0 +1,113 @@
+/** 
+ * @file llcubemaparray.cpp
+ * @brief LLCubeMap class implementation
+ *
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2022, 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 "llworkerthread.h"
+
+#include "llcubemaparray.h"
+
+#include "v4coloru.h"
+#include "v3math.h"
+#include "v3dmath.h"
+#include "m3math.h"
+#include "m4math.h"
+
+#include "llrender.h"
+#include "llglslshader.h"
+
+#include "llglheaders.h"
+
+//#pragma optimize("", off)
+
+// MUST match order of OpenGL face-layers
+GLenum LLCubeMapArray::sTargets[6] =
+{
+    GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB,
+    GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB,
+    GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB,
+    GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB,
+    GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB,
+    GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB
+};
+
+LLCubeMapArray::LLCubeMapArray()
+	: mTextureStage(0)
+{
+	
+}
+
+LLCubeMapArray::~LLCubeMapArray()
+{
+}
+
+void LLCubeMapArray::allocate(U32 resolution, U32 components, U32 count)
+{
+    U32 texname = 0;
+
+    LLImageGL::generateTextures(1, &texname);
+
+    mImage = new LLImageGL(resolution, resolution, components, TRUE);
+    mImage->setTexName(texname);
+    mImage->setTarget(sTargets[0], LLTexUnit::TT_CUBE_MAP_ARRAY);
+
+    mImage->setUseMipMaps(TRUE);
+    mImage->setHasMipMaps(TRUE);
+
+    bind(0);
+
+    glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY_ARB, 0, GL_RGB, resolution, resolution, count*6, 0,
+        GL_RGB, GL_UNSIGNED_BYTE, nullptr);
+
+    mImage->setAddressMode(LLTexUnit::TAM_CLAMP);
+
+    mImage->setFilteringOption(LLTexUnit::TFO_ANISOTROPIC);
+
+    glGenerateMipmap(GL_TEXTURE_CUBE_MAP_ARRAY_ARB);
+
+    unbind();
+}
+
+void LLCubeMapArray::bind(S32 stage)
+{
+    mTextureStage = stage;
+    gGL.getTexUnit(stage)->bindManual(LLTexUnit::TT_CUBE_MAP_ARRAY, getGLName(), TRUE);
+}
+
+void LLCubeMapArray::unbind()
+{
+    gGL.getTexUnit(mTextureStage)->unbind(LLTexUnit::TT_CUBE_MAP_ARRAY);
+    mTextureStage = -1;
+}
+
+GLuint LLCubeMapArray::getGLName()
+{
+	return mImage->getTexName();
+}
+
+void LLCubeMapArray::destroyGL()
+{
+    mImage = NULL;
+}
diff --git a/indra/llrender/llcubemaparray.h b/indra/llrender/llcubemaparray.h
new file mode 100644
index 00000000000..52e21f1dda5
--- /dev/null
+++ b/indra/llrender/llcubemaparray.h
@@ -0,0 +1,60 @@
+/** 
+ * @file llcubemaparray.h
+ * @brief LLCubeMap class definition
+ *
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2022, 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 "llgl.h"
+
+#include <vector>
+
+class LLVector3;
+
+// Environment map hack!
+class LLCubeMapArray : public LLRefCount
+{
+public:
+	LLCubeMapArray();
+
+    static GLenum sTargets[6];
+
+    // allocate a cube map array 
+    // res - resolution of each cube face
+    // components - number of components per pixel
+    // count - number of cube maps in the array
+    void allocate(U32 res, U32 components, U32 count);
+	void bind(S32 stage);
+    void unbind();
+	
+	GLuint getGLName();
+
+	void destroyGL();
+
+protected:
+	friend class LLTexUnit;
+	~LLCubeMapArray();
+	LLPointer<LLImageGL> mImage;
+	S32 mTextureStage;
+};
diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp
index 639d1fba32a..6023796f7c6 100644
--- a/indra/llrender/llgl.cpp
+++ b/indra/llrender/llgl.cpp
@@ -89,9 +89,10 @@ void APIENTRY gl_debug_callback(GLenum source,
 	if (gGLDebugLoggingEnabled)
 	{
 
-        if (severity != GL_DEBUG_SEVERITY_HIGH_ARB &&
-            severity != GL_DEBUG_SEVERITY_MEDIUM_ARB &&
-            severity != GL_DEBUG_SEVERITY_LOW_ARB)
+        if (severity != GL_DEBUG_SEVERITY_HIGH_ARB // &&
+            //severity != GL_DEBUG_SEVERITY_MEDIUM_ARB &&
+            //severity != GL_DEBUG_SEVERITY_LOW_ARB
+            )
         { //suppress out-of-spec messages sent by nvidia driver (mostly vertexbuffer hints)
             return;
         }
@@ -316,6 +317,15 @@ PFNGLGETUNIFORMIVARBPROC glGetUniformivARB = NULL;
 PFNGLGETSHADERSOURCEARBPROC glGetShaderSourceARB = NULL;
 PFNGLVERTEXATTRIBIPOINTERPROC glVertexAttribIPointer = NULL;
 
+// GL_ARB_uniform_buffer_object
+PFNGLGETUNIFORMINDICESPROC glGetUniformIndices = NULL;
+PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv = NULL;
+PFNGLGETACTIVEUNIFORMNAMEPROC glGetActiveUniformName = NULL;
+PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex = NULL;
+PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv = NULL;
+PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC glGetActiveUniformBlockName = NULL;
+PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding = NULL;
+
 #if LL_WINDOWS
 PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB = NULL;
 #endif
@@ -436,6 +446,7 @@ LLGLManager::LLGLManager() :
 	mHasTextureRectangle(FALSE),
 	mHasTextureMultisample(FALSE),
 	mHasTransformFeedback(FALSE),
+    mHasUniformBufferObject(FALSE),
 	mMaxSampleMaskWords(0),
 	mMaxColorTextureSamples(0),
 	mMaxDepthTextureSamples(0),
@@ -1054,6 +1065,7 @@ void LLGLManager::initExtensions()
 	mHasTextureMultisample = ExtensionExists("GL_ARB_texture_multisample", gGLHExts.mSysExts);
 	mHasDebugOutput = ExtensionExists("GL_ARB_debug_output", gGLHExts.mSysExts);
 	mHasTransformFeedback = mGLVersion >= 4.f ? TRUE : FALSE;
+    mHasUniformBufferObject = ExtensionExists("GL_ARB_uniform_buffer_object", gGLHExts.mSysExts);
 #if !LL_DARWIN
 	mHasPointParameters = ExtensionExists("GL_ARB_point_parameters", gGLHExts.mSysExts);
 #endif
@@ -1286,6 +1298,10 @@ void LLGLManager::initExtensions()
 		mGLMaxVertexRange = 0;
 		mGLMaxIndexRange = 0;
 	}
+
+    // same with glTexImage3D et al
+    glTexImage3D = (PFNGLTEXIMAGE3DPROC)GLH_EXT_GET_PROC_ADDRESS("glTexImage3D");
+    glCopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC)GLH_EXT_GET_PROC_ADDRESS("glCopyTexSubImage3D");
 #endif // !LL_LINUX || LL_LINUX_NV_GL_HEADERS
 #if LL_LINUX_NV_GL_HEADERS
 	// nvidia headers are critically different from mesa-esque
@@ -1319,6 +1335,17 @@ void LLGLManager::initExtensions()
 		glPointParameterfvARB = (PFNGLPOINTPARAMETERFVARBPROC)GLH_EXT_GET_PROC_ADDRESS("glPointParameterfvARB");
 	}
 
+    if (mHasUniformBufferObject)
+    {
+       glGetUniformIndices = (PFNGLGETUNIFORMINDICESPROC)GLH_EXT_GET_PROC_ADDRESS("glGetUniformIndices");
+       glGetActiveUniformsiv = (PFNGLGETACTIVEUNIFORMSIVPROC)GLH_EXT_GET_PROC_ADDRESS("glGetUniformIndices");
+       glGetActiveUniformName = (PFNGLGETACTIVEUNIFORMNAMEPROC)GLH_EXT_GET_PROC_ADDRESS("glGetActiveUniformName");
+       glGetUniformBlockIndex = (PFNGLGETUNIFORMBLOCKINDEXPROC)GLH_EXT_GET_PROC_ADDRESS("glGetUniformBlockIndex");
+       glGetActiveUniformBlockiv = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)GLH_EXT_GET_PROC_ADDRESS("glGetActiveUniformBlockiv");
+       glGetActiveUniformBlockName = (PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)GLH_EXT_GET_PROC_ADDRESS("glGetActiveUniformBlockName");
+       glUniformBlockBinding = (PFNGLUNIFORMBLOCKBINDINGPROC)GLH_EXT_GET_PROC_ADDRESS("glUniformBlockBinding");
+    }
+
     // Assume shader capabilities
     glDeleteObjectARB         = (PFNGLDELETEOBJECTARBPROC) GLH_EXT_GET_PROC_ADDRESS("glDeleteObjectARB");
     glGetHandleARB            = (PFNGLGETHANDLEARBPROC) GLH_EXT_GET_PROC_ADDRESS("glGetHandleARB");
diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h
index 52338364e61..eea53ed01e7 100644
--- a/indra/llrender/llgl.h
+++ b/indra/llrender/llgl.h
@@ -104,6 +104,7 @@ class LLGLManager
 	BOOL mHasTextureRectangle;
 	BOOL mHasTextureMultisample;
 	BOOL mHasTransformFeedback;
+    BOOL mHasUniformBufferObject;
 	S32 mMaxSampleMaskWords;
 	S32 mMaxColorTextureSamples;
 	S32 mMaxDepthTextureSamples;
diff --git a/indra/llrender/llglheaders.h b/indra/llrender/llglheaders.h
index 3d93cc07627..aa429505c18 100644
--- a/indra/llrender/llglheaders.h
+++ b/indra/llrender/llglheaders.h
@@ -564,6 +564,15 @@ extern PFNGLDEBUGMESSAGEINSERTARBPROC glDebugMessageInsertARB;
 extern PFNGLDEBUGMESSAGECALLBACKARBPROC glDebugMessageCallbackARB;
 extern PFNGLGETDEBUGMESSAGELOGARBPROC glGetDebugMessageLogARB;
 
+// GL_ARB_uniform_buffer_object
+extern PFNGLGETUNIFORMINDICESPROC glGetUniformIndices;
+extern PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv;
+extern PFNGLGETACTIVEUNIFORMNAMEPROC glGetActiveUniformName;
+extern PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex;
+extern PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv;
+extern PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC glGetActiveUniformBlockName;
+extern PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding;
+
 #elif LL_DARWIN
 //----------------------------------------------------------------------------
 // LL_DARWIN
diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp
index a52dcd5aa18..0212c605c34 100644
--- a/indra/llrender/llglslshader.cpp
+++ b/indra/llrender/llglslshader.cpp
@@ -780,7 +780,8 @@ GLint LLGLSLShader::mapUniformTextureChannel(GLint location, GLenum type, GLint
     LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER;
 
     if ((type >= GL_SAMPLER_1D_ARB && type <= GL_SAMPLER_2D_RECT_SHADOW_ARB) ||
-        type == GL_SAMPLER_2D_MULTISAMPLE)
+        type == GL_SAMPLER_2D_MULTISAMPLE ||
+        type == GL_SAMPLER_CUBE_MAP_ARRAY_ARB)
     {   //this here is a texture
         GLint ret = mActiveTextureChannels;
         if (size == 1)
@@ -1289,6 +1290,30 @@ void LLGLSLShader::uniform1iv(U32 index, U32 count, const GLint* v)
     }
 }
 
+void LLGLSLShader::uniform4iv(U32 index, U32 count, const GLint* v)
+{
+    if (mProgramObject)
+    {
+        if (mUniform.size() <= index)
+        {
+            LL_SHADER_UNIFORM_ERRS() << "Uniform index out of bounds." << LL_ENDL;
+            return;
+        }
+
+        if (mUniform[index] >= 0)
+        {
+            const auto& iter = mValue.find(mUniform[index]);
+            LLVector4 vec(v[0], v[1], v[2], v[3]);
+            if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1)
+            {
+                glUniform1ivARB(mUniform[index], count, v);
+                mValue[mUniform[index]] = vec;
+            }
+        }
+    }
+}
+
+
 void LLGLSLShader::uniform1fv(U32 index, U32 count, const GLfloat* v)
 {
     if (mProgramObject)
@@ -1526,6 +1551,40 @@ void LLGLSLShader::uniform1i(const LLStaticHashedString& uniform, GLint v)
     }
 }
 
+void LLGLSLShader::uniform1iv(const LLStaticHashedString& uniform, U32 count, const GLint* v)
+{
+    GLint location = getUniformLocation(uniform);
+
+    if (location >= 0)
+    {
+        LLVector4 vec(v[0], 0, 0, 0);
+        const auto& iter = mValue.find(location);
+        if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1)
+        {
+            LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER;
+            glUniform1ivARB(location, count, v);
+            mValue[location] = vec;
+        }
+    }
+}
+
+void LLGLSLShader::uniform4iv(const LLStaticHashedString& uniform, U32 count, const GLint* v)
+{
+    GLint location = getUniformLocation(uniform);
+
+    if (location >= 0)
+    {
+        LLVector4 vec(v[0], v[1], v[2], v[3]);
+        const auto& iter = mValue.find(location);
+        if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1)
+        {
+            LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER;
+            glUniform4ivARB(location, count, v);
+            mValue[location] = vec;
+        }
+    }
+}
+
 void LLGLSLShader::uniform2i(const LLStaticHashedString& uniform, GLint i, GLint j)
 {
     GLint location = getUniformLocation(uniform);
diff --git a/indra/llrender/llglslshader.h b/indra/llrender/llglslshader.h
index 7d6b341d3d1..fe0aaae4674 100644
--- a/indra/llrender/llglslshader.h
+++ b/indra/llrender/llglslshader.h
@@ -178,6 +178,7 @@ class LLGLSLShader
 	void uniform3f(U32 index, GLfloat x, GLfloat y, GLfloat z);
 	void uniform4f(U32 index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
 	void uniform1iv(U32 index, U32 count, const GLint* i);
+    void uniform4iv(U32 index, U32 count, const GLint* i);
 	void uniform1fv(U32 index, U32 count, const GLfloat* v);
 	void uniform2fv(U32 index, U32 count, const GLfloat* v);
 	void uniform3fv(U32 index, U32 count, const GLfloat* v);
@@ -188,6 +189,8 @@ class LLGLSLShader
 	void uniformMatrix3x4fv(U32 index, U32 count, GLboolean transpose, const GLfloat *v);
 	void uniformMatrix4fv(U32 index, U32 count, GLboolean transpose, const GLfloat *v);
 	void uniform1i(const LLStaticHashedString& uniform, GLint i);
+    void uniform1iv(const LLStaticHashedString& uniform, U32 count, const GLint* v);
+    void uniform4iv(const LLStaticHashedString& uniform, U32 count, const GLint* v);
 	void uniform1f(const LLStaticHashedString& uniform, GLfloat v);
 	void uniform2f(const LLStaticHashedString& uniform, GLfloat x, GLfloat y);
 	void uniform3f(const LLStaticHashedString& uniform, GLfloat x, GLfloat y, GLfloat z);
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 9bd3a0a6b0a..f2da859e23d 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -1688,26 +1688,38 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name)
         LL_PROFILE_ZONE_NAMED("cglt - sync");
         if (gGLManager.mHasSync)
         {
-            // post a sync to the main thread (will execute before tex name swap lambda below)
-            // glFlush calls here are partly superstitious and partly backed by observation
-            // on AMD hardware
-            glFlush();
-            auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
-            glFlush();
-            LL::WorkQueue::postMaybe(
-                mMainQueue,
-                [=]()
-                {
-                    LL_PROFILE_ZONE_NAMED("cglt - wait sync");
-                    {
-                        LL_PROFILE_ZONE_NAMED("glWaitSync");
-                        glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
-                    }
+            if (gGLManager.mIsNVIDIA)
+            {
+                // wait for texture upload to finish before notifying main thread
+                // upload is complete
+                auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+                glFlush();
+                glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
+                glDeleteSync(sync);
+            }
+            else
+            {
+                // post a sync to the main thread (will execute before tex name swap lambda below)
+                // glFlush calls here are partly superstitious and partly backed by observation
+                // on AMD hardware
+                glFlush();
+                auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+                glFlush();
+                LL::WorkQueue::postMaybe(
+                    mMainQueue,
+                    [=]()
                     {
-                        LL_PROFILE_ZONE_NAMED("glDeleteSync");
-                        glDeleteSync(sync);
-                    }
-                });
+                        LL_PROFILE_ZONE_NAMED("cglt - wait sync");
+                        {
+                            LL_PROFILE_ZONE_NAMED("glWaitSync");
+                            glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
+                        }
+                        {
+                            LL_PROFILE_ZONE_NAMED("glDeleteSync");
+                            glDeleteSync(sync);
+                        }
+                    });
+            }
         }
         else
         {
@@ -1726,6 +1738,7 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name)
         });
 }
 
+
 void LLImageGL::syncTexName(LLGLuint texname)
 {
     if (texname != 0)
diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp
index a03a27cf949..3adb47f493f 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -71,6 +71,7 @@ static const GLenum sGLTextureType[] =
 	GL_TEXTURE_2D,
 	GL_TEXTURE_RECTANGLE_ARB,
 	GL_TEXTURE_CUBE_MAP_ARB,
+    GL_TEXTURE_CUBE_MAP_ARRAY_ARB,
 	GL_TEXTURE_2D_MULTISAMPLE,
     GL_TEXTURE_3D
 };
@@ -888,6 +889,9 @@ void LLRender::init()
 
     glCullFace(GL_BACK);
 
+    // necessary for reflection maps
+    glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
+
 	if (sGLCoreProfile && !LLVertexBuffer::sUseVAO)
 	{ //bind a dummy vertex array object so we're core profile compliant
 #ifdef GL_ARB_vertex_array_object
diff --git a/indra/llrender/llrender.h b/indra/llrender/llrender.h
index e2489876e45..095ed400f4d 100644
--- a/indra/llrender/llrender.h
+++ b/indra/llrender/llrender.h
@@ -52,7 +52,7 @@ class LLTexture ;
 
 #define LL_MATRIX_STACK_DEPTH 32
 
-class LLTexUnit
+class LLTexUnit 
 {
 	friend class LLRender;
 public:
@@ -63,6 +63,7 @@ class LLTexUnit
 		TT_TEXTURE = 0,			// Standard 2D Texture
 		TT_RECT_TEXTURE,	    // Non power of 2 texture
 		TT_CUBE_MAP,		    // 6-sided cube map texture
+        TT_CUBE_MAP_ARRAY,	    // Array of cube maps
 		TT_MULTISAMPLE_TEXTURE, // see GL_ARB_texture_multisample
         TT_TEXTURE_3D,          // standard 3D Texture
 		TT_NONE, 		        // No texture type is currently enabled        
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index cd7ec478bb7..8e8f44e99b5 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -712,8 +712,15 @@ GLhandleARB LLShaderMgr::loadShaderFile(const std::string& filename, S32 & shade
 	{  
         if (major_version >= 4)
         {
-            //set version to 400
-			shader_code_text[shader_code_count++] = strdup("#version 400\n");
+            //set version to 400 or 420
+            if (minor_version >= 20)
+            {
+                shader_code_text[shader_code_count++] = strdup("#version 420\n");
+            }
+            else
+            {
+                shader_code_text[shader_code_count++] = strdup("#version 400\n");
+            }
         }
         else if (major_version == 3)
         {
@@ -1155,7 +1162,7 @@ void LLShaderMgr::initAttribsAndUniforms()
 	mReservedUniforms.push_back("bumpMap");
     mReservedUniforms.push_back("bumpMap2");
 	mReservedUniforms.push_back("environmentMap");
-    mReservedUniforms.push_back("reflectionMap");
+    mReservedUniforms.push_back("reflectionProbes");
 	mReservedUniforms.push_back("cloud_noise_texture");
     mReservedUniforms.push_back("cloud_noise_texture_next");
 	mReservedUniforms.push_back("fullbright");
diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h
index 7ca4862ed9e..663ba28b6d8 100644
--- a/indra/llrender/llshadermgr.h
+++ b/indra/llrender/llshadermgr.h
@@ -80,7 +80,7 @@ class LLShaderMgr
         BUMP_MAP,                           //  "bumpMap"
         BUMP_MAP2,                          //  "bumpMap2"
         ENVIRONMENT_MAP,                    //  "environmentMap"
-        REFLECTION_MAP,                     //  "reflectionMap"
+        REFLECTION_PROBES,                     //  "reflectionProbes"
         CLOUD_NOISE_MAP,                    //  "cloud_noise_texture"
         CLOUD_NOISE_MAP_NEXT,               //  "cloud_noise_texture_next"
         FULLBRIGHT,                         //  "fullbright"
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 20b708fe99e..284b779be3f 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -10269,6 +10269,17 @@
       <integer>2</integer>
     </map>
 
+  <key>RenderReflectionProbeDrawDistance</key>
+  <map>
+    <key>Comment</key>
+    <string>Camera far clip to use when updating reflection probes.</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>F32</string>
+    <key>Value</key>
+    <real>64</real>
+  </map>
     <key>RenderReflectionRes</key>
     <map>
       <key>Comment</key>
diff --git a/indra/newview/app_settings/shaders/class1/deferred/aoUtil.glsl b/indra/newview/app_settings/shaders/class1/deferred/aoUtil.glsl
index 23adbded5e1..73c125bbdcd 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/aoUtil.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/aoUtil.glsl
@@ -29,8 +29,6 @@ uniform sampler2DRect   depthMap;
 
 uniform float ssao_radius;
 uniform float ssao_max_radius;
-uniform float ssao_factor;
-uniform float ssao_factor_inv;
 
 uniform mat4 inv_proj;
 uniform vec2 screen_res;
@@ -83,14 +81,14 @@ float calcAmbientOcclusion(vec4 pos, vec3 norm, vec2 pos_screen)
 {
     float ret = 1.0;
     vec3 pos_world = pos.xyz;
-    vec2 noise_reflect = texture2D(noiseMap, pos_screen.xy/128.0).xy;
+    
         
     float angle_hidden = 0.0;
     float points = 0;
         
-    float scale = min(ssao_radius / -pos_world.z, ssao_max_radius);
-    
     // it was found that keeping # of samples a constant was the fastest, probably due to compiler optimizations (unrolling?)
+    float scale = min(ssao_radius / -pos_world.z, ssao_max_radius);
+    vec2 noise_reflect = texture2D(noiseMap, pos_screen.xy/128.0).xy;
     for (int i = 0; i < 8; i++)
     {
         vec2 samppos_screen = pos_screen + scale * reflect(getKern(i), noise_reflect);
@@ -105,14 +103,14 @@ float calcAmbientOcclusion(vec4 pos, vec3 norm, vec2 pos_screen)
         //(k should vary inversely with # of samples, but this is taken care of later)
         
         float funky_val = (dot((samppos_world - 0.05*norm - pos_world), norm) > 0.0) ? 1.0 : 0.0;
-        angle_hidden = angle_hidden + funky_val * min(1.0/dist2, ssao_factor_inv);
+        angle_hidden = angle_hidden + funky_val * min(1.0/dist2, 1.0);
             
         // 'blocked' samples (significantly closer to camera relative to pos_world) are "no data", not "no occlusion" 
         float diffz_val = (diff.z > -1.0) ? 1.0 : 0.0;
         points = points + diffz_val;
     }
-        
-    angle_hidden = min(ssao_factor*angle_hidden/points, 1.0);
+
+    angle_hidden = min(angle_hidden/points, 1.0);
     
     float points_val = (points > 0.0) ? 1.0 : 0.0;
     ret = (1.0 - (points_val * angle_hidden));
diff --git a/indra/newview/app_settings/shaders/class1/deferred/blurLightF.glsl b/indra/newview/app_settings/shaders/class1/deferred/blurLightF.glsl
index 596d0274af3..fa3634f3b64 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/blurLightF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/blurLightF.glsl
@@ -69,36 +69,47 @@ void main()
     tc_mod *= 2.0;
     tc += ( (tc_mod - 0.5) * kern[1].z * dlt * 0.5 );
 
-    for (int i = 1; i < 4; i++)
+    // TODO: move this to kern instead of building kernel per pixel
+    vec3 k[7];
+    k[0] = kern[0];
+    k[2] = kern[1];
+    k[4] = kern[2];
+    k[6] = kern[3];
+
+    k[1] = (k[0]+k[2])*0.5f;
+    k[3] = (k[2]+k[4])*0.5f;
+    k[5] = (k[4]+k[6])*0.5f;
+    
+    for (int i = 1; i < 7; i++)
     {
-        vec2 samptc = tc + kern[i].z*dlt;
+        vec2 samptc = tc + k[i].z*dlt*2.0;
         vec3 samppos = getPosition(samptc).xyz; 
 
         float d = dot(norm.xyz, samppos.xyz-pos.xyz);// dist from plane
         
         if (d*d <= pointplanedist_tolerance_pow2)
         {
-            col += texture2DRect(lightMap, samptc)*kern[i].xyxx;
-            defined_weight += kern[i].xy;
+            col += texture2DRect(lightMap, samptc)*k[i].xyxx;
+            defined_weight += k[i].xy;
         }
     }
 
-    for (int i = 1; i < 4; i++)
+    for (int i = 1; i < 7; i++)
     {
-        vec2 samptc = tc - kern[i].z*dlt;
+        vec2 samptc = tc - k[i].z*dlt*2.0;
         vec3 samppos = getPosition(samptc).xyz; 
 
         float d = dot(norm.xyz, samppos.xyz-pos.xyz);// dist from plane
         
         if (d*d <= pointplanedist_tolerance_pow2)
         {
-            col += texture2DRect(lightMap, samptc)*kern[i].xyxx;
-            defined_weight += kern[i].xy;
+            col += texture2DRect(lightMap, samptc)*k[i].xyxx;
+            defined_weight += k[i].xy;
         }
     }
 
     col /= defined_weight.xyxx;
-    col.y *= col.y;
+    //col.y *= max(col.y, 0.75);
     
     frag_color = col;
 
diff --git a/indra/newview/app_settings/shaders/class1/interface/reflectionmipF.glsl b/indra/newview/app_settings/shaders/class1/interface/reflectionmipF.glsl
new file mode 100644
index 00000000000..ea687aab4f0
--- /dev/null
+++ b/indra/newview/app_settings/shaders/class1/interface/reflectionmipF.glsl
@@ -0,0 +1,75 @@
+/** 
+ * @file reflectionmipF.glsl
+ *
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2022, 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$
+ */
+ 
+#extension GL_ARB_texture_rectangle : enable
+
+/*[EXTRA_CODE_HERE]*/
+
+#ifdef DEFINE_GL_FRAGCOLOR
+out vec4 frag_color;
+#else
+#define frag_color gl_FragColor
+#endif
+
+uniform sampler2DRect screenMap;
+
+VARYING vec2 vary_texcoord0;
+
+void main() 
+{
+    float w[9];
+
+    float c = 1.0/16.0;  //corner weight
+    float e = 1.0/8.0; //edge weight
+    float m = 1.0/4.0; //middle weight
+
+    //float wsum = c*4+e*4+m;
+
+    w[0] = c;   w[1] = e;    w[2] = c;
+    w[3] = e;    w[4] = m;     w[5] = e;
+    w[6] = c;   w[7] = e;    w[8] = c;
+    
+    vec2 tc[9];
+
+    float ed = 1;
+    float cd = 1;
+
+
+    tc[0] = vec2(-cd, cd);    tc[1] = vec2(0, ed);    tc[2] = vec2(cd, cd);
+    tc[3] = vec2(-ed, 0);    tc[4] = vec2(0, 0);    tc[5] = vec2(ed, 0);
+    tc[6] = vec2(-cd, -cd);    tc[7] = vec2(0, -ed);   tc[8] = vec2(cd, -1);
+
+    vec3 color = vec3(0,0,0);
+
+    for (int i = 0; i < 9; ++i)
+    {
+        color += texture2DRect(screenMap, vary_texcoord0.xy+tc[i]).rgb * w[i];
+        //color += texture2DRect(screenMap, vary_texcoord0.xy+tc[i]*2.0).rgb * w[i]*0.5;
+    }
+
+    //color /= wsum;
+
+    frag_color = vec4(color, 1.0);
+}
diff --git a/indra/newview/app_settings/shaders/class2/deferred/softenLightF.glsl b/indra/newview/app_settings/shaders/class2/deferred/softenLightF.glsl
index 5bb64e18a75..3a1287e9109 100644
--- a/indra/newview/app_settings/shaders/class2/deferred/softenLightF.glsl
+++ b/indra/newview/app_settings/shaders/class2/deferred/softenLightF.glsl
@@ -26,9 +26,10 @@
 #extension GL_ARB_texture_rectangle : enable
 #extension GL_ARB_shader_texture_lod : enable
 
-/*[EXTRA_CODE_HERE]*/
+#define FLT_MAX 3.402823466e+38
 
-#define REFMAP_COUNT 8
+#define REFMAP_COUNT 256
+#define REF_SAMPLE_COUNT 64 //maximum number of samples to consider
 
 #ifdef DEFINE_GL_FRAGCOLOR
 out vec4 frag_color;
@@ -42,12 +43,26 @@ uniform sampler2DRect normalMap;
 uniform sampler2DRect lightMap;
 uniform sampler2DRect depthMap;
 uniform samplerCube   environmentMap;
-uniform samplerCube   reflectionMap[REFMAP_COUNT];
+uniform samplerCubeArray   reflectionProbes;
 uniform sampler2D     lightFunc;
 
-uniform int refmapCount;
-
-uniform vec3 refOrigin[REFMAP_COUNT];
+layout (std140, binding = 1) uniform ReflectionProbes
+{
+    // list of sphere based reflection probes sorted by distance to camera (closest first)
+    vec4 refSphere[REFMAP_COUNT];
+    // index  of cube map in reflectionProbes for a corresponding reflection probe
+    // e.g. cube map channel of refSphere[2] is stored in refIndex[2]
+    // refIndex.x - cubemap channel in reflectionProbes
+    // refIndex.y - index in refNeighbor of neighbor list (index is ivec4 index, not int index)
+    // refIndex.z - number of neighbors
+    ivec4 refIndex[REFMAP_COUNT];
+
+    // neighbor list data (refSphere indices, not cubemap array layer)
+    ivec4 refNeighbor[1024];
+
+    // number of reflection probes present in refSphere
+    int refmapCount;
+};
 
 uniform float blur_size;
 uniform float blur_fidelity;
@@ -80,7 +95,116 @@ vec3 srgb_to_linear(vec3 c);
 vec4 applyWaterFogView(vec3 pos, vec4 color);
 #endif
 
+// list of probeIndexes shader will actually use after "getRefIndex" is called
+// (stores refIndex/refSphere indices, NOT rerflectionProbes layer)
+int probeIndex[REF_SAMPLE_COUNT];
+
+// number of probes stored in probeIndex
+int probeInfluences = 0;
+
+
+// return true if probe at index i influences position pos
+bool shouldSampleProbe(int i, vec3 pos)
+{
+    vec3 delta = pos.xyz - refSphere[i].xyz;
+    float d = dot(delta, delta);
+    float r2 = refSphere[i].w;
+    r2 *= r2;
+    return d < r2;
+}
+
+// populate "probeIndex" with N probe indices that influence pos where N is REF_SAMPLE_COUNT
+// overall algorithm -- 
+void getRefIndex(vec3 pos)
+{
+    // TODO: make some sort of structure that reduces the number of distance checks
 
+    for (int i = 0; i < refmapCount; ++i)
+    {
+        // found an influencing probe
+        if (shouldSampleProbe(i, pos))
+        {
+            probeIndex[probeInfluences] = i;
+            ++probeInfluences;
+
+            int neighborIdx = refIndex[i].y;
+            if (neighborIdx != -1)
+            {
+                int neighborCount = min(refIndex[i].z, REF_SAMPLE_COUNT-1);
+
+                int count = 0;
+                while (count < neighborCount)
+                {
+                    // check up to REF_SAMPLE_COUNT-1 neighbors (neighborIdx is ivec4 index)
+
+                    int idx = refNeighbor[neighborIdx].x;
+                    if (shouldSampleProbe(idx, pos))
+                    {
+                        probeIndex[probeInfluences++] = idx;
+                        if (probeInfluences == REF_SAMPLE_COUNT)
+                        {
+                            return;
+                        }
+                    }
+                    count++;
+                    if (count == neighborCount)
+                    {
+                        return;
+                    }
+
+                    idx = refNeighbor[neighborIdx].y;
+                    if (shouldSampleProbe(idx, pos))
+                    {
+                        probeIndex[probeInfluences++] = idx;
+                        if (probeInfluences == REF_SAMPLE_COUNT)
+                        {
+                            return;
+                        }
+                    }
+                    count++;
+                    if (count == neighborCount)
+                    {
+                        return;
+                    }
+
+                    idx = refNeighbor[neighborIdx].z;
+                    if (shouldSampleProbe(idx, pos))
+                    {
+                        probeIndex[probeInfluences++] = idx;
+                        if (probeInfluences == REF_SAMPLE_COUNT)
+                        {
+                            return;
+                        }
+                    }
+                    count++;
+                    if (count == neighborCount)
+                    {
+                        return;
+                    }
+
+                    idx = refNeighbor[neighborIdx].w;
+                    if (shouldSampleProbe(idx, pos))
+                    {
+                        probeIndex[probeInfluences++] = idx;
+                        if (probeInfluences == REF_SAMPLE_COUNT)
+                        {
+                            return;
+                        }
+                    }
+                    count++;
+                    if (count == neighborCount)
+                    {
+                        return;
+                    }
+
+                    ++neighborIdx;
+                }
+
+                return;
+            }
+        }
+    }
+}
 
 // from https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection
 
@@ -120,13 +244,10 @@ bool intersect(const Ray &ray) const
 } */
 
 // adapted -- assume that origin is inside sphere, return distance from origin to edge of sphere
-float sphereIntersect(vec3 origin, vec3 dir, vec4 sph )
+float sphereIntersect(vec3 origin, vec3 dir, vec3 center, float radius2)
 { 
         float t0, t1; // solutions for t if the ray intersects 
 
-        vec3 center = sph.xyz;
-        float radius2 = sph.w * sph.w;
-
         vec3 L = center - origin; 
         float tca = dot(L,dir);
 
@@ -139,33 +260,78 @@ float sphereIntersect(vec3 origin, vec3 dir, vec4 sph )
         return t1; 
 } 
 
+// Tap a sphere based reflection probe
+// pos - position of pixel
+// dir - pixel normal
+// lod - which mip to bias towards (lower is higher res, sharper reflections)
+// c - center of probe
+// r2 - radius of probe squared
+// i - index of probe 
+// vi - point at which reflection vector struck the influence volume, in clip space
+vec3 tapRefMap(vec3 pos, vec3 dir, float lod, vec3 c, float r2, int i, out vec3 vi)
+{
+    //lod = max(lod, 1);
+// parallax adjustment
+    float d = sphereIntersect(pos, dir, c, r2);
+
+    {
+        vec3 v = pos + dir * d;
+        vi = v;
+        v -= c.xyz;
+        v = env_mat * v;
+
+        float min_lod = textureQueryLod(reflectionProbes,v).y; // lower is higher res
+        return textureLod(reflectionProbes, vec4(v.xyz, refIndex[i].x), max(min_lod, lod)).rgb;
+        //return texture(reflectionProbes, vec4(v.xyz, refIndex[i].x)).rgb;
+    }
+}
+
 vec3 sampleRefMap(vec3 pos, vec3 dir, float lod)
 {
     float wsum = 0.0;
-
     vec3 col = vec3(0,0,0);
+    float vd2 = dot(pos,pos); // view distance squared
 
-    for (int i = 0; i < refmapCount; ++i)
-    //int i = 0;
+    for (int idx = 0; idx < probeInfluences; ++idx)
     {
-        float r = 16.0;
-        vec3 delta = pos.xyz-refOrigin[i].xyz;
-        if (length(delta) < r)
+        int i = probeIndex[idx];
+        float r = refSphere[i].w; // radius of sphere volume
+        float rr = r*r; // radius squred
+        float r1 = r * 0.1; // 75% of radius (outer sphere to start interpolating down)
+        vec3 delta = pos.xyz-refSphere[i].xyz;
+        float d2 = dot(delta,delta);
+        float r2 = r1*r1; 
+        
         {
-            float w = 1.0/max(dot(delta, delta), r);
-            w *= w;
-            w *= w;
+            vec3 vi;
+            vec3 refcol = tapRefMap(pos, dir, lod, refSphere[i].xyz, rr, i, vi);
+            
+            float w = 1.0/d2;
 
-            // parallax adjustment
-            float d = sphereIntersect(pos, dir, vec4(refOrigin[i].xyz, r));
+            float atten = 1.0-max(d2-r2, 0.0)/(rr-r2);
+            w *= atten;
 
-            {
-                vec3 v = pos + dir * d;
-                v -= refOrigin[i].xyz;
-                v = env_mat * v;
+            col += refcol*w;
+            
+            wsum += w;
+        }
+    }
 
-                float min_lod = textureQueryLod(reflectionMap[i],v).y; // lower is higher res
-                col += textureLod(reflectionMap[i], v, max(min_lod, lod)).rgb*w;
+    if (probeInfluences <= 1)
+    { //edge-of-scene probe or no probe influence, mix in with embiggened version of probes closest to camera 
+        for (int idx = 0; idx < 8; ++idx)
+        {
+            int i = idx;
+            vec3 delta = pos.xyz-refSphere[i].xyz;
+            float d2 = dot(delta,delta);
+            
+            {
+                vec3 vi;
+                vec3 refcol = tapRefMap(pos, dir, lod, refSphere[i].xyz, d2, i, vi);
+                
+                float w = 1.0/d2;
+                w *= w;
+                col += refcol*w;
                 wsum += w;
             }
         }
@@ -175,13 +341,6 @@ vec3 sampleRefMap(vec3 pos, vec3 dir, float lod)
     {
         col *= 1.0/wsum;
     }
-    else
-    {
-        // this pixel not covered by a probe, fallback to "full scene" environment map
-        vec3 v = env_mat * dir;
-        float min_lod = textureQueryLod(environmentMap, v).y; // lower is higher res
-        col = textureLod(environmentMap, v, max(min_lod, lod)).rgb;
-    }
     
     return col;
 }
@@ -202,13 +361,26 @@ vec3 sampleAmbient(vec3 pos, vec3 dir, float lod)
     
     col *= 0.333333;
 
-    return col*0.6; // fudge darker
+    return col*0.8; // fudge darker
 
 }
 
+// brighten a color so that at least one component is 1
+vec3 brighten(vec3 c)
+{
+    float m = max(max(c.r, c.g), c.b);
+
+    if (m == 0)
+    {
+        return vec3(1,1,1);
+    }
+
+    return c * 1.0/m;
+}
+
 void main()
 {
-    float reflection_lods = 11; // TODO -- base this on resolution of reflection map instead of hard coding
+    float reflection_lods = 8; // TODO -- base this on resolution of reflection map instead of hard coding
 
     vec2  tc           = vary_fragcoord.xy;
     float depth        = texture2DRect(depthMap, tc.xy).r;
@@ -238,14 +410,16 @@ void main()
     vec3 amblit;
     vec3 additive;
     vec3 atten;
+
+    getRefIndex(pos.xyz);
+
     calcAtmosphericVars(pos.xyz, light_dir, ambocc, sunlit, amblit, additive, atten, true);
 
     //vec3 amb_vec = env_mat * norm.xyz;
 
-    vec3 ambenv = sampleAmbient(pos.xyz, norm.xyz, reflection_lods-1);
-    amblit = mix(ambenv, amblit, amblit);
-    color.rgb = amblit;
-    
+    vec3 ambenv = sampleAmbient(pos.xyz, norm.xyz, reflection_lods);
+    amblit = max(ambenv, amblit);
+    color.rgb = amblit*ambocc;
 
     //float ambient = min(abs(dot(norm.xyz, sun_dir.xyz)), 1.0);
     //ambient *= 0.5;
@@ -255,6 +429,7 @@ void main()
 
     vec3 sun_contrib = min(da, scol) * sunlit;
     color.rgb += sun_contrib;
+    color.rgb = min(color.rgb, vec3(1,1,1));
     color.rgb *= diffuse.rgb;
 
     vec3 refnormpersp = reflect(pos.xyz, norm.xyz);
@@ -274,26 +449,23 @@ void main()
         
         float lod = (1.0-spec.a)*reflection_lods;
         vec3 reflected_color = sampleRefMap(pos.xyz, normalize(refnormpersp), lod);
-        reflected_color *= 0.5; // fudge darker, not sure where there's a multiply by two and it's late
+        reflected_color *= 0.35; // fudge darker
         float fresnel = 1.0+dot(normalize(pos.xyz), norm.xyz);
-        fresnel += spec.a;
-        fresnel *= fresnel;
-        //fresnel *= spec.a;
+        float minf = spec.a * 0.1;
+        fresnel = fresnel * (1.0-minf) + minf;
         reflected_color *= spec.rgb*min(fresnel, 1.0);
-        //reflected_color = srgb_to_linear(reflected_color);
-        vec3 mixer = clamp(color.rgb + vec3(1,1,1) - spec.rgb, vec3(0,0,0), vec3(1,1,1));
-        color.rgb = mix(reflected_color, color, mixer);
+        color.rgb += reflected_color;
     }
 
     color.rgb = mix(color.rgb, diffuse.rgb, diffuse.a);
 
     if (envIntensity > 0.0)
     {  // add environmentmap
-        vec3 reflected_color = sampleRefMap(pos.xyz, normalize(refnormpersp), 0.0);
+        vec3 reflected_color = sampleRefMap(pos.xyz, normalize(refnormpersp), 0.0)*0.5; //fudge darker
         float fresnel = 1.0+dot(normalize(pos.xyz), norm.xyz);
         fresnel *= fresnel;
-        fresnel = fresnel * 0.95 + 0.05;
-        reflected_color *= fresnel;
+        fresnel = min(fresnel+envIntensity, 1.0);
+        reflected_color *= (envIntensity*fresnel)*brighten(spec.rgb);
         color = mix(color.rgb, reflected_color, envIntensity);
     }
 
@@ -311,5 +483,7 @@ void main()
 
     // convert to linear as fullscreen lights need to sum in linear colorspace
     // and will be gamma (re)corrected downstream...
+    //color = vec3(ambocc);
+    //color = ambenv;    
     frag_color.rgb = srgb_to_linear(color.rgb);
 }
diff --git a/indra/newview/llagentcamera.cpp b/indra/newview/llagentcamera.cpp
index 84a41113be3..8d02bec53e8 100644
--- a/indra/newview/llagentcamera.cpp
+++ b/indra/newview/llagentcamera.cpp
@@ -1188,12 +1188,18 @@ void LLAgentCamera::updateLookAt(const S32 mouse_x, const S32 mouse_y)
 
 static LLTrace::BlockTimerStatHandle FTM_UPDATE_CAMERA("Camera");
 
+extern BOOL gCubeSnapshot;
+
 //-----------------------------------------------------------------------------
 // updateCamera()
 //-----------------------------------------------------------------------------
 void LLAgentCamera::updateCamera()
 {
 	LL_RECORD_BLOCK_TIME(FTM_UPDATE_CAMERA);
+    if (gCubeSnapshot)
+    {
+        return;
+    }
 
 	// - changed camera_skyward to the new global "mCameraUpVector"
 	mCameraUpVector = LLVector3::z_axis;
diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp
index eebd89f77fd..47330debd36 100644
--- a/indra/newview/lldrawpoolalpha.cpp
+++ b/indra/newview/lldrawpoolalpha.cpp
@@ -129,6 +129,8 @@ static void prepare_alpha_shader(LLGLSLShader* shader, bool textureGamma, bool d
     }
 }
 
+extern BOOL gCubeSnapshot;
+
 void LLDrawPoolAlpha::renderPostDeferred(S32 pass) 
 { 
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL;
@@ -155,7 +157,7 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass)
     forwardRender();
 
     // final pass, render to depth for depth of field effects
-    if (!LLPipeline::sImpostorRender && gSavedSettings.getBOOL("RenderDepthOfField"))
+    if (!LLPipeline::sImpostorRender && gSavedSettings.getBOOL("RenderDepthOfField") && !gCubeSnapshot)
     { 
         //update depth buffer sampler
         gPipeline.mScreen.flush();
diff --git a/indra/newview/lldrawpoolbump.cpp b/indra/newview/lldrawpoolbump.cpp
index 2892fc6f9f5..0df0137b7aa 100644
--- a/indra/newview/lldrawpoolbump.cpp
+++ b/indra/newview/lldrawpoolbump.cpp
@@ -735,6 +735,7 @@ void LLDrawPoolBump::renderDeferred(S32 pass)
 
 void LLDrawPoolBump::renderPostDeferred(S32 pass)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL
     for (int i = 0; i < 2; ++i)
     { // two passes -- static and rigged
         mRigged = (i == 1);
diff --git a/indra/newview/lldrawpoolpbropaque.cpp b/indra/newview/lldrawpoolpbropaque.cpp
index 64850424f4b..2478aa0cff1 100644
--- a/indra/newview/lldrawpoolpbropaque.cpp
+++ b/indra/newview/lldrawpoolpbropaque.cpp
@@ -87,6 +87,6 @@ void LLDrawPoolPBROpaque::renderDeferred(S32 pass)
     // TODO: handle under water?
     // if (LLPipeline::sUnderWaterRender)
     // PASS_SIMPLE or PASS_MATERIAL
-    pushBatches(LLRenderPass::PASS_SIMPLE, getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX, TRUE, TRUE);
+    //pushBatches(LLRenderPass::PASS_SIMPLE, getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX, TRUE, TRUE);
 }
 
diff --git a/indra/newview/llreflectionmap.cpp b/indra/newview/llreflectionmap.cpp
index b75dba877ee..0cf2f228221 100644
--- a/indra/newview/llreflectionmap.cpp
+++ b/indra/newview/llreflectionmap.cpp
@@ -29,36 +29,183 @@
 #include "llreflectionmap.h"
 #include "pipeline.h"
 #include "llviewerwindow.h"
+#include "llviewerregion.h"
+
+extern F32SecondsImplicit gFrameTimeSeconds;
 
 LLReflectionMap::LLReflectionMap()
 {
+    mLastUpdateTime = gFrameTimeSeconds;
 }
 
-void LLReflectionMap::update(const LLVector3& origin, U32 resolution)
+void LLReflectionMap::update(U32 resolution, U32 face)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    mLastUpdateTime = gFrameTimeSeconds;
+    llassert(mCubeArray.notNull());
+    llassert(mCubeIndex != -1);
     llassert(LLPipeline::sRenderDeferred);
-
-    // make sure resolution is < gPipeline.mDeferredScreen.getWidth()
-
+    
+    // make sure we don't walk off the edge of the render target
     while (resolution > gPipeline.mDeferredScreen.getWidth() ||
         resolution > gPipeline.mDeferredScreen.getHeight())
     {
         resolution /= 2;
     }
+    gViewerWindow->cubeSnapshot(LLVector3(mOrigin), mCubeArray, mCubeIndex, face);
+}
 
-    if (resolution == 0)
-    {
-        return;
+bool LLReflectionMap::shouldUpdate()
+{
+    const F32 UPDATE_INTERVAL = 10.f; // update no more than this often
+    const F32 TIMEOUT_INTERVAL = 30.f; // update no less than this often
+    const F32 RENDER_TIMEOUT = 1.f; // don't update if hasn't been used for rendering for this long
+    
+    if (mLastBindTime > gFrameTimeSeconds - RENDER_TIMEOUT)
+    {   
+        if (mDirty && mLastUpdateTime < gFrameTimeSeconds - UPDATE_INTERVAL)
+        {
+            return true;
+        }
+
+        if (mLastUpdateTime < gFrameTimeSeconds - TIMEOUT_INTERVAL)
+        {
+            return true;
+        }
     }
 
-    mOrigin.load3(origin.mV);
+    return false;
+}
+
+void LLReflectionMap::dirty()
+{
+    mDirty = true;
+    mLastUpdateTime = gFrameTimeSeconds;
+}
+
+void LLReflectionMap::autoAdjustOrigin()
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+
+    if (mGroup)
+    {
+        const LLVector4a* bounds = mGroup->getBounds();
+        auto* node = mGroup->getOctreeNode();
+
+        if (mGroup->getSpatialPartition()->mPartitionType == LLViewerRegion::PARTITION_TERRAIN)
+        {
+            // for terrain, make probes float a couple meters above the highest point in the surface patch
+            mOrigin = bounds[0];
+            mOrigin.getF32ptr()[2] += bounds[1].getF32ptr()[2] + 3.f;
+
+            // update radius to encompass bounding box
+            LLVector4a d;
+            d.setAdd(bounds[0], bounds[1]);
+            d.sub(mOrigin);
+            mRadius = d.getLength3().getF32();
+        }
+        else if (mGroup->getSpatialPartition()->mPartitionType == LLViewerRegion::PARTITION_VOLUME)
+        {
+            // cast a ray towards 8 corners of bounding box
+            // nudge origin towards center of empty space
+
+            if (node->isLeaf() || node->getChildCount() > 1 || node->getData().size() > 0)
+            { // use center of object bounding box for leaf nodes or nodes with multiple child nodes
+                mOrigin = bounds[0];
 
-    mCubeMap = new LLCubeMap(false);
-    mCubeMap->initReflectionMap(resolution);
+                LLVector4a start;
+                LLVector4a end;
 
-    gViewerWindow->cubeSnapshot(origin, mCubeMap);
+                LLVector4a size = bounds[1];
 
-    mCubeMap->generateMipMaps();
+                LLVector4a corners[] =
+                {
+                    { 1, 1, 1 },
+                    { -1, 1, 1 },
+                    { 1, -1, 1 },
+                    { -1, -1, 1 },
+                    { 1, 1, -1 },
+                    { -1, 1, -1 },
+                    { 1, -1, -1 },
+                    { -1, -1, -1 }
+                };
+
+                for (int i = 0; i < 8; ++i)
+                {
+                    corners[i].mul(size);
+                    corners[i].add(bounds[0]);
+                }
+
+                LLVector4a extents[2];
+                extents[0].setAdd(bounds[0], bounds[1]);
+                extents[1].setSub(bounds[0], bounds[1]);
+
+                bool hit = false;
+                for (int i = 0; i < 8; ++i)
+                {
+                    int face = -1;
+                    LLVector4a intersection;
+                    LLDrawable* drawable = mGroup->lineSegmentIntersect(bounds[0], corners[i], true, false, &face, &intersection);
+                    if (drawable != nullptr)
+                    {
+                        hit = true;
+                        update_min_max(extents[0], extents[1], intersection);
+                    }
+                    else
+                    {
+                        update_min_max(extents[0], extents[1], corners[i]);
+                    }
+                }
+
+                if (hit)
+                {
+                    mOrigin.setAdd(extents[0], extents[1]);
+                    mOrigin.mul(0.5f);
+                }
+
+                // make sure radius encompasses all objects
+                LLSimdScalar r2 = 0.0;
+                for (int i = 0; i < 8; ++i)
+                {
+                    LLVector4a v;
+                    v.setSub(corners[i], mOrigin);
+
+                    LLSimdScalar d = v.dot3(v);
+
+                    if (d > r2)
+                    {
+                        r2 = d;
+                    }
+                }
+
+                mRadius = llmax(sqrtf(r2.getF32()), 8.f);
+            }
+            else
+            {
+                // use center of octree node volume for nodes that are just branches without data
+                mOrigin = node->getCenter();
+
+                // update radius to encompass entire octree node volume
+                mRadius = node->getSize().getLength3().getF32();
+
+                //mOrigin = bounds[0];
+                //mRadius = bounds[1].getLength3().getF32();
+
+            }
+        }
+    }
 }
 
+bool LLReflectionMap::intersects(LLReflectionMap* other)
+{
+    LLVector4a delta;
+    delta.setSub(other->mOrigin, mOrigin);
+
+    F32 dist = delta.dot3(delta).getF32();
+
+    F32 r2 = mRadius + other->mRadius;
+
+    r2 *= r2;
+
+    return dist < r2;
+}
diff --git a/indra/newview/llreflectionmap.h b/indra/newview/llreflectionmap.h
index 7d39e7e562d..ed1c2680b66 100644
--- a/indra/newview/llreflectionmap.h
+++ b/indra/newview/llreflectionmap.h
@@ -26,26 +26,61 @@
 
 #pragma once
 
-#include "llcubemap.h"
+#include "llcubemaparray.h"
+#include "llmemory.h"
 
-class LLReflectionMap : public LLRefCount
+class LLSpatialGroup;
+
+class alignas(16) LLReflectionMap : public LLRefCount
 {
+    LL_ALIGN_NEW
 public:
     // allocate an environment map of the given resolution 
     LLReflectionMap();
 
     // update this environment map
-    // origin - position in agent space to generate environment map from in agent space
     // resolution - size of cube map to generate
-    void update(const LLVector3& origin, U32 resolution);
-    
-    // point at which environment map was generated from (in agent space)
-    LLVector4a mOrigin;
+    void update(U32 resolution, U32 face);
+
+    // return true if this probe should update *now*
+    bool shouldUpdate();
 
+    // Mark this reflection map as needing an update (resets last update time, so spamming this call will cause a cube map to never update)
+    void dirty();
+
+    // for volume partition probes, try to place this probe in the best spot
+    void autoAdjustOrigin();
+
+    // return true if given Reflection Map's influence volume intersect's with this one's
+    bool intersects(LLReflectionMap* other);
+
+    // point at which environment map was last generated from (in agent space)
+    LLVector4a mOrigin;
+    
     // distance from viewer camera
     F32 mDistance;
 
+    // radius of this probe's affected area
+    F32 mRadius = 16.f;
+
+    // last time this probe was updated (or when its update timer got reset)
+    F32 mLastUpdateTime = 0.f;
+
+    // last time this probe was bound for rendering
+    F32 mLastBindTime = 0.f;
+
     // cube map used to sample this environment map
-    LLPointer<LLCubeMap> mCubeMap;
+    LLPointer<LLCubeMapArray> mCubeArray;
+    S32 mCubeIndex = -1; // index into cube map array or -1 if not currently stored in cube map array
+
+    // index into array packed by LLReflectionMapManager::getReflectionMaps
+    // WARNING -- only valid immediately after call to getReflectionMaps
+    S32 mProbeIndex = -1;
+
+    // set of any LLReflectionMaps that intersect this map (maintained by LLReflectionMapManager
+    std::vector<LLReflectionMap*> mNeighbors;
+
+    LLSpatialGroup* mGroup = nullptr;
+    bool mDirty = true;
 };
 
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index 95b72e1d3b0..a6b704d57da 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -28,30 +28,84 @@
 
 #include "llreflectionmapmanager.h"
 #include "llviewercamera.h"
+#include "llspatialpartition.h"
+#include "llviewerregion.h"
+#include "pipeline.h"
+#include "llviewershadermgr.h"
+
+extern BOOL gCubeSnapshot;
+extern BOOL gTeleportDisplay;
+
+//#pragma optimize("", off)
 
 LLReflectionMapManager::LLReflectionMapManager()
 {
-
+    for (int i = 0; i < LL_REFLECTION_PROBE_COUNT; ++i)
+    {
+        mCubeFree[i] = true;
+    }
 }
 
 struct CompareReflectionMapDistance
 {
-    
+
 };
 
 
 struct CompareProbeDistance
 {
-    bool operator()(const LLReflectionMap& lhs, const LLReflectionMap& rhs)
+    bool operator()(const LLPointer<LLReflectionMap>& lhs, const LLPointer<LLReflectionMap>& rhs)
     {
-        return lhs.mDistance < rhs.mDistance;
+        return lhs->mDistance < rhs->mDistance;
     }
 };
 
 // helper class to seed octree with probes
 void LLReflectionMapManager::update()
 {
+    if (!LLPipeline::sRenderDeferred || gTeleportDisplay)
+    {
+        return;
+    }
+
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    llassert(!gCubeSnapshot); // assert a snapshot is not in progress
+    if (LLAppViewer::instance()->logoutRequestSent())
+    {
+        return;
+    }
+
+    // =============== TODO -- move to an init function  =================
+
+    if (mTexture.isNull())
+    {
+        mTexture = new LLCubeMapArray();
+        mTexture->allocate(LL_REFLECTION_PROBE_RESOLUTION, 3, LL_REFLECTION_PROBE_COUNT);
+    }
+
+    if (!mRenderTarget.isComplete())
+    {
+        U32 color_fmt = GL_RGBA;
+        const bool use_depth_buffer = true;
+        const bool use_stencil_buffer = true;
+        U32 targetRes = LL_REFLECTION_PROBE_RESOLUTION * 4; // super sample
+        mRenderTarget.allocate(targetRes, targetRes, color_fmt, use_depth_buffer, use_stencil_buffer, LLTexUnit::TT_RECT_TEXTURE);
+    }
+
+    if (mMipChain.empty())
+    {
+        U32 res = LL_REFLECTION_PROBE_RESOLUTION*2;
+        U32 count = log2((F32)res) + 0.5f;
+        
+        mMipChain.resize(count);
+        for (int i = 0; i < count; ++i)
+        {
+            mMipChain[i].allocate(res, res, GL_RGB, false, false, LLTexUnit::TT_RECT_TEXTURE);
+            res /= 2;
+        }
+    }
+
+    // =============== TODO -- move to an init function  =================
 
     // naively drop probes every 16m as we move the camera around for now
     // later, use LLSpatialPartition to manage probes
@@ -61,46 +115,462 @@ void LLReflectionMapManager::update()
     LLVector4a camera_pos;
     camera_pos.load3(LLViewerCamera::instance().getOrigin().mV);
 
-    for (auto& probe : mProbes)
+    // process kill list
+    for (int i = 0; i < mProbes.size(); )
     {
-        LLVector4a d;
-        d.setSub(camera_pos, probe.mOrigin);
-        probe.mDistance = d.getLength3().getF32();
+        auto& iter = std::find(mKillList.begin(), mKillList.end(), mProbes[i]);
+        if (iter != mKillList.end())
+        {
+            deleteProbe(i);
+            mProbes.erase(mProbes.begin() + i);
+            mKillList.erase(iter);
+        }
+        else
+        {
+            ++i;
+        }
     }
 
-    if (mProbes.empty() || mProbes[0].mDistance > PROBE_SPACING)
+    mKillList.clear();
+    
+    // process create list
+    for (auto& probe : mCreateList)
     {
-        addProbe(LLViewerCamera::instance().getOrigin());
+        mProbes.push_back(probe);
     }
 
-    // update distance to camera for all probes
-    std::sort(mProbes.begin(), mProbes.end(), CompareProbeDistance());
+    mCreateList.clear();
+
+    const F32 UPDATE_INTERVAL = 10.f;  //update no more than once every 5 seconds
+
+    bool did_update = false;
+
+    if (mUpdatingProbe != nullptr)
+    {
+        did_update = true;
+        doProbeUpdate();
+    }
 
-    if (mProbes.size() > MAX_PROBES)
+    for (int i = 0; i < mProbes.size(); ++i)
     {
-        mProbes.resize(MAX_PROBES);
+        LLReflectionMap* probe = mProbes[i];
+        if (probe->getNumRefs() == 1)
+        { // no references held outside manager, delete this probe
+            deleteProbe(i);
+            --i;
+            continue;
+        }
+        
+        probe->mProbeIndex = i;
+
+        LLVector4a d;
+        
+        if (probe->shouldUpdate() && !did_update && i < LL_REFLECTION_PROBE_COUNT)
+        {
+            if (probe->mCubeIndex == -1)
+            {
+                probe->mCubeArray = mTexture;
+                probe->mCubeIndex = allocateCubeIndex();
+            }
+
+            did_update = true; // only update one probe per frame
+            probe->autoAdjustOrigin();
+
+            mUpdatingProbe = probe;
+            doProbeUpdate();
+            probe->mDirty = false;
+        }
+
+        d.setSub(camera_pos, probe->mOrigin);
+        probe->mDistance = d.getLength3().getF32()-probe->mRadius;
     }
+
+    // update distance to camera for all probes
+    std::sort(mProbes.begin(), mProbes.end(), CompareProbeDistance());
 }
 
 void LLReflectionMapManager::addProbe(const LLVector3& pos)
 {
-    LLReflectionMap probe;
-    probe.update(pos, 1024);
-    mProbes.push_back(probe);
+    //LLReflectionMap* probe = new LLReflectionMap();
+    //probe->update(pos, 1024);
+    //mProbes.push_back(probe);
+}
+
+LLReflectionMap* LLReflectionMapManager::addProbe(LLSpatialGroup* group)
+{
+    LLReflectionMap* probe = new LLReflectionMap();
+    probe->mGroup = group;
+    probe->mOrigin = group->getOctreeNode()->getCenter();
+    probe->mDirty = true;
+
+    if (gCubeSnapshot)
+    { //snapshot is in progress, mProbes is being iterated over, defer insertion until next update
+        mCreateList.push_back(probe);
+    }
+    else
+    {
+        mProbes.push_back(probe);
+    }
+
+    return probe;
 }
 
 void LLReflectionMapManager::getReflectionMaps(std::vector<LLReflectionMap*>& maps)
 {
-    // just null out for now
-    U32 i = 0;
-    for (i = 0; i < maps.size() && i < mProbes.size(); ++i)
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+
+    U32 count = 0;
+    U32 lastIdx = 0;
+    for (U32 i = 0; count < maps.size() && i < mProbes.size(); ++i)
+    {
+        mProbes[i]->mLastBindTime = gFrameTimeSeconds; // something wants to use this probe, indicate it's been requested
+        if (mProbes[i]->mCubeIndex != -1)
+        {
+            mProbes[i]->mProbeIndex = count;
+            maps[count++] = mProbes[i];
+        }
+        else
+        {
+            mProbes[i]->mProbeIndex = -1;
+        }
+        lastIdx = i;
+    }
+
+    // set remaining probe indices to -1
+    for (U32 i = lastIdx+1; i < mProbes.size(); ++i)
+    {
+        mProbes[i]->mProbeIndex = -1;
+    }
+
+    // null terminate list
+    if (count < maps.size())
+    {
+        maps[count] = nullptr;
+    }
+}
+
+LLReflectionMap* LLReflectionMapManager::registerSpatialGroup(LLSpatialGroup* group)
+{
+    if (group->getSpatialPartition()->mPartitionType == LLViewerRegion::PARTITION_VOLUME)
+    {
+        OctreeNode* node = group->getOctreeNode();
+        F32 size = node->getSize().getF32ptr()[0];
+        if (size >= 7.f && size <= 17.f)
+        {
+            return addProbe(group);
+        }
+    }
+    
+    if (group->getSpatialPartition()->mPartitionType == LLViewerRegion::PARTITION_TERRAIN)
+    {
+        OctreeNode* node = group->getOctreeNode();
+        F32 size = node->getSize().getF32ptr()[0];
+        if (size >= 15.f && size <= 17.f)
+        {
+            return addProbe(group);
+        }
+    }
+
+    return nullptr;
+}
+
+S32 LLReflectionMapManager::allocateCubeIndex()
+{
+    for (int i = 0; i < LL_REFLECTION_PROBE_COUNT; ++i)
+    {
+        if (mCubeFree[i])
+        {
+            mCubeFree[i] = false;
+            return i;
+        }
+    }
+
+    // no cubemaps free, steal one from the back of the probe list
+    for (int i = mProbes.size() - 1; i >= LL_REFLECTION_PROBE_COUNT; --i)
+    {
+        if (mProbes[i]->mCubeIndex != -1)
+        {
+            S32 ret = mProbes[i]->mCubeIndex;
+            mProbes[i]->mCubeIndex = -1;
+            return ret;
+        }
+    }
+
+    llassert(false); // should never fail to allocate, something is probably wrong with mCubeFree
+    return -1;
+}
+
+void LLReflectionMapManager::deleteProbe(U32 i)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    LLReflectionMap* probe = mProbes[i];
+
+    if (probe->mCubeIndex != -1)
+    { // mark the cube index used by this probe as being free
+        mCubeFree[probe->mCubeIndex] = true;
+    }
+    if (mUpdatingProbe == probe)
+    {
+        mUpdatingProbe = nullptr;
+        mUpdatingFace = 0;
+    }
+
+    // remove from any Neighbors lists
+    for (auto& other : probe->mNeighbors)
+    {
+        auto& iter = std::find(other->mNeighbors.begin(), other->mNeighbors.end(), probe);
+        llassert(iter != other->mNeighbors.end());
+        other->mNeighbors.erase(iter);
+    }
+
+    mProbes.erase(mProbes.begin() + i);
+}
+
+
+void LLReflectionMapManager::doProbeUpdate()
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    llassert(mUpdatingProbe != nullptr);
+
+    mRenderTarget.bindTarget();
+    mUpdatingProbe->update(mRenderTarget.getWidth(), mUpdatingFace);
+    mRenderTarget.flush();
+
+    // generate mipmaps
     {
-        maps[i] = &(mProbes[i]);
+        LLGLDepthTest depth(GL_FALSE, GL_FALSE);
+        LLGLDisable cull(GL_CULL_FACE);
+
+        gReflectionMipProgram.bind();
+        gGL.matrixMode(gGL.MM_MODELVIEW);
+        gGL.pushMatrix();
+        gGL.loadIdentity();
+
+        gGL.matrixMode(gGL.MM_PROJECTION);
+        gGL.pushMatrix();
+        gGL.loadIdentity();
+
+        gGL.flush();
+        U32 res = LL_REFLECTION_PROBE_RESOLUTION*4;
+
+        S32 mips = log2((F32) LL_REFLECTION_PROBE_RESOLUTION)+0.5f;
+
+        for (int i = 0; i < mMipChain.size(); ++i)
+        {
+            mMipChain[i].bindTarget();
+
+            if (i == 0)
+            {
+                gGL.getTexUnit(0)->bind(&mRenderTarget);
+            }
+            else
+            {
+                gGL.getTexUnit(0)->bind(&(mMipChain[i - 1]));
+            }
+
+            gGL.begin(gGL.QUADS);
+            
+            gGL.texCoord2f(0, 0);
+            gGL.vertex2f(-1, -1);
+            
+            gGL.texCoord2f(res, 0);
+            gGL.vertex2f(1, -1);
+
+            gGL.texCoord2f(res, res);
+            gGL.vertex2f(1, 1);
+
+            gGL.texCoord2f(0, res);
+            gGL.vertex2f(-1, 1);
+            gGL.end();
+            gGL.flush();
+
+            res /= 2;
+
+            S32 mip = i - (mMipChain.size() - mips);
+
+            if (mip >= 0)
+            {
+                mTexture->bind(0);
+                glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, mUpdatingProbe->mCubeIndex * 6 + mUpdatingFace, 0, 0, res, res);
+                mTexture->unbind();
+            }
+            mMipChain[i].flush();
+        }
+
+        gGL.popMatrix();
+        gGL.matrixMode(gGL.MM_MODELVIEW);
+        gGL.popMatrix();
+
+        gReflectionMipProgram.unbind();
+    }
+    
+    if (++mUpdatingFace == 6)
+    {
+        updateNeighbors(mUpdatingProbe);
+        mUpdatingProbe = nullptr;
+        mUpdatingFace = 0;
+    }
+}
+
+void LLReflectionMapManager::rebuild()
+{
+    for (auto& probe : mProbes)
+    {
+        probe->mLastUpdateTime = 0.f;
     }
+}
 
-    for (++i; i < maps.size(); ++i)
+void LLReflectionMapManager::shift(const LLVector4a& offset)
+{
+    for (auto& probe : mProbes)
     {
-        maps[i] = nullptr;
+        probe->mOrigin.add(offset);
     }
 }
 
+void LLReflectionMapManager::updateNeighbors(LLReflectionMap* probe)
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+
+    //remove from existing neighbors
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmun - clear");
+    
+        for (auto& other : probe->mNeighbors)
+        {
+            auto& iter = std::find(other->mNeighbors.begin(), other->mNeighbors.end(), probe);
+            llassert(iter != other->mNeighbors.end()); // <--- bug davep if this ever happens, something broke badly
+            other->mNeighbors.erase(iter);
+        }
+
+        probe->mNeighbors.clear();
+    }
+
+    // search for new neighbors
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmun - search");
+        for (auto& other : mProbes)
+        {
+            if (other != probe)
+            {
+                if (probe->intersects(other))
+                {
+                    probe->mNeighbors.push_back(other);
+                    other->mNeighbors.push_back(probe);
+                }
+            }
+        }
+    }
+}
+
+void LLReflectionMapManager::setUniforms()
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+
+    // structure for packing uniform buffer object
+    // see class2/deferred/softenLightF.glsl
+    struct ReflectionProbeData
+    {
+        LLVector4 refSphere[LL_REFLECTION_PROBE_COUNT]; //origin and radius of refmaps in clip space
+        GLint refIndex[LL_REFLECTION_PROBE_COUNT][4];
+        GLint refNeighbor[4096];
+        GLint refmapCount;
+    };
+
+    mReflectionMaps.resize(LL_REFLECTION_PROBE_COUNT);
+    getReflectionMaps(mReflectionMaps);
+
+    ReflectionProbeData rpd;
+
+    // load modelview matrix into matrix 4a
+    LLMatrix4a modelview;
+    modelview.loadu(gGLModelView);
+    LLVector4a oa; // scratch space for transformed origin
+
+    S32 count = 0;
+    U32 nc = 0; // neighbor "cursor" - index into refNeighbor to start writing the next probe's list of neighbors
+
+    for (auto* refmap : mReflectionMaps)
+    {
+        if (refmap == nullptr)
+        {
+            break;
+        }
+
+        llassert(refmap->mProbeIndex == count);
+        llassert(mReflectionMaps[refmap->mProbeIndex] == refmap);
+
+        llassert(refmap->mCubeIndex >= 0); // should always be  true, if not, getReflectionMaps is bugged
+
+        {
+            //LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmsu - refSphere");
+
+            modelview.affineTransform(refmap->mOrigin, oa);
+            rpd.refSphere[count].set(oa.getF32ptr());
+            rpd.refSphere[count].mV[3] = refmap->mRadius;
+        }
+
+        rpd.refIndex[count][0] = refmap->mCubeIndex;
+        llassert(nc % 4 == 0);
+        rpd.refIndex[count][1] = nc / 4;
+
+        S32 ni = nc; // neighbor ("index") - index into refNeighbor to write indices for current reflection probe's neighbors
+        {
+            //LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmsu - refNeighbors");
+            //pack neghbor list
+            for (auto& neighbor : refmap->mNeighbors)
+            {
+                if (ni >= 4096)
+                { // out of space
+                    break;
+                }
+                
+                GLint idx = neighbor->mProbeIndex;
+                if (idx == -1)
+                {
+                    continue;
+                }
+
+                // this neighbor may be sampled
+                rpd.refNeighbor[ni++] = idx;
+            }
+        }
+        
+        if (nc == ni)
+        {
+            //no neighbors, tag as empty
+            rpd.refIndex[count][1] = -1;
+        }
+        else
+        {
+            rpd.refIndex[count][2] = ni - nc;
+
+            // move the cursor forward
+            nc = ni;
+            if (nc % 4 != 0)
+            { // jump to next power of 4 for compatibility with ivec4
+                nc += 4 - (nc % 4);
+            }
+        }
+        
+        
+        count++;
+    }
+
+    rpd.refmapCount = count;
+
+    //copy rpd into uniform buffer object
+    if (mUBO == 0)
+    {
+        glGenBuffersARB(1, &mUBO);
+    }
+
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmsu - update buffer");
+        glBindBufferARB(GL_UNIFORM_BUFFER, mUBO);
+        glBufferDataARB(GL_UNIFORM_BUFFER, sizeof(ReflectionProbeData), &rpd, GL_STREAM_DRAW);
+        glBindBufferARB(GL_UNIFORM_BUFFER, 0);
+    }
+
+    glBindBufferBase(GL_UNIFORM_BUFFER, 1, mUBO);
+}
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index 40e925d9168..24ac40b2647 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -27,9 +27,20 @@
 #pragma once
 
 #include "llreflectionmap.h"
+#include "llrendertarget.h"
+#include "llcubemaparray.h"
 
-class LLReflectionMapManager
+class LLSpatialGroup;
+
+// number of reflection probes to keep in vram
+#define LL_REFLECTION_PROBE_COUNT 256
+
+// reflection probe resolution
+#define LL_REFLECTION_PROBE_RESOLUTION 256
+
+class alignas(16) LLReflectionMapManager
 {
+    LL_ALIGN_NEW
 public:
     // allocate an environment map of the given resolution 
     LLReflectionMapManager();
@@ -40,12 +51,74 @@ class LLReflectionMapManager
     // drop a reflection probe at the specified position in agent space
     void addProbe(const LLVector3& pos);
 
+    // add a probe for the given spatial group
+    LLReflectionMap* addProbe(LLSpatialGroup* group);
+    
     // Populate "maps" with the N most relevant Reflection Maps where N is no more than maps.size()
     // If less than maps.size() ReflectionMaps are available, will assign trailing elements to nullptr.
     //  maps -- presized array of Reflection Map pointers
     void getReflectionMaps(std::vector<LLReflectionMap*>& maps);
 
+    // called by LLSpatialGroup constructor
+    // If spatial group should receive a Reflection Probe, will create one for the specified spatial group
+    LLReflectionMap* registerSpatialGroup(LLSpatialGroup* group);
+
+    // force an update of all probes
+    void rebuild();
+
+    // called on region crossing to "shift" probes into new coordinate frame
+    void shift(const LLVector4a& offset);
+
+private:
+    friend class LLPipeline;
+
+    // delete the probe with the given index in mProbes
+    void deleteProbe(U32 i);
+
+    // get a free cube index
+    // if no cube indices are free, free one starting from the back of the probe list
+    S32 allocateCubeIndex();
+
+    // update the neighbors of the given probe 
+    void updateNeighbors(LLReflectionMap* probe);
+
+    void setUniforms();
+
+    // render target for cube snapshots
+    // used to generate mipmaps without doing a copy-to-texture
+    LLRenderTarget mRenderTarget;
+
+    std::vector<LLRenderTarget> mMipChain;
+
+    // storage for reflection probes
+    LLPointer<LLCubeMapArray> mTexture;
+
+    // array indicating if a particular cubemap is free
+    bool mCubeFree[LL_REFLECTION_PROBE_COUNT];
+
+    // start tracking the given spatial group
+    void trackGroup(LLSpatialGroup* group);
+    
+    // perform an update on the currently updating Probe
+    void doProbeUpdate();
+    
     // list of active reflection maps
-    std::vector<LLReflectionMap> mProbes;
+    std::vector<LLPointer<LLReflectionMap> > mProbes;
+
+    // list of reflection maps to kill
+    std::vector<LLPointer<LLReflectionMap> > mKillList;
+
+    // list of reflection maps to create
+    std::vector<LLPointer<LLReflectionMap> > mCreateList;
+
+    // handle to UBO
+    U32 mUBO = 0;
+
+    // list of maps being used for rendering
+    std::vector<LLReflectionMap*> mReflectionMaps;
+
+    LLReflectionMap* mUpdatingProbe = nullptr;
+    U32 mUpdatingFace = 0;
+
 };
 
diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp
index 5c648c11e1e..5909cd1f4ce 100644
--- a/indra/newview/llspatialpartition.cpp
+++ b/indra/newview/llspatialpartition.cpp
@@ -554,9 +554,12 @@ LLSpatialGroup::LLSpatialGroup(OctreeNode* node, LLSpatialPartition* part) : LLO
 	sg_assert(mOctreeNode->getListenerCount() == 0);
 	setState(SG_INITIAL_STATE_MASK);
 	gPipeline.markRebuild(this, TRUE);
+    
+    // let the reflection map manager know about this spatial group
+    mReflectionProbe = gPipeline.mReflectionMapManager.registerSpatialGroup(this);
 
-	mRadius = 1;
-	mPixelArea = 1024.f;
+    mRadius = 1;
+    mPixelArea = 1024.f;
 }
 
 void LLSpatialGroup::updateDistance(LLCamera &camera)
@@ -735,8 +738,17 @@ BOOL LLSpatialGroup::changeLOD()
 	return FALSE;
 }
 
+void LLSpatialGroup::dirtyReflectionProbe()
+{
+    if (mReflectionProbe != nullptr)
+    {
+        mReflectionProbe->dirty();
+    }
+}
+
 void LLSpatialGroup::handleInsertion(const TreeNode* node, LLViewerOctreeEntry* entry)
 {
+    dirtyReflectionProbe();
 	addObject((LLDrawable*)entry->getDrawable());
 	unbound();
 	setState(OBJECT_DIRTY);
@@ -744,6 +756,7 @@ void LLSpatialGroup::handleInsertion(const TreeNode* node, LLViewerOctreeEntry*
 
 void LLSpatialGroup::handleRemoval(const TreeNode* node, LLViewerOctreeEntry* entry)
 {
+    dirtyReflectionProbe();
 	removeObject((LLDrawable*)entry->getDrawable(), TRUE);
 	LLViewerOctreeGroup::handleRemoval(node, entry);
 }
@@ -780,6 +793,8 @@ void LLSpatialGroup::handleChildAddition(const OctreeNode* parent, OctreeNode* c
 {
 	LL_PROFILE_ZONE_SCOPED_CATEGORY_SPATIAL
 
+    dirtyReflectionProbe();
+
 	if (child->getListenerCount() == 0)
 	{
 		new LLSpatialGroup(child, getSpatialPartition());
@@ -794,6 +809,11 @@ void LLSpatialGroup::handleChildAddition(const OctreeNode* parent, OctreeNode* c
 	assert_states_valid(this);
 }
 
+void LLSpatialGroup::handleChildRemoval(const oct_node* parent, const oct_node* child)
+{
+    dirtyReflectionProbe();
+}
+
 void LLSpatialGroup::destroyGL(bool keep_occlusion) 
 {
 	setState(LLSpatialGroup::GEOM_DIRTY | LLSpatialGroup::IMAGE_DIRTY);
@@ -1398,7 +1418,9 @@ S32 LLSpatialPartition::cull(LLCamera &camera, std::vector<LLDrawable *>* result
 	
 	return 0;
 	}
-	
+
+extern BOOL gCubeSnapshot;
+
 S32 LLSpatialPartition::cull(LLCamera &camera, bool do_occlusion)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_SPATIAL;
@@ -1417,7 +1439,7 @@ S32 LLSpatialPartition::cull(LLCamera &camera, bool do_occlusion)
         LLOctreeCullShadow culler(&camera);
         culler.traverse(mOctree);
     }
-    else if (mInfiniteFarClip || !LLPipeline::sUseFarClip)
+    else if (mInfiniteFarClip || (!LLPipeline::sUseFarClip && !gCubeSnapshot))
     {
         LLOctreeCullNoFarClip culler(&camera);
         culler.traverse(mOctree);
@@ -1737,12 +1759,71 @@ void renderOctree(LLSpatialGroup* group)
 			}
 		}*/
 	}
-	
+
 //	LLSpatialGroup::OctreeNode* node = group->mOctreeNode;
 //	gGL.diffuseColor4f(0,1,0,1);
 //	drawBoxOutline(LLVector3(node->getCenter()), LLVector3(node->getSize()));
 }
 
+void renderReflectionProbes(LLSpatialGroup* group)
+{
+    if (group->mReflectionProbe)
+    { // draw lines from corners of object aabb to reflection probe
+
+        const LLVector4a* bounds = group->getBounds();
+        LLVector4a o = bounds[0];
+
+        gGL.flush();
+        gGL.diffuseColor4f(0, 0, 1, 1);
+        F32* c = o.getF32ptr();
+
+        const F32* bc = bounds[0].getF32ptr();
+        const F32* bs = bounds[1].getF32ptr();
+
+        // daaw blue lines from corners to center of node
+        gGL.begin(gGL.LINES);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] + bs[0], bc[1] + bs[1], bc[2] + bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] - bs[0], bc[1] + bs[1], bc[2] + bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] + bs[0], bc[1] - bs[1], bc[2] + bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] - bs[0], bc[1] - bs[1], bc[2] + bs[2]);
+
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] + bs[0], bc[1] + bs[1], bc[2] - bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] - bs[0], bc[1] + bs[1], bc[2] - bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] + bs[0], bc[1] - bs[1], bc[2] - bs[2]);
+        gGL.vertex3fv(c);
+        gGL.vertex3f(bc[0] - bs[0], bc[1] - bs[1], bc[2] - bs[2]);
+        gGL.end();
+
+        F32* po = group->mReflectionProbe->mOrigin.getF32ptr();
+        //draw yellow line from center of node to reflection probe origin
+        gGL.flush();
+        gGL.diffuseColor4f(1, 1, 0, 1);
+        gGL.begin(gGL.LINES);
+        gGL.vertex3fv(c);
+        gGL.vertex3fv(po);
+        gGL.end();
+        gGL.flush();
+
+        //draw orange line from probe to neighbors
+        gGL.flush();
+        gGL.diffuseColor4f(1, 0.5f, 0, 1);
+        gGL.begin(gGL.LINES);
+        for (auto& probe : group->mReflectionProbe->mNeighbors)
+        {
+            gGL.vertex3fv(po);
+            gGL.vertex3fv(probe->mOrigin.getF32ptr());
+        }
+        gGL.end();
+        gGL.flush();
+    }
+}
 std::set<LLSpatialGroup*> visible_selected_groups;
 
 void renderVisibility(LLSpatialGroup* group, LLCamera* camera)
@@ -3289,6 +3370,12 @@ class LLOctreeRenderNonOccluded : public OctreeTraveler
 				stop_glerror();
 			}
 
+            //draw reflection probes and links between them
+            if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_REFLECTION_PROBES))
+            {
+                renderReflectionProbes(group);
+            }
+
 			//render visibility wireframe
 			if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_OCCLUSION))
 			{
@@ -3703,7 +3790,8 @@ void LLSpatialPartition::renderDebug()
 									  //LLPipeline::RENDER_DEBUG_BUILD_QUEUE |
 									  LLPipeline::RENDER_DEBUG_SHADOW_FRUSTA |
 									  LLPipeline::RENDER_DEBUG_RENDER_COMPLEXITY |
-									  LLPipeline::RENDER_DEBUG_TEXEL_DENSITY)) 
+									  LLPipeline::RENDER_DEBUG_TEXEL_DENSITY |
+                                      LLPipeline::RENDER_DEBUG_REFLECTION_PROBES))
 	{
 		return;
 	}
@@ -3947,6 +4035,23 @@ LLDrawable* LLSpatialPartition::lineSegmentIntersect(const LLVector4a& start, co
 	return drawable;
 }
 
+LLDrawable* LLSpatialGroup::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
+    BOOL pick_transparent,
+    BOOL pick_rigged,
+    S32* face_hit,                   // return the face hit
+    LLVector4a* intersection,         // return the intersection point
+    LLVector2* tex_coord,            // return the texture coordinates of the intersection point
+    LLVector4a* normal,               // return the surface normal at the intersection point
+    LLVector4a* tangent			// return the surface tangent at the intersection point
+)
+
+{
+    LLOctreeIntersect intersect(start, end, pick_transparent, pick_rigged, face_hit, intersection, tex_coord, normal, tangent);
+    LLDrawable* drawable = intersect.check(getOctreeNode());
+
+    return drawable;
+}
+
 LLDrawInfo::LLDrawInfo(U16 start, U16 end, U32 count, U32 offset, 
 					   LLViewerTexture* texture, LLVertexBuffer* buffer,
 					   bool selected,
diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h
index acfcd63686c..a30660b2de4 100644
--- a/indra/newview/llspatialpartition.h
+++ b/indra/newview/llspatialpartition.h
@@ -53,6 +53,7 @@ class LLSpatialPartition;
 class LLSpatialBridge;
 class LLSpatialGroup;
 class LLViewerRegion;
+class LLReflectionMap;
 
 void pushVerts(LLFace* face, U32 mask);
 
@@ -300,6 +301,17 @@ class LLSpatialGroup : public LLOcclusionCullingGroup
 
 	void drawObjectBox(LLColor4 col);
 
+    LLDrawable* lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
+        BOOL pick_transparent,
+        BOOL pick_rigged,
+        S32* face_hit,                          // return the face hit
+        LLVector4a* intersection = NULL,         // return the intersection point
+        LLVector2* tex_coord = NULL,            // return the texture coordinates of the intersection point
+        LLVector4a* normal = NULL,               // return the surface normal at the intersection point
+        LLVector4a* tangent = NULL             // return the surface tangent at the intersection point
+    );
+
+
 	LLSpatialPartition* getSpatialPartition() {return (LLSpatialPartition*)mSpatialPartition;}
 
 	 //LISTENER FUNCTIONS
@@ -307,6 +319,7 @@ class LLSpatialGroup : public LLOcclusionCullingGroup
 	virtual void handleRemoval(const TreeNode* node, LLViewerOctreeEntry* face);
 	virtual void handleDestruction(const TreeNode* node);
 	virtual void handleChildAddition(const OctreeNode* parent, OctreeNode* child);
+    virtual void handleChildRemoval(const oct_node* parent, const oct_node* child);
 
 public:
 	LL_ALIGN_16(LLVector4a mViewAngle);
@@ -314,6 +327,8 @@ class LLSpatialGroup : public LLOcclusionCullingGroup
 
 	F32 mObjectBoxSize; //cached mObjectBounds[1].getLength3()
 
+    void dirtyReflectionProbe();
+
 protected:
 	virtual ~LLSpatialGroup();
 
@@ -338,6 +353,10 @@ class LLSpatialGroup : public LLOcclusionCullingGroup
 	
 	F32 mPixelArea;
 	F32 mRadius;
+
+    // Reflection Probe associated with this node (if any)
+    LLPointer<LLReflectionMap> mReflectionProbe = nullptr;
+
 } LL_ALIGN_POSTFIX(64);
 
 class LLGeometryManager
diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp
index 6a2b06d9b56..f375852dfee 100644
--- a/indra/newview/llviewerdisplay.cpp
+++ b/indra/newview/llviewerdisplay.cpp
@@ -97,6 +97,7 @@ BOOL gResizeScreenTexture = FALSE;
 BOOL gResizeShadowTexture = FALSE;
 BOOL gWindowResized = FALSE;
 BOOL gSnapshot = FALSE;
+BOOL gCubeSnapshot = FALSE;
 BOOL gShaderProfileFrame = FALSE;
 
 // This is how long the sim will try to teleport you before giving up.
@@ -193,15 +194,23 @@ void display_update_camera()
 	// Cut draw distance in half when customizing avatar,
 	// but on the viewer only.
 	F32 final_far = gAgentCamera.mDrawDistance;
-	if (CAMERA_MODE_CUSTOMIZE_AVATAR == gAgentCamera.getCameraMode())
+    if (gCubeSnapshot)
+    {
+        final_far = gSavedSettings.getF32("RenderReflectionProbeDrawDistance");
+    }
+    else if (CAMERA_MODE_CUSTOMIZE_AVATAR == gAgentCamera.getCameraMode())
+        
 	{
 		final_far *= 0.5f;
 	}
 	LLViewerCamera::getInstance()->setFar(final_far);
 	gViewerWindow->setup3DRender();
 	
-	// Update land visibility too
-	LLWorld::getInstance()->setLandFarClip(final_far);
+    if (!gCubeSnapshot)
+    {
+        // Update land visibility too
+        LLWorld::getInstance()->setLandFarClip(final_far);
+    }
 }
 
 // Write some stats to LL_INFOS()
@@ -1048,6 +1057,112 @@ void display(BOOL rebuild, F32 zoom_factor, int subfield, BOOL for_snapshot)
 	}
 }
 
+// WIP simplified copy of display() that does minimal work
+void display_cube_face()
+{
+    LL_RECORD_BLOCK_TIME(FTM_RENDER);
+    llassert(!gSnapshot);
+    llassert(!gTeleportDisplay);
+    llassert(LLPipeline::sRenderDeferred);
+    llassert(LLStartUp::getStartupState() >= STATE_PRECACHE);
+    llassert(!LLAppViewer::instance()->logoutRequestSent());
+    llassert(!gRestoreGL);
+    llassert(!gUseWireframe);
+
+    bool rebuild = false;
+
+    LLGLSDefault gls_default;
+    LLGLDepthTest gls_depth(GL_TRUE, GL_TRUE, GL_LEQUAL);
+
+    LLVertexBuffer::unbind();
+
+    gPipeline.disableLights();
+
+    gPipeline.mBackfaceCull = TRUE;
+
+    LLViewerCamera::getInstance()->setNear(MIN_NEAR_PLANE);
+    gViewerWindow->setup3DViewport();
+
+    if (gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_HUD))
+    { //don't draw hud objects in this frame
+        gPipeline.toggleRenderType(LLPipeline::RENDER_TYPE_HUD);
+    }
+
+    if (gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_HUD_PARTICLES))
+    { //don't draw hud particles in this frame
+        gPipeline.toggleRenderType(LLPipeline::RENDER_TYPE_HUD_PARTICLES);
+    }
+
+    display_update_camera();
+
+    LLSpatialGroup::sNoDelete = TRUE;
+        
+    S32 occlusion = LLPipeline::sUseOcclusion;
+    LLPipeline::sUseOcclusion = 0; // occlusion data is from main camera point of view, don't read or write it during cube snapshots
+    //gDepthDirty = TRUE; //let "real" render pipe know it can't trust the depth buffer for occlusion data
+
+    static LLCullResult result;
+    LLViewerCamera::sCurCameraID = LLViewerCamera::CAMERA_WORLD;
+    LLPipeline::sUnderWaterRender = LLViewerCamera::getInstance()->cameraUnderWater();
+    gPipeline.updateCull(*LLViewerCamera::getInstance(), result);
+
+    gGL.setColorMask(true, true);
+    glClearColor(0, 0, 0, 0);
+    gPipeline.generateSunShadow(*LLViewerCamera::getInstance());
+        
+    glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+    {
+        LLViewerCamera::sCurCameraID = LLViewerCamera::CAMERA_WORLD;
+        gPipeline.stateSort(*LLViewerCamera::getInstance(), result);
+
+        if (rebuild)
+        {
+            //////////////////////////////////////
+            //
+            // rebuildPools
+            //
+            //
+            gPipeline.rebuildPools();
+            stop_glerror();
+        }
+    }
+    
+    LLPipeline::sUseOcclusion = occlusion;
+
+    LLAppViewer::instance()->pingMainloopTimeout("Display:RenderStart");
+
+    LLPipeline::sUnderWaterRender = LLViewerCamera::getInstance()->cameraUnderWater() ? TRUE : FALSE;
+
+    gGL.setColorMask(true, true);
+
+    gPipeline.mDeferredScreen.bindTarget();
+    glClearColor(1, 0, 1, 1);
+    gPipeline.mDeferredScreen.clear();
+        
+    gGL.setColorMask(true, false);
+
+    LLViewerCamera::sCurCameraID = LLViewerCamera::CAMERA_WORLD;
+
+    gPipeline.renderGeomDeferred(*LLViewerCamera::getInstance());
+
+    gGL.setColorMask(true, true);
+
+    gPipeline.mDeferredScreen.flush();
+       
+    gPipeline.renderDeferredLighting(&gPipeline.mScreen);
+
+    LLPipeline::sUnderWaterRender = FALSE;
+
+    // Finalize scene
+    gPipeline.renderFinalize();
+
+    LLSpatialGroup::sNoDelete = FALSE;
+    gPipeline.clearReferences();
+
+    gPipeline.rebuildGroups();
+}
+
 void render_hud_attachments()
 {
 	gGL.matrixMode(LLRender::MM_PROJECTION);
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index f5ea060e82f..8732bde35c9 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -1081,6 +1081,10 @@ U64 info_display_from_string(std::string info_display)
 	{
 		return LLPipeline::RENDER_DEBUG_IMPOSTORS;
 	}
+    else if ("reflection probes" == info_display)
+    {
+    return LLPipeline::RENDER_DEBUG_REFLECTION_PROBES;
+    }
 	else
 	{
 		LL_WARNS() << "unrecognized feature name '" << info_display << "'" << LL_ENDL;
@@ -8304,9 +8308,9 @@ void handle_cache_clear_immediately()
 	LLNotificationsUtil::add("ConfirmClearCache", LLSD(), LLSD(), callback_clear_cache_immediately);
 }
 
-void handle_override_environment_map()
+void handle_rebuild_reflection_probes()
 {
-    gPipeline.overrideEnvironmentMap();
+    gPipeline.mReflectionMapManager.rebuild();
 }
 
 
@@ -9409,7 +9413,7 @@ void initialize_menus()
 	//Develop (clear cache immediately)
 	commit.add("Develop.ClearCache", boost::bind(&handle_cache_clear_immediately) );
     //Develop (override environment map)
-    commit.add("Develop.OverrideEnvironmentMap", boost::bind(&handle_override_environment_map));
+    commit.add("Develop.RebuildReflectionProbes", boost::bind(&handle_rebuild_reflection_probes));
 
 	// Admin >Object
 	view_listener_t::addMenu(new LLAdminForceTakeCopy(), "Admin.ForceTakeCopy");
diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp
index 50a0ff07fc2..94af320a73b 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -82,6 +82,7 @@ LLGLSLShader	gOcclusionCubeProgram;
 LLGLSLShader	gCustomAlphaProgram;
 LLGLSLShader	gGlowCombineProgram;
 LLGLSLShader	gSplatTextureRectProgram;
+LLGLSLShader	gReflectionMipProgram;
 LLGLSLShader	gGlowCombineFXAAProgram;
 LLGLSLShader	gTwoTextureAddProgram;
 LLGLSLShader	gTwoTextureCompareProgram;
@@ -635,7 +636,6 @@ void LLViewerShaderMgr::setShaders()
     }
 
     if (loaded)
-
     {
         loaded = loadTransformShaders();
         if (loaded)
@@ -746,6 +746,7 @@ void LLViewerShaderMgr::unloadShaders()
 	gCustomAlphaProgram.unload();
 	gGlowCombineProgram.unload();
 	gSplatTextureRectProgram.unload();
+    gReflectionMipProgram.unload();
 	gGlowCombineFXAAProgram.unload();
 	gTwoTextureAddProgram.unload();
 	gTwoTextureCompareProgram.unload();
@@ -3587,7 +3588,6 @@ BOOL LLViewerShaderMgr::loadShadersInterface()
 		}
 	}
 
-
 	if (success)
 	{
 		gTwoTextureAddProgram.mName = "Two Texture Add Shader";
@@ -3763,6 +3763,22 @@ BOOL LLViewerShaderMgr::loadShadersInterface()
 		success = gAlphaMaskProgram.createShader(NULL, NULL);
 	}
 
+    if (success)
+    {
+        gReflectionMipProgram.mName = "Reflection Mip Shader";
+        gReflectionMipProgram.mShaderFiles.clear();
+        gReflectionMipProgram.mShaderFiles.push_back(make_pair("interface/splattexturerectV.glsl", GL_VERTEX_SHADER_ARB));
+        gReflectionMipProgram.mShaderFiles.push_back(make_pair("interface/reflectionmipF.glsl", GL_FRAGMENT_SHADER_ARB));
+        gReflectionMipProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE];
+        success = gReflectionMipProgram.createShader(NULL, NULL);
+        if (success)
+        {
+            gReflectionMipProgram.bind();
+            gReflectionMipProgram.uniform1i(sScreenMap, 0);
+            gReflectionMipProgram.unbind();
+        }
+    }
+
 	if( !success )
 	{
 		mShaderLevel[SHADER_INTERFACE] = 0;
diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h
index 50a3daebaa1..f0187db3027 100644
--- a/indra/newview/llviewershadermgr.h
+++ b/indra/newview/llviewershadermgr.h
@@ -161,6 +161,7 @@ extern LLGLSLShader			gOcclusionCubeProgram;
 extern LLGLSLShader			gCustomAlphaProgram;
 extern LLGLSLShader			gGlowCombineProgram;
 extern LLGLSLShader			gSplatTextureRectProgram;
+extern LLGLSLShader			gReflectionMipProgram;
 extern LLGLSLShader			gGlowCombineFXAAProgram;
 extern LLGLSLShader			gDebugProgram;
 extern LLGLSLShader			gClipProgram;
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index f562a9458b2..9bda4bbf926 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -228,6 +228,7 @@ extern BOOL gDebugClicks;
 extern BOOL gDisplaySwapBuffers;
 extern BOOL gDepthDirty;
 extern BOOL gResizeScreenTexture;
+extern BOOL gCubeSnapshot;
 
 LLViewerWindow	*gViewerWindow = NULL;
 
@@ -5267,29 +5268,30 @@ BOOL LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_
     return true;
 }
 
-BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap)
+void display_cube_face();
+
+BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMapArray* cubearray, S32 cubeIndex, S32 face)
 {
     // NOTE: implementation derived from LLFloater360Capture::capture360Images() and simpleSnapshot
     LL_PROFILE_ZONE_SCOPED_CATEGORY_APP;
     llassert(LLPipeline::sRenderDeferred);
+    llassert(!gCubeSnapshot); //assert a snapshot isn't already in progress
     
-    U32 res = cubemap->getResolution();
+    U32 res = LLRenderTarget::sCurResX;
 
     llassert(res <= gPipeline.mDeferredScreen.getWidth());
     llassert(res <= gPipeline.mDeferredScreen.getHeight());
 
-    
-
     // save current view/camera settings so we can restore them afterwards
     S32 old_occlusion = LLPipeline::sUseOcclusion;
 
     // set new parameters specific to the 360 requirements
     LLPipeline::sUseOcclusion = 0;
     LLViewerCamera* camera = LLViewerCamera::getInstance();
-    LLVector3 old_origin = camera->getOrigin();
-    F32 old_fov = camera->getView();
-    F32 old_aspect = camera->getAspect();
-    F32 old_yaw = camera->getYaw();
+    
+    LLViewerCamera saved_camera = LLViewerCamera::instance();
+    glh::matrix4f saved_proj = get_current_projection();
+    glh::matrix4f saved_mod = get_current_modelview();
 
     // camera constants for the square, cube map capture image
     camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV
@@ -5297,8 +5299,6 @@ BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap)
     camera->yaw(0.0);
     camera->setOrigin(origin);
 
-    gDisplaySwapBuffers = FALSE;
-
     glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
     
     BOOL prev_draw_ui = gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI) ? TRUE : FALSE;
@@ -5306,55 +5306,38 @@ BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap)
     {
         LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI);
     }
-
-    LLPipeline::sShowHUDAttachments = FALSE;
-    LLRect window_rect = getWorldViewRectRaw();
-
-    LLRenderTarget scratch_space;  // TODO: hold onto "scratch space" render target and allocate oncer per session (allocate takes > 1ms)
-    U32 color_fmt = GL_RGBA;
-    const bool use_depth_buffer = true;
-    const bool use_stencil_buffer = true;
-    if (scratch_space.allocate(res, res, color_fmt, use_depth_buffer, use_stencil_buffer))
-    {
-        mWorldViewRectRaw.set(0, res, res, 0);
-        scratch_space.bindTarget();
-    }
-    else
+    BOOL prev_draw_particles = gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_PARTICLES);
+    if (prev_draw_particles)
     {
-        return FALSE;
+        gPipeline.toggleRenderType(LLPipeline::RENDER_TYPE_PARTICLES);
     }
+    LLPipeline::sShowHUDAttachments = FALSE;
+    LLRect window_rect = getWorldViewRectRaw();
 
-    // "target" parameter of glCopyTexImage2D for each side of cubemap
-    U32 targets[6] = {
-        GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB,
-        GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB,
-        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB,
-        GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB,
-        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB,
-        GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB
-    };
+    mWorldViewRectRaw.set(0, res, res, 0);
 
-    // these are the 6 directions we will point the camera, see LLCubeMap::mTargets
+    // these are the 6 directions we will point the camera, see LLCubeMapArray::sTargets
     LLVector3 look_dirs[6] = {
-        LLVector3(-1, 0, 0),
         LLVector3(1, 0, 0),
-        LLVector3(0, -1, 0),
+        LLVector3(-1, 0, 0),
         LLVector3(0, 1, 0),
-        LLVector3(0, 0, -1),
-        LLVector3(0, 0, 1)
+        LLVector3(0, -1, 0),
+        LLVector3(0, 0, 1),
+        LLVector3(0, 0, -1)
     };
 
     LLVector3 look_upvecs[6] = {
         LLVector3(0, -1, 0),
         LLVector3(0, -1, 0),
-        LLVector3(0, 0, -1),
         LLVector3(0, 0, 1),
+        LLVector3(0, 0, -1),
         LLVector3(0, -1, 0),
         LLVector3(0, -1, 0)
     };
 
     // for each of six sides of cubemap
-    for (int i = 0; i < 6; ++i)
+    //for (int i = 0; i < 6; ++i)
+    int i = face;
     {
         // set up camera to look in each direction
         camera->lookDir(look_dirs[i], look_upvecs[i]);
@@ -5368,22 +5351,12 @@ BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap)
         gDisplaySwapBuffers = FALSE;
 
         // actually render the scene
-        const U32 subfield = 0;
-        const bool do_rebuild = true;
-        const F32 zoom = 1.0;
-        const bool for_snapshot = TRUE;
-        display(do_rebuild, zoom, subfield, for_snapshot);
-
-        // copy results to cube map face
-        cubemap->enable(0);
-        cubemap->bind();
-        glCopyTexImage2D(targets[i], 0, GL_RGB, 0, 0, res, res, 0);
-        gGL.getTexUnit(0)->disable();
-        cubemap->disable();
+        gCubeSnapshot = TRUE;
+        display_cube_face();
+        gCubeSnapshot = FALSE;
     }
 
-    gDisplaySwapBuffers = FALSE;
-    gDepthDirty = TRUE;
+    gDisplaySwapBuffers = TRUE;
 
     if (!gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI))
     {
@@ -5393,23 +5366,23 @@ BOOL LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap)
         }
     }
 
+    if (prev_draw_particles)
+    {
+        gPipeline.toggleRenderType(LLPipeline::RENDER_TYPE_PARTICLES);
+    }
+
     LLPipeline::sShowHUDAttachments = TRUE;
 
     gPipeline.resetDrawOrders();
     mWorldViewRectRaw = window_rect;
-    scratch_space.flush();
-    scratch_space.release();
     
     // restore original view/camera/avatar settings settings
-    camera->setAspect(old_aspect);
-    camera->setView(old_fov);
-    camera->yaw(old_yaw);
-    camera->setOrigin(old_origin);
-
+    *camera = saved_camera;
+    set_current_modelview(saved_mod);
+    set_current_projection(saved_proj);
     LLPipeline::sUseOcclusion = old_occlusion;
 
     // ====================================================
-
     return true;
 }
 
diff --git a/indra/newview/llviewerwindow.h b/indra/newview/llviewerwindow.h
index be4e9a17a53..ac7f8b2e390 100644
--- a/indra/newview/llviewerwindow.h
+++ b/indra/newview/llviewerwindow.h
@@ -69,6 +69,7 @@ class LLViewerWindowListener;
 class LLVOPartGroup;
 class LLPopupView;
 class LLCubeMap;
+class LLCubeMapArray;
 
 #define PICK_HALF_WIDTH 5
 #define PICK_DIAMETER (2 * PICK_HALF_WIDTH + 1)
@@ -365,8 +366,9 @@ class LLViewerWindow : public LLWindowCallbacks
     
     // take a cubemap snapshot
     // origin - vantage point to take the snapshot from
-    // cubemap - cubemap to store the results
-    BOOL cubeSnapshot(const LLVector3& origin, LLCubeMap* cubemap);
+    // cubearray - cubemap array for storing the results
+    // index - cube index in the array to use (cube index, not face-layer)
+    BOOL cubeSnapshot(const LLVector3& origin, LLCubeMapArray* cubearray, S32 index, S32 face);
 
     
     // special implementation of simpleSnapshot for reflection maps
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index 17d92fda38b..ef6ee4b910c 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -106,6 +106,7 @@ LLPointer<LLObjectMediaDataClient> LLVOVolume::sObjectMediaClient = NULL;
 LLPointer<LLObjectMediaNavigateClient> LLVOVolume::sObjectMediaNavigateClient = NULL;
 
 extern BOOL gGLDebugLoggingEnabled;
+extern BOOL gCubeSnapshot;
 
 // Implementation class of LLMediaDataClientObject.  See llmediadataclient.h
 class LLMediaDataClientObjectImpl : public LLMediaDataClientObject
@@ -718,7 +719,7 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
     LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME;
 	// Update the pixel area of all faces
 
-    if (mDrawable.isNull())
+    if (mDrawable.isNull() || gCubeSnapshot)
     {
         return;
     }
@@ -3388,6 +3389,11 @@ F32 LLVOVolume::getSpotLightPriority() const
 
 void LLVOVolume::updateSpotLightPriority()
 {
+    if (gCubeSnapshot)
+    {
+        return;
+    }
+
     F32 r = getLightRadius();
 	LLVector3 pos = mDrawable->getPositionAgent();
 
@@ -5497,6 +5503,8 @@ static inline void add_face(T*** list, U32* count, T* face)
 void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME;
+    llassert(!gCubeSnapshot);
+
 	if (group->changeLOD())
 	{
 		group->mLastUpdateDistance = group->mDistance;
diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp
index 8abb49fba8b..8837038d02b 100644
--- a/indra/newview/llworld.cpp
+++ b/indra/newview/llworld.cpp
@@ -820,7 +820,7 @@ void LLWorld::updateNetStats()
 
 void LLWorld::printPacketsLost()
 {
-	LL_INFOS() << "Simulators:" << LL_ENDL;
+	LL_INFOS() << "Simulators:" << LL_ENDL; 
 	LL_INFOS() << "----------" << LL_ENDL;
 
 	LLCircuitData *cdp = NULL;
@@ -855,6 +855,7 @@ F32 LLWorld::getLandFarClip() const
 
 void LLWorld::setLandFarClip(const F32 far_clip)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT;
 	static S32 const rwidth = (S32)REGION_WIDTH_U32;
 	S32 const n1 = (llceil(mLandFarClip) - 1) / rwidth;
 	S32 const n2 = (llceil(far_clip) - 1) / rwidth;
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index ef616f5d835..5eb9817fc4f 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -227,6 +227,7 @@ extern S32 gBoxFrame;
 //extern BOOL gHideSelectedObjects;
 extern BOOL gDisplaySwapBuffers;
 extern BOOL gDebugGL;
+extern BOOL gCubeSnapshot;
 
 bool	gAvatarBacklight = false;
 
@@ -2006,6 +2007,7 @@ void LLPipeline::updateMove()
 //static
 F32 LLPipeline::calcPixelArea(LLVector3 center, LLVector3 size, LLCamera &camera)
 {
+    llassert(!gCubeSnapshot); // shouldn't be doing ANY of this during cube snap shots
 	LLVector3 lookAt = center - camera.getOrigin();
 	F32 dist = lookAt.length();
 
@@ -2475,7 +2477,7 @@ void LLPipeline::markNotCulled(LLSpatialGroup* group, LLCamera& camera)
 	
 	group->setVisible();
 
-	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD)
+	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot)
 	{
 		group->updateDistance(camera);
 	}
@@ -2581,6 +2583,8 @@ void LLPipeline::downsampleDepthBuffer(LLRenderTarget& source, LLRenderTarget& d
 
 void LLPipeline::doOcclusion(LLCamera& camera, LLRenderTarget& source, LLRenderTarget& dest, LLRenderTarget* scratch_space)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    llassert(!gCubeSnapshot);
 	downsampleDepthBuffer(source, dest, scratch_space);
 	dest.bindTarget();
 	doOcclusion(camera);
@@ -2823,7 +2827,7 @@ void LLPipeline::rebuildPriorityGroups()
 
 void LLPipeline::rebuildGroups()
 {
-	if (mGroupQ2.empty())
+	if (mGroupQ2.empty() || gCubeSnapshot)
 	{
 		return;
 	}
@@ -2873,6 +2877,10 @@ void LLPipeline::updateGeom(F32 max_dtime)
 	LLPointer<LLDrawable> drawablep;
 
 	LL_RECORD_BLOCK_TIME(FTM_GEO_UPDATE);
+    if (gCubeSnapshot)
+    {
+        return;
+    }
 
 	assertInitialized();
 
@@ -3112,6 +3120,8 @@ void LLPipeline::shiftObjects(const LLVector3 &offset)
 		}
 	}
 
+    mReflectionMapManager.shift(offseta);
+
 	LLHUDText::shiftAll(offset);
 	LLHUDNameTag::shiftAll(offset);
 
@@ -3292,7 +3302,7 @@ void LLPipeline::stateSort(LLCamera& camera, LLCullResult &result)
 		}
 	}
 
-	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD)
+	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot)
 	{
 		LLSpatialGroup* last_group = NULL;
 		BOOL fov_changed = LLViewerCamera::getInstance()->isDefaultFOVChanged();
@@ -3374,7 +3384,7 @@ void LLPipeline::stateSort(LLSpatialGroup* group, LLCamera& camera)
 			stateSort(drawablep, camera);
 		}
 
-		if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD)
+		if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot)
 		{ //avoid redundant stateSort calls
 			group->mLastUpdateDistance = group->mDistance;
 		}
@@ -3443,7 +3453,7 @@ void LLPipeline::stateSort(LLDrawable* drawablep, LLCamera& camera)
 		}
 	}
 
-	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD)
+	if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot)
 	{
 		//if (drawablep->isVisible()) isVisible() check here is redundant, if it wasn't visible, it wouldn't be here
 		{
@@ -3721,21 +3731,26 @@ void LLPipeline::postSort(LLCamera& camera)
 	assertInitialized();
 
 	LL_PUSH_CALLSTACKS();
-	//rebuild drawable geometry
-	for (LLCullResult::sg_iterator i = sCull->beginDrawableGroups(); i != sCull->endDrawableGroups(); ++i)
-	{
-		LLSpatialGroup* group = *i;
-		if (!sUseOcclusion || 
-			!group->isOcclusionState(LLSpatialGroup::OCCLUDED))
-		{
-			group->rebuildGeom();
-		}
-	}
-	LL_PUSH_CALLSTACKS();
-	//rebuild groups
-	sCull->assertDrawMapsEmpty();
 
-	rebuildPriorityGroups();
+    if (!gCubeSnapshot)
+    {
+        //rebuild drawable geometry
+        for (LLCullResult::sg_iterator i = sCull->beginDrawableGroups(); i != sCull->endDrawableGroups(); ++i)
+        {
+            LLSpatialGroup* group = *i;
+            if (!sUseOcclusion ||
+                !group->isOcclusionState(LLSpatialGroup::OCCLUDED))
+            {
+                group->rebuildGeom();
+            }
+        }
+        LL_PUSH_CALLSTACKS();
+        //rebuild groups
+        sCull->assertDrawMapsEmpty();
+
+        rebuildPriorityGroups();
+    }
+
 	LL_PUSH_CALLSTACKS();
 
 	
@@ -3751,7 +3766,7 @@ void LLPipeline::postSort(LLCamera& camera)
 			continue;
 		}
 
-		if (group->hasState(LLSpatialGroup::NEW_DRAWINFO) && group->hasState(LLSpatialGroup::GEOM_DIRTY))
+		if (group->hasState(LLSpatialGroup::NEW_DRAWINFO) && group->hasState(LLSpatialGroup::GEOM_DIRTY) && !gCubeSnapshot)
 		{ //no way this group is going to be drawable without a rebuild
 			group->rebuildGeom();
 		}
@@ -3769,7 +3784,7 @@ void LLPipeline::postSort(LLCamera& camera)
                 LLDrawInfo* info = *k;
 				
 				sCull->pushDrawInfo(j->first, info);
-                if (!sShadowRender && !sReflectionRender)
+                if (!sShadowRender && !sReflectionRender && !gCubeSnapshot)
                 {
                     touchTextures(info);
                     addTrianglesDrawn(info->mCount, info->mDrawMode);
@@ -3784,7 +3799,7 @@ void LLPipeline::postSort(LLCamera& camera)
 			if (alpha != group->mDrawMap.end())
 			{ //store alpha groups for sorting
 				LLSpatialBridge* bridge = group->getSpatialPartition()->asBridge();
-				if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD)
+				if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot)
 				{
 					if (bridge)
 					{
@@ -4411,7 +4426,7 @@ void LLPipeline::renderGeom(LLCamera& camera, bool forceVBOUpdate)
 		gGL.loadMatrix(gGLModelView);
 
 		if (occlude)
-		{
+		{ // catch uncommon condition where pools at drawpool grass and later are disabled
 			occlude = false;
 			gGLLastMatrix = NULL;
 			gGL.loadMatrix(gGLModelView);
@@ -4607,6 +4622,7 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera, bool do_occlusion)
 
 		if (occlude && cur_type >= LLDrawPool::POOL_GRASS)
 		{
+            llassert(!gCubeSnapshot); // never do occlusion culling on cube snapshots
 			occlude = false;
 			gGLLastMatrix = NULL;
 			gGL.loadMatrix(gGLModelView);
@@ -5925,6 +5941,7 @@ void LLPipeline::setupAvatarLights(bool for_edit)
 
 static F32 calc_light_dist(LLVOVolume* light, const LLVector3& cam_pos, F32 max_dist)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL;
 	F32 inten = light->getLightIntensity();
 	if (inten < .001f)
 	{
@@ -5948,9 +5965,10 @@ static F32 calc_light_dist(LLVOVolume* light, const LLVector3& cam_pos, F32 max_
 
 void LLPipeline::calcNearbyLights(LLCamera& camera)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL;
 	assertInitialized();
 
-	if (LLPipeline::sReflectionRender)
+	if (LLPipeline::sReflectionRender || gCubeSnapshot)
 	{
 		return;
 	}
@@ -6133,6 +6151,7 @@ void LLPipeline::calcNearbyLights(LLCamera& camera)
 
 void LLPipeline::setupHWLights(LLDrawPool* pool)
 {
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL;
 	assertInitialized();
 	
     LLEnvironment& environment = LLEnvironment::instance();
@@ -7608,7 +7627,8 @@ void LLPipeline::renderFinalize()
 
         bool dof_enabled = !LLViewerCamera::getInstance()->cameraUnderWater() &&
                            (RenderDepthOfFieldInEditMode || !LLToolMgr::getInstance()->inBuildMode()) &&
-                           RenderDepthOfField;
+                           RenderDepthOfField &&
+                            !gCubeSnapshot;
 
         bool multisample = RenderFSAASamples > 1 && mFXAABuffer.isComplete();
 
@@ -8196,45 +8216,13 @@ void LLPipeline::bindDeferredShader(LLGLSLShader& shader, LLRenderTarget* light_
 		}
 	}
 
-    channel = shader.enableTexture(LLShaderMgr::REFLECTION_MAP, LLTexUnit::TT_CUBE_MAP);
-    if (channel > -1)
+    channel = shader.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
+    if (channel > -1 && mReflectionMapManager.mTexture.notNull())
     {
-        mReflectionMaps.resize(8); //TODO -- declare the number of reflection maps the shader knows about somewhere sane
-        mReflectionMapManager.getReflectionMaps(mReflectionMaps);
-        LLVector3 origin[8]; //origin of refmaps in clip space
-
-        // load modelview matrix into matrix 4a
-        LLMatrix4a modelview;
-        modelview.loadu(gGLModelView);
-        LLVector4a oa; // scratch space for transformed origin
-
-        S32 count = 0;
-        for (auto* refmap : mReflectionMaps)
-        {
-            if (refmap)
-            {
-                LLCubeMap* cubemap = refmap->mCubeMap;
-                if (cubemap)
-                {
-                    cubemap->enable(channel + count);
-                    cubemap->bind();
-
-                    modelview.affineTransform(refmap->mOrigin, oa);
-                    origin[count].set(oa.getF32ptr());
-
-                    count++;
-                }
-            }
-        }
-
-        if (count > 0)
-        {
-            LLStaticHashedString refmapCount("refmapCount");
-            LLStaticHashedString refOrigin("refOrigin");
-            shader.uniform1i(refmapCount, count);
-            shader.uniform3fv(refOrigin, count, (F32*)origin);
-            setup_env_mat = true;
-        }
+        // see comments in class2/deferred/softenLightF.glsl for what these uniforms mean
+        mReflectionMapManager.mTexture->bind(channel);
+        mReflectionMapManager.setUniforms();
+        setup_env_mat = true;
     }
 
     if (setup_env_mat)
@@ -8276,7 +8264,14 @@ void LLPipeline::bindDeferredShader(LLGLSLShader& shader, LLRenderTarget* light_
         }
     }
 
-	shader.uniform4fv(LLShaderMgr::DEFERRED_SHADOW_CLIP, 1, mSunClipPlanes.mV);
+    /*if (gCubeSnapshot)
+    { // we only really care about the first two values, but the shader needs increasing separation between clip planes
+        shader.uniform4f(LLShaderMgr::DEFERRED_SHADOW_CLIP, 1.f, 64.f, 128.f, 256.f);
+    }
+    else*/
+    {
+        shader.uniform4fv(LLShaderMgr::DEFERRED_SHADOW_CLIP, 1, mSunClipPlanes.mV);
+    }
 	shader.uniform1f(LLShaderMgr::DEFERRED_SUN_WASH, RenderDeferredSunWash);
 	shader.uniform1f(LLShaderMgr::DEFERRED_SHADOW_NOISE, RenderShadowNoise);
 	shader.uniform1f(LLShaderMgr::DEFERRED_BLUR_SIZE, RenderShadowBlurSize);
@@ -8469,66 +8464,78 @@ void LLPipeline::renderDeferredLighting(LLRenderTarget *screen_target)
         }
 
         if (RenderDeferredSSAO)
-        {  // soften direct lighting lightmap
-            LL_PROFILE_ZONE_NAMED_CATEGORY_PIPELINE("renderDeferredLighting - soften shadow");
-            // blur lightmap
-            screen_target->bindTarget();
-            glClearColor(1, 1, 1, 1);
-            screen_target->clear(GL_COLOR_BUFFER_BIT);
-            glClearColor(0, 0, 0, 0);
-
-            bindDeferredShader(gDeferredBlurLightProgram);
-            mDeferredVB->setBuffer(LLVertexBuffer::MAP_VERTEX);
-            LLVector3 go          = RenderShadowGaussian;
-            const U32 kern_length = 4;
-            F32       blur_size   = RenderShadowBlurSize;
-            F32       dist_factor = RenderShadowBlurDistFactor;
-
-            // sample symmetrically with the middle sample falling exactly on 0.0
-            F32 x = 0.f;
-
-            LLVector3 gauss[32];  // xweight, yweight, offset
-
-            for (U32 i = 0; i < kern_length; i++)
-            {
-                gauss[i].mV[0] = llgaussian(x, go.mV[0]);
-                gauss[i].mV[1] = llgaussian(x, go.mV[1]);
-                gauss[i].mV[2] = x;
-                x += 1.f;
+        {
+            /*if (gCubeSnapshot)
+            { // SSAO and shadows disabled in reflection maps
+                deferred_light_target->bindTarget();
+                glClearColor(1, 1, 1, 1);
+                deferred_light_target->clear();
+                glClearColor(0, 0, 0, 0);
+                deferred_light_target->flush();
             }
+            else*/
+            {
+                // soften direct lighting lightmap
+                LL_PROFILE_ZONE_NAMED_CATEGORY_PIPELINE("renderDeferredLighting - soften shadow");
+                // blur lightmap
+                screen_target->bindTarget();
+                glClearColor(1, 1, 1, 1);
+                screen_target->clear(GL_COLOR_BUFFER_BIT);
+                glClearColor(0, 0, 0, 0);
 
-            gDeferredBlurLightProgram.uniform2f(sDelta, 1.f, 0.f);
-            gDeferredBlurLightProgram.uniform1f(sDistFactor, dist_factor);
-            gDeferredBlurLightProgram.uniform3fv(sKern, kern_length, gauss[0].mV);
-            gDeferredBlurLightProgram.uniform1f(sKernScale, blur_size * (kern_length / 2.f - 0.5f));
+                bindDeferredShader(gDeferredBlurLightProgram);
+                mDeferredVB->setBuffer(LLVertexBuffer::MAP_VERTEX);
+                LLVector3 go = RenderShadowGaussian;
+                const U32 kern_length = 4;
+                F32       blur_size = RenderShadowBlurSize;
+                F32       dist_factor = RenderShadowBlurDistFactor;
 
-            {
-                LLGLDisable   blend(GL_BLEND);
-                LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS);
-                stop_glerror();
-                mDeferredVB->drawArrays(LLRender::TRIANGLES, 0, 3);
-                stop_glerror();
-            }
+                // sample symmetrically with the middle sample falling exactly on 0.0
+                F32 x = 0.f;
+
+                LLVector3 gauss[32];  // xweight, yweight, offset
 
-            screen_target->flush();
-            unbindDeferredShader(gDeferredBlurLightProgram);
+                for (U32 i = 0; i < kern_length; i++)
+                {
+                    gauss[i].mV[0] = llgaussian(x, go.mV[0]);
+                    gauss[i].mV[1] = llgaussian(x, go.mV[1]);
+                    gauss[i].mV[2] = x;
+                    x += 1.f;
+                }
 
-            bindDeferredShader(gDeferredBlurLightProgram, screen_target);
+                gDeferredBlurLightProgram.uniform2f(sDelta, 1.f, 0.f);
+                gDeferredBlurLightProgram.uniform1f(sDistFactor, dist_factor);
+                gDeferredBlurLightProgram.uniform3fv(sKern, kern_length, gauss[0].mV);
+                gDeferredBlurLightProgram.uniform1f(sKernScale, blur_size * (kern_length / 2.f - 0.5f));
 
-            mDeferredVB->setBuffer(LLVertexBuffer::MAP_VERTEX);
-            deferred_light_target->bindTarget();
+                {
+                    LLGLDisable   blend(GL_BLEND);
+                    LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS);
+                    stop_glerror();
+                    mDeferredVB->drawArrays(LLRender::TRIANGLES, 0, 3);
+                    stop_glerror();
+                }
 
-            gDeferredBlurLightProgram.uniform2f(sDelta, 0.f, 1.f);
+                screen_target->flush();
+                unbindDeferredShader(gDeferredBlurLightProgram);
 
-            {
-                LLGLDisable   blend(GL_BLEND);
-                LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS);
-                stop_glerror();
-                mDeferredVB->drawArrays(LLRender::TRIANGLES, 0, 3);
-                stop_glerror();
+                bindDeferredShader(gDeferredBlurLightProgram, screen_target);
+
+                mDeferredVB->setBuffer(LLVertexBuffer::MAP_VERTEX);
+                deferred_light_target->bindTarget();
+
+                gDeferredBlurLightProgram.uniform2f(sDelta, 0.f, 1.f);
+
+                {
+                    LLGLDisable   blend(GL_BLEND);
+                    LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS);
+                    stop_glerror();
+                    mDeferredVB->drawArrays(LLRender::TRIANGLES, 0, 3);
+                    stop_glerror();
+                }
+                deferred_light_target->flush();
+                unbindDeferredShader(gDeferredBlurLightProgram);
             }
-            deferred_light_target->flush();
-            unbindDeferredShader(gDeferredBlurLightProgram);
         }
 
         stop_glerror();
@@ -8595,7 +8602,7 @@ void LLPipeline::renderDeferredLighting(LLRenderTarget *screen_target)
             gPipeline.popRenderTypeMask();
         }
 
-        bool render_local = RenderLocalLights;
+        bool render_local = RenderLocalLights; // && !gCubeSnapshot;
 
         if (render_local)
         {
@@ -8604,9 +8611,12 @@ void LLPipeline::renderDeferredLighting(LLRenderTarget *screen_target)
             LLDrawable::drawable_list_t spot_lights;
             LLDrawable::drawable_list_t fullscreen_spot_lights;
 
-            for (U32 i = 0; i < 2; i++)
+            if (!gCubeSnapshot)
             {
-                mTargetShadowSpotLight[i] = NULL;
+                for (U32 i = 0; i < 2; i++)
+                {
+                    mTargetShadowSpotLight[i] = NULL;
+                }
             }
 
             std::list<LLVector4> light_colors;
@@ -8942,6 +8952,7 @@ void LLPipeline::renderDeferredLighting(LLRenderTarget *screen_target)
         popRenderTypeMask();
     }
 
+    if (!gCubeSnapshot)
     {
         // render highlights, etc.
         renderHighlights();
@@ -9053,6 +9064,7 @@ void LLPipeline::setupSpotLight(LLGLSLShader& shader, LLDrawable* drawablep)
 		shader.uniform1f(LLShaderMgr::PROJECTOR_SHADOW_FADE, 1.f);
 	}
 
+    if (!gCubeSnapshot)
 	{
 		LLDrawable* potential = drawablep;
 		//determine if this is a good light for casting shadows
@@ -9146,14 +9158,10 @@ void LLPipeline::unbindDeferredShader(LLGLSLShader &shader)
 		}
 	}
 
-    channel = shader.disableTexture(LLShaderMgr::REFLECTION_MAP, LLTexUnit::TT_CUBE_MAP);
-    if (channel > -1)
+    channel = shader.disableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP);
+    if (channel > -1 && mReflectionMapManager.mTexture.notNull())
     {
-        for (int i = 0; i < mReflectionMaps.size(); ++i)
-        {
-            gGL.getTexUnit(channel + i)->disable();
-        }
-
+        mReflectionMapManager.mTexture->unbind();
         if (channel == 0)
         {
             gGL.getTexUnit(channel)->enable(LLTexUnit::TT_TEXTURE);
@@ -9176,7 +9184,7 @@ void LLPipeline::generateWaterReflection(LLCamera& camera_in)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_PIPELINE;
 
-    if (!assertInitialized())
+    if (!assertInitialized() || gCubeSnapshot)
     {
         return;
     }
@@ -9767,7 +9775,10 @@ void LLPipeline::renderShadow(glh::matrix4f& view, glh::matrix4f& proj, LLCamera
 
     LLRenderTarget& occlusion_source = mShadow[LLViewerCamera::sCurCameraID - 1];
 
-    doOcclusion(shadow_cam, occlusion_source, occlusion_target);
+    if (occlude > 1)
+    {
+        doOcclusion(shadow_cam, occlusion_source, occlusion_target);
+    }
 
     if (use_shader)
     {
@@ -10163,6 +10174,12 @@ void LLPipeline::generateSunShadow(LLCamera& camera)
 	clip = RenderShadowOrthoClipPlanes;
 	mSunOrthoClipPlanes = LLVector4(clip, clip.mV[2]*clip.mV[2]/clip.mV[1]);
 
+    //if (gCubeSnapshot)
+    { //always do a single 64m shadow in reflection maps
+        mSunClipPlanes.set(64.f, 128.f, 256.f);
+        mSunOrthoClipPlanes.set(64.f, 128.f, 256.f);
+    }
+
 	//currently used for amount to extrude frusta corners for constructing shadow frusta
 	//LLVector3 n = RenderShadowNearDist;
 	//F32 nearDist[] = { n.mV[0], n.mV[1], n.mV[2], n.mV[2] };
@@ -10279,9 +10296,8 @@ void LLPipeline::generateSunShadow(LLCamera& camera)
 	// convenience array of 4 near clip plane distances
 	F32 dist[] = { near_clip, mSunClipPlanes.mV[0], mSunClipPlanes.mV[1], mSunClipPlanes.mV[2], mSunClipPlanes.mV[3] };
 	
-
 	if (mSunDiffuse == LLColor4::black)
-	{ //sun diffuse is totally black, shadows don't matter
+	{ //sun diffuse is totally shadows don't matter
 		LLGLDepthTest depth(GL_TRUE);
 
 		for (S32 j = 0; j < 4; j++)
@@ -10293,7 +10309,21 @@ void LLPipeline::generateSunShadow(LLCamera& camera)
 	}
 	else
 	{
-		for (S32 j = 0; j < 4; j++)
+        /*if (gCubeSnapshot)
+        {
+            // do one shadow split for cube snapshots, clear the rest
+            mSunClipPlanes.set(64.f, 64.f, 64.f);
+            dist[1] = dist[2] = dist[3] = dist[4] = 64.f;
+            for (S32 j = 1; j < 4; j++)
+            {
+                mShadow[j].bindTarget();
+                mShadow[j].clear();
+                mShadow[j].flush();
+            }
+        }*/
+
+		//for (S32 j = 0; j < (gCubeSnapshot ? 1 : 4); j++)
+        for (S32 j = 0; j < 4; j++)
 		{
 			if (!hasRenderDebugMask(RENDER_DEBUG_SHADOW_FRUSTA))
 			{
@@ -10672,142 +10702,145 @@ void LLPipeline::generateSunShadow(LLCamera& camera)
 
 	if (gen_shadow)
 	{
-		LLTrace::CountStatHandle<>* velocity_stat = LLViewerCamera::getVelocityStat();
-		F32 fade_amt = gFrameIntervalSeconds.value() 
-			* llmax(LLTrace::get_frame_recording().getLastRecording().getSum(*velocity_stat) / LLTrace::get_frame_recording().getLastRecording().getDuration().value(), 1.0);
-
-		//update shadow targets
-		for (U32 i = 0; i < 2; i++)
-		{ //for each current shadow
-			LLViewerCamera::sCurCameraID = (LLViewerCamera::eCameraID)(LLViewerCamera::CAMERA_SHADOW4+i);
-
-			if (mShadowSpotLight[i].notNull() && 
-				(mShadowSpotLight[i] == mTargetShadowSpotLight[0] ||
-				mShadowSpotLight[i] == mTargetShadowSpotLight[1]))
-			{ //keep this spotlight
-				mSpotLightFade[i] = llmin(mSpotLightFade[i]+fade_amt, 1.f);
-			}
-			else
-			{ //fade out this light
-				mSpotLightFade[i] = llmax(mSpotLightFade[i]-fade_amt, 0.f);
+        if (!gCubeSnapshot) //skip updating spot shadow maps during cubemap updates
+        {
+		    LLTrace::CountStatHandle<>* velocity_stat = LLViewerCamera::getVelocityStat();
+		    F32 fade_amt = gFrameIntervalSeconds.value() 
+			    * llmax(LLTrace::get_frame_recording().getLastRecording().getSum(*velocity_stat) / LLTrace::get_frame_recording().getLastRecording().getDuration().value(), 1.0);
+
+		    //update shadow targets
+		    for (U32 i = 0; i < 2; i++)
+		    { //for each current shadow
+			    LLViewerCamera::sCurCameraID = (LLViewerCamera::eCameraID)(LLViewerCamera::CAMERA_SHADOW4+i);
+
+			    if (mShadowSpotLight[i].notNull() && 
+				    (mShadowSpotLight[i] == mTargetShadowSpotLight[0] ||
+				    mShadowSpotLight[i] == mTargetShadowSpotLight[1]))
+			    { //keep this spotlight
+				    mSpotLightFade[i] = llmin(mSpotLightFade[i]+fade_amt, 1.f);
+			    }
+			    else
+			    { //fade out this light
+				    mSpotLightFade[i] = llmax(mSpotLightFade[i]-fade_amt, 0.f);
 				
-				if (mSpotLightFade[i] == 0.f || mShadowSpotLight[i].isNull())
-				{ //faded out, grab one of the pending spots (whichever one isn't already taken)
-					if (mTargetShadowSpotLight[0] != mShadowSpotLight[(i+1)%2])
-					{
-						mShadowSpotLight[i] = mTargetShadowSpotLight[0];
-					}
-					else
-					{
-						mShadowSpotLight[i] = mTargetShadowSpotLight[1];
-					}
-				}
-			}
-		}
+				    if (mSpotLightFade[i] == 0.f || mShadowSpotLight[i].isNull())
+				    { //faded out, grab one of the pending spots (whichever one isn't already taken)
+					    if (mTargetShadowSpotLight[0] != mShadowSpotLight[(i+1)%2])
+					    {
+						    mShadowSpotLight[i] = mTargetShadowSpotLight[0];
+					    }
+					    else
+					    {
+						    mShadowSpotLight[i] = mTargetShadowSpotLight[1];
+					    }
+				    }
+			    }
+		    }
+
+            for (S32 i = 0; i < 2; i++)
+            {
+                set_current_modelview(saved_view);
+                set_current_projection(saved_proj);
 
-		for (S32 i = 0; i < 2; i++)
-		{
-			set_current_modelview(saved_view);
-			set_current_projection(saved_proj);
+                if (mShadowSpotLight[i].isNull())
+                {
+                    continue;
+                }
 
-			if (mShadowSpotLight[i].isNull())
-			{
-				continue;
-			}
+                LLVOVolume* volume = mShadowSpotLight[i]->getVOVolume();
 
-			LLVOVolume* volume = mShadowSpotLight[i]->getVOVolume();
+                if (!volume)
+                {
+                    mShadowSpotLight[i] = NULL;
+                    continue;
+                }
 
-			if (!volume)
-			{
-				mShadowSpotLight[i] = NULL;
-				continue;
-			}
+                LLDrawable* drawable = mShadowSpotLight[i];
 
-			LLDrawable* drawable = mShadowSpotLight[i];
+                LLVector3 params = volume->getSpotLightParams();
+                F32 fov = params.mV[0];
 
-			LLVector3 params = volume->getSpotLightParams();
-			F32 fov = params.mV[0];
+                //get agent->light space matrix (modelview)
+                LLVector3 center = drawable->getPositionAgent();
+                LLQuaternion quat = volume->getRenderRotation();
 
-			//get agent->light space matrix (modelview)
-			LLVector3 center = drawable->getPositionAgent();
-			LLQuaternion quat = volume->getRenderRotation();
+                //get near clip plane
+                LLVector3 scale = volume->getScale();
+                LLVector3 at_axis(0, 0, -scale.mV[2] * 0.5f);
+                at_axis *= quat;
 
-			//get near clip plane
-			LLVector3 scale = volume->getScale();
-			LLVector3 at_axis(0,0,-scale.mV[2]*0.5f);
-			at_axis *= quat;
+                LLVector3 np = center + at_axis;
+                at_axis.normVec();
 
-			LLVector3 np = center+at_axis;
-			at_axis.normVec();
+                //get origin that has given fov for plane np, at_axis, and given scale
+                F32 dist = (scale.mV[1] * 0.5f) / tanf(fov * 0.5f);
 
-			//get origin that has given fov for plane np, at_axis, and given scale
-			F32 dist = (scale.mV[1]*0.5f)/tanf(fov*0.5f);
+                LLVector3 origin = np - at_axis * dist;
 
-			LLVector3 origin = np - at_axis*dist;
+                LLMatrix4 mat(quat, LLVector4(origin, 1.f));
 
-			LLMatrix4 mat(quat, LLVector4(origin, 1.f));
+                view[i + 4] = glh::matrix4f((F32*)mat.mMatrix);
 
-			view[i+4] = glh::matrix4f((F32*) mat.mMatrix);
+                view[i + 4] = view[i + 4].inverse();
 
-			view[i+4] = view[i+4].inverse();
+                //get perspective matrix
+                F32 near_clip = dist + 0.01f;
+                F32 width = scale.mV[VX];
+                F32 height = scale.mV[VY];
+                F32 far_clip = dist + volume->getLightRadius() * 1.5f;
 
-			//get perspective matrix
-			F32 near_clip = dist+0.01f;
-			F32 width = scale.mV[VX];
-			F32 height = scale.mV[VY];
-			F32 far_clip = dist+volume->getLightRadius()*1.5f;
+                F32 fovy = fov * RAD_TO_DEG;
+                F32 aspect = width / height;
 
-			F32 fovy = fov * RAD_TO_DEG;
-			F32 aspect = width/height;
-			
-			proj[i+4] = gl_perspective(fovy, aspect, near_clip, far_clip);
+                proj[i + 4] = gl_perspective(fovy, aspect, near_clip, far_clip);
 
-			//translate and scale to from [-1, 1] to [0, 1]
-			glh::matrix4f trans(0.5f, 0.f, 0.f, 0.5f,
-							0.f, 0.5f, 0.f, 0.5f,
-							0.f, 0.f, 0.5f, 0.5f,
-							0.f, 0.f, 0.f, 1.f);
+                //translate and scale to from [-1, 1] to [0, 1]
+                glh::matrix4f trans(0.5f, 0.f, 0.f, 0.5f,
+                    0.f, 0.5f, 0.f, 0.5f,
+                    0.f, 0.f, 0.5f, 0.5f,
+                    0.f, 0.f, 0.f, 1.f);
 
-			set_current_modelview(view[i+4]);
-			set_current_projection(proj[i+4]);
+                set_current_modelview(view[i + 4]);
+                set_current_projection(proj[i + 4]);
 
-			mSunShadowMatrix[i+4] = trans*proj[i+4]*view[i+4]*inv_view;
-			
-			for (U32 j = 0; j < 16; j++)
-			{
-				gGLLastModelView[j] = mShadowModelview[i+4].m[j];
-				gGLLastProjection[j] = mShadowProjection[i+4].m[j];
-			}
+                mSunShadowMatrix[i + 4] = trans * proj[i + 4] * view[i + 4] * inv_view;
 
-			mShadowModelview[i+4] = view[i+4];
-			mShadowProjection[i+4] = proj[i+4];
+                for (U32 j = 0; j < 16; j++)
+                {
+                    gGLLastModelView[j] = mShadowModelview[i + 4].m[j];
+                    gGLLastProjection[j] = mShadowProjection[i + 4].m[j];
+                }
 
-			LLCamera shadow_cam = camera;
-			shadow_cam.setFar(far_clip);
-			shadow_cam.setOrigin(origin);
+                mShadowModelview[i + 4] = view[i + 4];
+                mShadowProjection[i + 4] = proj[i + 4];
 
-			LLViewerCamera::updateFrustumPlanes(shadow_cam, FALSE, FALSE, TRUE);
+                LLCamera shadow_cam = camera;
+                shadow_cam.setFar(far_clip);
+                shadow_cam.setOrigin(origin);
 
-			stop_glerror();
+                LLViewerCamera::updateFrustumPlanes(shadow_cam, FALSE, FALSE, TRUE);
 
-			mShadow[i+4].bindTarget();
-			mShadow[i+4].getViewport(gGLViewport);
-			mShadow[i+4].clear();
+                stop_glerror();
+
+                mShadow[i + 4].bindTarget();
+                mShadow[i + 4].getViewport(gGLViewport);
+                mShadow[i + 4].clear();
 
-			U32 target_width = mShadow[i+4].getWidth();
+                U32 target_width = mShadow[i + 4].getWidth();
 
-			static LLCullResult result[2];
+                static LLCullResult result[2];
 
-			LLViewerCamera::sCurCameraID = (LLViewerCamera::eCameraID)(LLViewerCamera::CAMERA_SHADOW0 + i + 4);
+                LLViewerCamera::sCurCameraID = (LLViewerCamera::eCameraID)(LLViewerCamera::CAMERA_SHADOW0 + i + 4);
 
-            RenderSpotLight = drawable;            
+                RenderSpotLight = drawable;
 
-			renderShadow(view[i+4], proj[i+4], shadow_cam, result[i], FALSE, FALSE, target_width);
+                renderShadow(view[i + 4], proj[i + 4], shadow_cam, result[i], FALSE, FALSE, target_width);
 
-            RenderSpotLight = nullptr;
+                RenderSpotLight = nullptr;
 
-			mShadow[i+4].flush();
- 		}
+                mShadow[i + 4].flush();
+            }
+        }
 	}
 	else
 	{ //no spotlight shadows
@@ -11526,7 +11559,7 @@ void LLPipeline::restoreHiddenObject( const LLUUID& id )
 
 void LLPipeline::overrideEnvironmentMap()
 {
-    mReflectionMapManager.mProbes.clear();
-    mReflectionMapManager.addProbe(LLViewerCamera::instance().getOrigin());
+    //mReflectionMapManager.mProbes.clear();
+    //mReflectionMapManager.addProbe(LLViewerCamera::instance().getOrigin());
 }
 
diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h
index 975380929d7..88eaca558a4 100644
--- a/indra/newview/pipeline.h
+++ b/indra/newview/pipeline.h
@@ -578,7 +578,8 @@ class LLPipeline
 		RENDER_DEBUG_ATTACHMENT_BYTES	=  0x20000000, // not used
 		RENDER_DEBUG_TEXEL_DENSITY		=  0x40000000,
 		RENDER_DEBUG_TRIANGLE_COUNT		=  0x80000000,
-		RENDER_DEBUG_IMPOSTORS			= 0x100000000
+		RENDER_DEBUG_IMPOSTORS			= 0x100000000,
+        RENDER_DEBUG_REFLECTION_PROBES  = 0x200000000
 	};
 
 public:
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index db4e794ed47..0b0f8e17bc6 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -2911,6 +2911,16 @@ function="World.EnvPreset"
            function="Advanced.ToggleInfoDisplay"
            parameter="lights" />
         </menu_item_check>
+        <menu_item_check
+         label="Reflection Probes"
+         name="Reflection Probes">
+          <menu_item_check.on_check
+           function="Advanced.CheckInfoDisplay"
+           parameter="reflection probes" />
+          <menu_item_check.on_click
+           function="Advanced.ToggleInfoDisplay"
+           parameter="reflection probes" />
+        </menu_item_check>
         <menu_item_check
          label="Particles"
          name="Particles">
@@ -3252,10 +3262,10 @@ function="World.EnvPreset"
             </menu_item_check>
             <menu_item_call
               enabled="true"
-              label="Override Environment Map"
-              name="Override Environment Map">
+              label="Rebuild Reflection Probes"
+              name="Rebuild Reflection Probes">
               <menu_item_call.on_click
-               function="Develop.OverrideEnvironmentMap" />
+               function="Develop.RebuildReflectionProbes" />
             </menu_item_call>
           <menu_item_separator />
           
-- 
GitLab