diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index 27ac4053dfe8f1351c40712b50bc7d632e30cf1d..421b9ee2d6e69d4bf04aa8b17fcca26f47c3b84e 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1431,6 +1431,7 @@ void LLShaderMgr::initAttribsAndUniforms()
     mReservedUniforms.push_back("moon_brightness");
     mReservedUniforms.push_back("cloud_variance");
     mReservedUniforms.push_back("reflection_probe_ambiance");
+    mReservedUniforms.push_back("max_probe_lod");
 
     mReservedUniforms.push_back("sh_input_r");
     mReservedUniforms.push_back("sh_input_g");
diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h
index 86ada6c1325af4c2cc2b280fb39129264d0bd18d..a224b2a19b0bea2b266e23fda8b92415672f0c3e 100644
--- a/indra/llrender/llshadermgr.h
+++ b/indra/llrender/llshadermgr.h
@@ -267,6 +267,7 @@ class LLShaderMgr
         CLOUD_VARIANCE,                     //  "cloud_variance"
 
         REFLECTION_PROBE_AMBIANCE,          //  "reflection_probe_ambiance"
+        REFLECTION_PROBE_MAX_LOD,            //  "max_probe_lod"
         SH_INPUT_L1R,                       //  "sh_input_r"
         SH_INPUT_L1G,                       //  "sh_input_g"
         SH_INPUT_L1B,                       //  "sh_input_b"
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index d262a1285f048f7771108af1151c8421db41621e..41afca50f65848a63f0ad2ae5dc7328b2a839776 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -10382,6 +10382,17 @@
     <key>Value</key>
     <integer>256</integer>
   </map>
+  <key>RenderReflectionProbeResolution</key>
+  <map>
+    <key>Comment</key>
+    <string>Resolution of reflection probe radiance maps (requires restart).  Will be set to the next highest power of two clamped to [64, 512].  Note that changing this value may consume a massive amount of video memory.</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>U32</string>
+    <key>Value</key>
+    <integer>256</integer>
+  </map>
 
   <key>RenderReflectionProbeDrawDistance</key>
   <map>
diff --git a/indra/newview/app_settings/shaders/class1/interface/irradianceGenF.glsl b/indra/newview/app_settings/shaders/class1/interface/irradianceGenF.glsl
index 3e056aa04833d16fa161f761eb643e952e8512d0..2b1e794b5268f8cff444744c01ed591cd9d6b1c0 100644
--- a/indra/newview/app_settings/shaders/class1/interface/irradianceGenF.glsl
+++ b/indra/newview/app_settings/shaders/class1/interface/irradianceGenF.glsl
@@ -23,205 +23,11 @@
  * $/LicenseInfo$
  */
  
+// debug stub
 
-/*[EXTRA_CODE_HERE]*/
-
-
-#ifdef DEFINE_GL_FRAGCOLOR
 out vec4 frag_color;
-#else
-#define frag_color gl_FragColor
-#endif
-
-uniform samplerCubeArray   reflectionProbes;
-uniform int sourceIdx;
-
-VARYING vec3 vary_dir;
-
-
-// Code below is derived from the Khronos GLTF Sample viewer:
-// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/shaders/ibl_filtering.frag
-
-
-#define MATH_PI 3.1415926535897932384626433832795
-
-float u_roughness = 1.0;
-int u_sampleCount = 16;
-float u_lodBias = 2.0;
-int u_width = 64;
-
-// Hammersley Points on the Hemisphere
-// CC BY 3.0 (Holger Dammertz)
-// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
-// with adapted interface
-float radicalInverse_VdC(uint bits)
-{
-    bits = (bits << 16u) | (bits >> 16u);
-    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
-    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
-    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
-    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
-    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
-}
-
-// hammersley2d describes a sequence of points in the 2d unit square [0,1)^2
-// that can be used for quasi Monte Carlo integration
-vec2 hammersley2d(int i, int N) {
-    return vec2(float(i)/float(N), radicalInverse_VdC(uint(i)));
-}
-
-// Hemisphere Sample
-
-// TBN generates a tangent bitangent normal coordinate frame from the normal
-// (the normal must be normalized)
-mat3 generateTBN(vec3 normal)
-{
-    vec3 bitangent = vec3(0.0, 1.0, 0.0);
-
-    float NdotUp = dot(normal, vec3(0.0, 1.0, 0.0));
-    float epsilon = 0.0000001;
-    /*if (1.0 - abs(NdotUp) <= epsilon)
-    {
-        // Sampling +Y or -Y, so we need a more robust bitangent.
-        if (NdotUp > 0.0)
-        {
-            bitangent = vec3(0.0, 0.0, 1.0);
-        }
-        else
-        {
-            bitangent = vec3(0.0, 0.0, -1.0);
-        }
-    }*/
-
-    vec3 tangent = normalize(cross(bitangent, normal));
-    bitangent = cross(normal, tangent);
-
-    return mat3(tangent, bitangent, normal);
-}
-
-struct MicrofacetDistributionSample
-{
-    float pdf;
-    float cosTheta;
-    float sinTheta;
-    float phi;
-};
-
-MicrofacetDistributionSample Lambertian(vec2 xi, float roughness)
-{
-    MicrofacetDistributionSample lambertian;
-
-    // Cosine weighted hemisphere sampling
-    // http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#Cosine-WeightedHemisphereSampling
-    lambertian.cosTheta = sqrt(1.0 - xi.y);
-    lambertian.sinTheta = sqrt(xi.y); // equivalent to `sqrt(1.0 - cosTheta*cosTheta)`;
-    lambertian.phi = 2.0 * MATH_PI * xi.x;
-
-    lambertian.pdf = lambertian.cosTheta / MATH_PI; // evaluation for solid angle, therefore drop the sinTheta
-
-    return lambertian;
-}
 
-
-// getImportanceSample returns an importance sample direction with pdf in the .w component
-vec4 getImportanceSample(int sampleIndex, vec3 N, float roughness)
-{
-    // generate a quasi monte carlo point in the unit square [0.1)^2
-    vec2 xi = hammersley2d(sampleIndex, u_sampleCount);
-
-    MicrofacetDistributionSample importanceSample;
-
-    // generate the points on the hemisphere with a fitting mapping for
-    // the distribution (e.g. lambertian uses a cosine importance)
-    importanceSample = Lambertian(xi, roughness);
-    
-    // transform the hemisphere sample to the normal coordinate frame
-    // i.e. rotate the hemisphere to the normal direction
-    vec3 localSpaceDirection = normalize(vec3(
-        importanceSample.sinTheta * cos(importanceSample.phi), 
-        importanceSample.sinTheta * sin(importanceSample.phi), 
-        importanceSample.cosTheta
-    ));
-    mat3 TBN = generateTBN(N);
-    vec3 direction = TBN * localSpaceDirection;
-
-    return vec4(direction, importanceSample.pdf);
-}
-
-// Mipmap Filtered Samples (GPU Gems 3, 20.4)
-// https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-20-gpu-based-importance-sampling
-// https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf
-float computeLod(float pdf)
-{
-    // // Solid angle of current sample -- bigger for less likely samples
-    // float omegaS = 1.0 / (float(u_sampleCount) * pdf);
-    // // Solid angle of texel
-    // // note: the factor of 4.0 * MATH_PI 
-    // float omegaP = 4.0 * MATH_PI / (6.0 * float(u_width) * float(u_width));
-    // // Mip level is determined by the ratio of our sample's solid angle to a texel's solid angle 
-    // // note that 0.5 * log2 is equivalent to log4
-    // float lod = 0.5 * log2(omegaS / omegaP);
-
-    // babylon introduces a factor of K (=4) to the solid angle ratio
-    // this helps to avoid undersampling the environment map
-    // this does not appear in the original formulation by Jaroslav Krivanek and Mark Colbert
-    // log4(4) == 1
-    // lod += 1.0;
-
-    // We achieved good results by using the original formulation from Krivanek & Colbert adapted to cubemaps
-
-    // https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf
-    float lod = 0.5 * log2( 6.0 * float(u_width) * float(u_width) / (float(u_sampleCount) * pdf));
-
-
-    return lod;
-}
-
-vec4 filterColor(vec3 N)
-{
-    //return  textureLod(uCubeMap, N, 3.0).rgb;
-    vec4 color = vec4(0.f);
-    float weight = 0.0f;
-
-    for(int i = 0; i < u_sampleCount; ++i)
-    {
-        vec4 importanceSample = getImportanceSample(i, N, 1.0);
-
-        vec3 H = vec3(importanceSample.xyz);
-        float pdf = importanceSample.w;
-
-        // mipmap filtered samples (GPU Gems 3, 20.4)
-        float lod = computeLod(pdf);
-
-        // apply the bias to the lod
-        lod += u_lodBias;
-
-        lod = clamp(lod, 0, 6);
-        // sample lambertian at a lower resolution to avoid fireflies
-        vec4 lambertian = textureLod(reflectionProbes, vec4(H, sourceIdx), lod);
-
-        color += lambertian;
-    }
-
-    if(weight != 0.0f)
-    {
-        color /= weight;
-    }
-    else
-    {
-        color /= float(u_sampleCount);
-    }
-
-    return min(color*1.9, vec4(1));
-}
-
-// entry point
 void main()
 {
-    vec4 color = vec4(0);
-
-    color = filterColor(vary_dir);
-    
-    frag_color = color;
+    frag_color = vec4(0.5, 0, 0.5, 0);
 }
-
diff --git a/indra/newview/app_settings/shaders/class1/interface/radianceGenF.glsl b/indra/newview/app_settings/shaders/class1/interface/radianceGenF.glsl
index 858052281b210b34897de35514e9bffca0bb3cc1..e60ddcd569b1d0ece610318b793342f090583b83 100644
--- a/indra/newview/app_settings/shaders/class1/interface/radianceGenF.glsl
+++ b/indra/newview/app_settings/shaders/class1/interface/radianceGenF.glsl
@@ -37,6 +37,8 @@ VARYING vec3 vary_dir;
 
 uniform float mipLevel;
 uniform int u_width; 
+uniform float max_probe_lod;
+
 
 // =============================================================================================================
 // Parts of this file are (c) 2018 Sascha Willems
@@ -128,7 +130,7 @@ vec4 prefilterEnvMap(vec3 R)
 	float envMapDim = u_width;
     int numSamples = 4;
     
-    float numMips = 6.0;
+    float numMips = max_probe_lod;
 
     float roughness = mipLevel/numMips;
 
diff --git a/indra/newview/app_settings/shaders/class2/interface/irradianceGenF.glsl b/indra/newview/app_settings/shaders/class2/interface/irradianceGenF.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..a4aec48c5945f607bc3ecd4c28fc614075331c83
--- /dev/null
+++ b/indra/newview/app_settings/shaders/class2/interface/irradianceGenF.glsl
@@ -0,0 +1,231 @@
+/** 
+ * @file irradianceGenF.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$
+ */
+ 
+
+/*[EXTRA_CODE_HERE]*/
+
+
+#ifdef DEFINE_GL_FRAGCOLOR
+out vec4 frag_color;
+#else
+#define frag_color gl_FragColor
+#endif
+
+uniform samplerCubeArray   reflectionProbes;
+uniform int sourceIdx;
+
+uniform float max_probe_lod;
+
+VARYING vec3 vary_dir;
+
+
+// Code below is derived from the Khronos GLTF Sample viewer:
+// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/shaders/ibl_filtering.frag
+
+
+#define MATH_PI 3.1415926535897932384626433832795
+
+float u_roughness = 1.0;
+int u_sampleCount = 64;
+float u_lodBias = 2.0;
+int u_width = 64;
+
+// Hammersley Points on the Hemisphere
+// CC BY 3.0 (Holger Dammertz)
+// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
+// with adapted interface
+float radicalInverse_VdC(uint bits)
+{
+    bits = (bits << 16u) | (bits >> 16u);
+    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
+    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
+    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
+    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
+    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
+}
+
+// hammersley2d describes a sequence of points in the 2d unit square [0,1)^2
+// that can be used for quasi Monte Carlo integration
+vec2 hammersley2d(int i, int N) {
+    return vec2(float(i)/float(N), radicalInverse_VdC(uint(i)));
+}
+
+// Hemisphere Sample
+
+// TBN generates a tangent bitangent normal coordinate frame from the normal
+// (the normal must be normalized)
+mat3 generateTBN(vec3 normal)
+{
+    vec3 bitangent = vec3(0.0, 1.0, 0.0);
+
+    float NdotUp = dot(normal, vec3(0.0, 1.0, 0.0));
+    float epsilon = 0.0000001;
+    /*if (1.0 - abs(NdotUp) <= epsilon)
+    {
+        // Sampling +Y or -Y, so we need a more robust bitangent.
+        if (NdotUp > 0.0)
+        {
+            bitangent = vec3(0.0, 0.0, 1.0);
+        }
+        else
+        {
+            bitangent = vec3(0.0, 0.0, -1.0);
+        }
+    }*/
+
+    vec3 tangent = normalize(cross(bitangent, normal));
+    bitangent = cross(normal, tangent);
+
+    return mat3(tangent, bitangent, normal);
+}
+
+struct MicrofacetDistributionSample
+{
+    float pdf;
+    float cosTheta;
+    float sinTheta;
+    float phi;
+};
+
+MicrofacetDistributionSample Lambertian(vec2 xi, float roughness)
+{
+    MicrofacetDistributionSample lambertian;
+
+    // Cosine weighted hemisphere sampling
+    // http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#Cosine-WeightedHemisphereSampling
+    lambertian.cosTheta = sqrt(1.0 - xi.y);
+    lambertian.sinTheta = sqrt(xi.y); // equivalent to `sqrt(1.0 - cosTheta*cosTheta)`;
+    lambertian.phi = 2.0 * MATH_PI * xi.x;
+
+    lambertian.pdf = lambertian.cosTheta / MATH_PI; // evaluation for solid angle, therefore drop the sinTheta
+
+    return lambertian;
+}
+
+
+// getImportanceSample returns an importance sample direction with pdf in the .w component
+vec4 getImportanceSample(int sampleIndex, vec3 N, float roughness)
+{
+    // generate a quasi monte carlo point in the unit square [0.1)^2
+    vec2 xi = hammersley2d(sampleIndex, u_sampleCount);
+
+    MicrofacetDistributionSample importanceSample;
+
+    // generate the points on the hemisphere with a fitting mapping for
+    // the distribution (e.g. lambertian uses a cosine importance)
+    importanceSample = Lambertian(xi, roughness);
+    
+    // transform the hemisphere sample to the normal coordinate frame
+    // i.e. rotate the hemisphere to the normal direction
+    vec3 localSpaceDirection = normalize(vec3(
+        importanceSample.sinTheta * cos(importanceSample.phi), 
+        importanceSample.sinTheta * sin(importanceSample.phi), 
+        importanceSample.cosTheta
+    ));
+    mat3 TBN = generateTBN(N);
+    vec3 direction = TBN * localSpaceDirection;
+
+    return vec4(direction, importanceSample.pdf);
+}
+
+// Mipmap Filtered Samples (GPU Gems 3, 20.4)
+// https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-20-gpu-based-importance-sampling
+// https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf
+float computeLod(float pdf)
+{
+    // // Solid angle of current sample -- bigger for less likely samples
+    // float omegaS = 1.0 / (float(u_sampleCount) * pdf);
+    // // Solid angle of texel
+    // // note: the factor of 4.0 * MATH_PI 
+    // float omegaP = 4.0 * MATH_PI / (6.0 * float(u_width) * float(u_width));
+    // // Mip level is determined by the ratio of our sample's solid angle to a texel's solid angle 
+    // // note that 0.5 * log2 is equivalent to log4
+    // float lod = 0.5 * log2(omegaS / omegaP);
+
+    // babylon introduces a factor of K (=4) to the solid angle ratio
+    // this helps to avoid undersampling the environment map
+    // this does not appear in the original formulation by Jaroslav Krivanek and Mark Colbert
+    // log4(4) == 1
+    // lod += 1.0;
+
+    // We achieved good results by using the original formulation from Krivanek & Colbert adapted to cubemaps
+
+    // https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf
+    float lod = 0.5 * log2( 6.0 * float(u_width) * float(u_width) / (float(u_sampleCount) * pdf));
+
+
+    return lod;
+}
+
+vec4 filterColor(vec3 N)
+{
+    //return  textureLod(uCubeMap, N, 3.0).rgb;
+    vec4 color = vec4(0.f);
+    float weight = 0.0f;
+
+    for(int i = 0; i < u_sampleCount; ++i)
+    {
+        vec4 importanceSample = getImportanceSample(i, N, 1.0);
+
+        vec3 H = vec3(importanceSample.xyz);
+        float pdf = importanceSample.w;
+
+        // mipmap filtered samples (GPU Gems 3, 20.4)
+        float lod = computeLod(pdf);
+
+        // apply the bias to the lod
+        lod += u_lodBias;
+
+        lod = clamp(lod, 0, max_probe_lod);
+        // sample lambertian at a lower resolution to avoid fireflies
+        vec4 lambertian = textureLod(reflectionProbes, vec4(H, sourceIdx), lod);
+
+        color += lambertian;
+    }
+
+    if(weight != 0.0f)
+    {
+        color /= weight;
+    }
+    else
+    {
+        color /= float(u_sampleCount);
+    }
+
+    color = min(color*1.9, vec4(1)); 
+    color = pow(color, vec4(0.5));
+    return color;
+}
+
+// entry point
+void main()
+{
+    vec4 color = vec4(0);
+
+    color = filterColor(vary_dir);
+    
+    frag_color = color;
+}
+
diff --git a/indra/newview/app_settings/shaders/class2/windlight/atmosphericsFuncs.glsl b/indra/newview/app_settings/shaders/class2/windlight/atmosphericsFuncs.glsl
index c69eba93b6f591fe499720c23396c7fbc52790d7..ba02070e4537eeaaa53925e67343e7590283738e 100644
--- a/indra/newview/app_settings/shaders/class2/windlight/atmosphericsFuncs.glsl
+++ b/indra/newview/app_settings/shaders/class2/windlight/atmosphericsFuncs.glsl
@@ -162,90 +162,10 @@ float ambientLighting(vec3 norm, vec3 light_dir)
 void calcAtmosphericVarsLinear(vec3 inPositionEye, vec3 norm, vec3 light_dir, out vec3 sunlit, out vec3 amblit, out vec3 additive,
                          out vec3 atten)
 {
-#if 1
     calcAtmosphericVars(inPositionEye, light_dir, 1.0, sunlit, amblit, additive, atten, false);
     sunlit = srgb_to_linear(sunlit);
     additive = srgb_to_linear(additive);
     amblit = ambient_linear;
 
     amblit *= ambientLighting(norm, light_dir);
-#else 
-
-    //EXPERIMENTAL -- attempt to factor out srgb_to_linear conversions above
-    vec3 rel_pos = inPositionEye;
-
-    //(TERRAIN) limit altitude
-    if (abs(rel_pos.y) > max_y) rel_pos *= (max_y / rel_pos.y);
-
-    vec3  rel_pos_norm = normalize(rel_pos);
-    float rel_pos_len  = length(rel_pos);
-    vec3  sunlight     = (sun_up_factor == 1) ? vec3(sunlight_linear, 0.0) : vec3(moonlight_linear, 0.0);
-
-    // sunlight attenuation effect (hue and brightness) due to atmosphere
-    // this is used later for sunlight modulation at various altitudes
-    vec3 light_atten = (blue_density + vec3(haze_density * 0.25)) * (density_multiplier * max_y);
-    // I had thought blue_density and haze_density should have equal weighting,
-    // but attenuation due to haze_density tends to seem too strong
-
-    vec3 combined_haze = blue_density + vec3(haze_density);
-    vec3 blue_weight   = blue_density / combined_haze;
-    vec3 haze_weight   = vec3(haze_density) / combined_haze;
-
-    //(TERRAIN) compute sunlight from lightnorm y component. Factor is roughly cosecant(sun elevation) (for short rays like terrain)
-    float above_horizon_factor = 1.0 / max(1e-6, lightnorm.y);
-    sunlight *= exp(-light_atten * above_horizon_factor);  // for sun [horizon..overhead] this maps to an exp curve [0..1]
-
-    // main atmospheric scattering line integral
-    float density_dist = rel_pos_len * density_multiplier;
-
-    // Transparency (-> combined_haze)
-    // ATI Bugfix -- can't store combined_haze*density_dist*distance_multiplier in a variable because the ati
-    // compiler gets confused.
-    combined_haze = exp(-combined_haze * density_dist * distance_multiplier);
-
-    // final atmosphere attenuation factor
-    atten = combined_haze.rgb;
-
-    // compute haze glow
-    float haze_glow = dot(rel_pos_norm, lightnorm.xyz);
-
-    // dampen sun additive contrib when not facing it...
-    // SL-13539: This "if" clause causes an "additive" white artifact at roughly 77 degreees.
-    //    if (length(light_dir) > 0.01)
-    haze_glow *= max(0.0f, dot(light_dir, rel_pos_norm));
-
-    haze_glow = 1. - haze_glow;
-    // haze_glow is 0 at the sun and increases away from sun
-    haze_glow = max(haze_glow, .001);  // set a minimum "angle" (smaller glow.y allows tighter, brighter hotspot)
-    haze_glow *= glow.x;
-    // higher glow.x gives dimmer glow (because next step is 1 / "angle")
-    haze_glow = pow(haze_glow, glow.z);
-    // glow.z should be negative, so we're doing a sort of (1 / "angle") function
-
-    // add "minimum anti-solar illumination"
-    haze_glow += .25;
-
-    haze_glow *= sun_moon_glow_factor;
-
-    //vec3 amb_color = vec4(ambient_linear, 0.0);
-    vec3 amb_color = ambient_color;
-
-    // increase ambient when there are more clouds
-    vec3 tmpAmbient = amb_color + (vec3(1.) - amb_color) * cloud_shadow * 0.5;
-
-    // Similar/Shared Algorithms:
-    //     indra\llinventory\llsettingssky.cpp                                        -- LLSettingsSky::calculateLightSettings()
-    //     indra\newview\app_settings\shaders\class1\windlight\atmosphericsFuncs.glsl -- calcAtmosphericVars()
-    // haze color
-    vec3 cs = sunlight.rgb * (1. - cloud_shadow);
-    additive = (blue_horizon.rgb * blue_weight.rgb) * (cs + tmpAmbient.rgb) + (haze_horizon * haze_weight.rgb) * (cs * haze_glow + tmpAmbient.rgb);
-
-    // brightness of surface both sunlight and ambient
-    sunlit = min(sunlight.rgb, vec3(1));
-    amblit = tmpAmbient.rgb;
-    additive *= vec3(1.0 - combined_haze);
-
-    //sunlit = sunlight_linear;
-    amblit = ambient_linear*0.8;
-#endif
 }
diff --git a/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl b/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
index 9793ab13de1e18c12a0fb068042576b802bd32b8..bb3be7260b52dbfce7516117e824f814d59008d2 100644
--- a/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
+++ b/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
@@ -36,6 +36,7 @@ uniform samplerCubeArray   reflectionProbes;
 uniform samplerCubeArray   irradianceProbes;
 uniform sampler2D sceneMap;
 uniform int cube_snapshot;
+uniform float max_probe_lod;
 
 layout (std140) uniform ReflectionProbes
 {
@@ -623,7 +624,7 @@ vec3 sampleProbeAmbient(vec3 pos, vec3 dir)
     {
         col *= 1.0/wsum;
     }
-    
+
     return col;
 }
 
@@ -631,7 +632,7 @@ void sampleReflectionProbes(inout vec3 ambenv, inout vec3 glossenv,
         vec2 tc, vec3 pos, vec3 norm, float glossiness, bool errorCorrect)
 {
     // TODO - don't hard code lods
-    float reflection_lods = 6;
+    float reflection_lods = max_probe_lod;
     preProbeSample(pos);
 
     vec3 refnormpersp = reflect(pos.xyz, norm.xyz);
@@ -705,7 +706,7 @@ void sampleReflectionProbesLegacy(inout vec3 ambenv, inout vec3 glossenv, inout
         vec2 tc, vec3 pos, vec3 norm, float glossiness, float envIntensity)
 {
     // TODO - don't hard code lods
-    float reflection_lods = 7;
+    float reflection_lods = max_probe_lod;
     preProbeSample(pos);
 
     vec3 refnormpersp = reflect(pos.xyz, norm.xyz);
diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt
index 2821b6339189b34eb2c98c68b40596018e0ef92d..5bd74fe318ed0ec137a7e6f1169973ad5c552cd2 100644
--- a/indra/newview/featuretable.txt
+++ b/indra/newview/featuretable.txt
@@ -1,4 +1,4 @@
-version 46
+version 47
 // The version number above should be incremented IF AND ONLY IF some
 // change has been made that is sufficiently important to justify
 // resetting the graphics preferences of all users to the recommended
@@ -72,6 +72,7 @@ RenderFSAASamples			1	16
 RenderMaxTextureIndex		1	16
 RenderGLContextCoreProfile         1   1
 RenderGLMultiThreaded       1   0
+RenderReflectionProbeResolution 1 256
 
 
 //
@@ -271,6 +272,12 @@ RenderUseAdvancedAtmospherics 1 0
 list VRAMGT512
 RenderCompressTextures		1	0
 
+//
+// VRAM < 2GB
+//
+list VRAMLT2GB
+RenderReflectionProbeResolution 1 128
+
 //
 // "Default" setups for safe, low, medium, high
 //
diff --git a/indra/newview/llenvironment.cpp b/indra/newview/llenvironment.cpp
index efe4ad2af2df6dae9c2bffda949d89d5b8d9e23d..6d600efe37bc3d31e4fe763cc95a1bba49312da9 100644
--- a/indra/newview/llenvironment.cpp
+++ b/indra/newview/llenvironment.cpp
@@ -1630,28 +1630,31 @@ LLVector4 LLEnvironment::getRotatedLightNorm() const
     return toLightNorm(light_direction);
 }
 
+extern BOOL gCubeSnapshot;
+
 //-------------------------------------------------------------------------
 void LLEnvironment::update(const LLViewerCamera * cam)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT; //LL_RECORD_BLOCK_TIME(FTM_ENVIRONMENT_UPDATE);
     //F32Seconds now(LLDate::now().secondsSinceEpoch());
-    static LLFrameTimer timer;
-
-    F32Seconds delta(timer.getElapsedTimeAndResetF32());
-
+    if (!gCubeSnapshot)
     {
-        DayInstance::ptr_t keeper = mCurrentEnvironment;    
-        // make sure the current environment does not go away until applyTimeDelta is done.
-        mCurrentEnvironment->applyTimeDelta(delta);
+        static LLFrameTimer timer;
 
-    }
-    // update clouds, sun, and general
-    updateCloudScroll();
+        F32Seconds delta(timer.getElapsedTimeAndResetF32());
 
-    // cache this for use in rotating the rotated light vec for shader param updates later...
-    mLastCamYaw = cam->getYaw() + SUN_DELTA_YAW;
+        {
+            DayInstance::ptr_t keeper = mCurrentEnvironment;
+            // make sure the current environment does not go away until applyTimeDelta is done.
+            mCurrentEnvironment->applyTimeDelta(delta);
+
+        }
+        // update clouds, sun, and general
+        updateCloudScroll();
 
-    stop_glerror();
+        // cache this for use in rotating the rotated light vec for shader param updates later...
+        mLastCamYaw = cam->getYaw() + SUN_DELTA_YAW;
+    }
 
     updateSettingsUniforms();
 
@@ -1752,13 +1755,23 @@ void LLEnvironment::updateGLVariablesForSettings(LLShaderUniforms* uniforms, con
         {
             LLVector4 vect4(value);
 
-            switch (it.second.getShaderKey())
-            { // convert to linear color space if this is a color parameter
-            case LLShaderMgr::BLUE_HORIZON:
-            case LLShaderMgr::BLUE_DENSITY:
-                //vect4 = LLVector4(linearColor4(LLColor4(vect4.mV)).mV);
-                break;
+            if (gCubeSnapshot && !gPipeline.mReflectionMapManager.isRadiancePass())
+            { // maximize and remove tinting if this is an irradiance map render pass and the parameter feeds into the sky background color
+                auto max_vec = [](LLVector4 col)
+                {
+                    col.mV[0] = col.mV[1] = col.mV[2] = llmax(llmax(col.mV[0], col.mV[1]), col.mV[2]);
+                    return col;
+                };
+
+                switch (it.second.getShaderKey())
+                { 
+                case LLShaderMgr::BLUE_HORIZON:
+                case LLShaderMgr::BLUE_DENSITY:
+                    vect4 = max_vec(vect4);
+                        break;
+                }
             }
+
             //_WARNS("RIDER") << "pushing '" << (*it).first << "' as " << vect4 << LL_ENDL;
             shader->uniform3fv(it.second.getShaderKey(), LLVector3(vect4.mV) );
             break;
diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp
index 5817fdbfa0240e7f4c1ca832c9550661983a24c1..3b1bee05af9b30294c6433f6d84ccdf94f244604 100644
--- a/indra/newview/llfeaturemanager.cpp
+++ b/indra/newview/llfeaturemanager.cpp
@@ -651,6 +651,10 @@ void LLFeatureManager::applyBaseMasks()
 	{
 		maskFeatures("VRAMGT512");
 	}
+    if (gGLManager.mVRAM < 2048)
+    {
+        maskFeatures("VRAMLT2GB");
+    }
     if (gGLManager.mGLVersion < 3.99f)
     {
         maskFeatures("GL3");
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index 128aa99ccc34a395aeb4d659d7640ea6c159709b..ee058491309b738005651ea6cdfe1afc89d4d3f1 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -39,6 +39,10 @@
 extern BOOL gCubeSnapshot;
 extern BOOL gTeleportDisplay;
 
+// get the next highest power of two of v (or v if v is already a power of two)
+//defined in llvertexbuffer.cpp
+extern U32 nhpo2(U32 v);
+
 static void touch_default_probe(LLReflectionMap* probe)
 {
     LLVector3 origin = LLViewerCamera::getInstance()->getOrigin();
@@ -91,13 +95,13 @@ void LLReflectionMapManager::update()
     if (!mRenderTarget.isComplete())
     {
         U32 color_fmt = GL_RGB16F;
-        U32 targetRes = LL_REFLECTION_PROBE_RESOLUTION * 2; // super sample
+        U32 targetRes = mProbeResolution * 2; // super sample
         mRenderTarget.allocate(targetRes, targetRes, color_fmt, true);
     }
 
     if (mMipChain.empty())
     {
-        U32 res = LL_REFLECTION_PROBE_RESOLUTION;
+        U32 res = mProbeResolution;
         U32 count = log2((F32)res) + 0.5f;
         
         mMipChain.resize(count);
@@ -401,11 +405,27 @@ void LLReflectionMapManager::doProbeUpdate()
     if (++mUpdatingFace == 6)
     {
         updateNeighbors(mUpdatingProbe);
-        mUpdatingProbe = nullptr;
         mUpdatingFace = 0;
+        if (mRadiancePass)
+        {
+            mUpdatingProbe = nullptr;
+            mRadiancePass = false;
+        }
+        else
+        {
+            mRadiancePass = true;
+        }
     }
 }
 
+// Do the reflection map update render passes.
+// For every 12 calls of this function, one complete reflection probe radiance map and irradiance map is generated
+// First six passes render the scene with direct lighting only into a scratch space cube map at the end of the cube map array and generate 
+// a simple mip chain (not convolution filter).
+// At the end of these passes, an irradiance map is generated for this probe and placed into the irradiance cube map array at the index for this probe
+// The next six passes render the scene with both radiance and irradiance into the same scratch space cube map and generate a simple mip chain.
+// At the end of these passes, a radiance map is generated for this probe and placed into the radiance cube map array at the index for this probe.
+// In effect this simulates single-bounce lighting.
 void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
 {
     // hacky hot-swap of camera specific render targets
@@ -432,11 +452,11 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
     
     gPipeline.mRT = &gPipeline.mMainRT;
 
-    S32 targetIdx = mReflectionProbeCount;
+    S32 sourceIdx = mReflectionProbeCount;
 
     if (probe != mUpdatingProbe)
     { // this is the "realtime" probe that's updating every frame, use the secondary scratch space channel
-        targetIdx += 1;
+        sourceIdx += 1;
     }
 
     gGL.setColorMask(true, true);
@@ -457,9 +477,9 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         gGL.loadIdentity();
 
         gGL.flush();
-        U32 res = LL_REFLECTION_PROBE_RESOLUTION * 2;
+        U32 res = mProbeResolution * 2;
 
-        S32 mips = log2((F32)LL_REFLECTION_PROBE_RESOLUTION) + 0.5f;
+        S32 mips = log2((F32)mProbeResolution) + 0.5f;
 
         S32 diffuseChannel = gReflectionMipProgram.enableTexture(LLShaderMgr::DEFERRED_DIFFUSE, LLTexUnit::TT_TEXTURE);
         S32 depthChannel   = gReflectionMipProgram.enableTexture(LLShaderMgr::DEFERRED_DEPTH, LLTexUnit::TT_TEXTURE);
@@ -516,7 +536,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
                 LL_PROFILE_GPU_ZONE("probe mip copy");
                 mTexture->bind(0);
                 //glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, probe->mCubeIndex * 6 + face, 0, 0, res, res);
-                glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, targetIdx * 6 + face, 0, 0, res, res);
+                glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, sourceIdx * 6 + face, 0, 0, res, res);
                 //if (i == 0)
                 //{
                     //glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, probe->mCubeIndex * 6 + face, 0, 0, res, res);
@@ -537,89 +557,98 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
 
     if (face == 5)
     {
-        //generate radiance map
-        gRadianceGenProgram.bind();
-        mVertexBuffer->setBuffer();
-
-        S32 channel = gRadianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
-        mTexture->bind(channel);
-        static LLStaticHashedString sSourceIdx("sourceIdx");
-        gRadianceGenProgram.uniform1i(sSourceIdx, targetIdx);
-
         mMipChain[0].bindTarget();
-        U32 res = mMipChain[0].getWidth();
+        static LLStaticHashedString sSourceIdx("sourceIdx");
 
-        for (int i = 0; i < mMipChain.size(); ++i)
+        if (mRadiancePass)
         {
-            LL_PROFILE_GPU_ZONE("probe radiance gen");
-            static LLStaticHashedString sMipLevel("mipLevel");
-            static LLStaticHashedString sRoughness("roughness");
-            static LLStaticHashedString sWidth("u_width");
+            //generate radiance map (even if this is not the irradiance map, we need the mip chain for the irradiance map)
+            gRadianceGenProgram.bind();
+            mVertexBuffer->setBuffer();
 
-            gRadianceGenProgram.uniform1f(sRoughness, (F32)i / (F32)(mMipChain.size() - 1));
-            gRadianceGenProgram.uniform1f(sMipLevel, i);
-            gRadianceGenProgram.uniform1i(sWidth, mMipChain[i].getWidth());
+            S32 channel = gRadianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
+            mTexture->bind(channel);
+            gRadianceGenProgram.uniform1i(sSourceIdx, sourceIdx);
+            gRadianceGenProgram.uniform1f(LLShaderMgr::REFLECTION_PROBE_MAX_LOD, mMaxProbeLOD);
 
-            for (int cf = 0; cf < 6; ++cf)
-            { // for each cube face
-                LLCoordFrame frame;
-                frame.lookAt(LLVector3(0, 0, 0), LLCubeMapArray::sClipToCubeLookVecs[cf], LLCubeMapArray::sClipToCubeUpVecs[cf]);
+            U32 res = mMipChain[0].getWidth();
 
-                F32 mat[16];
-                frame.getOpenGLRotation(mat);
-                gGL.loadMatrix(mat);
+            for (int i = 0; i < mMipChain.size(); ++i)
+            {
+                LL_PROFILE_GPU_ZONE("probe radiance gen");
+                static LLStaticHashedString sMipLevel("mipLevel");
+                static LLStaticHashedString sRoughness("roughness");
+                static LLStaticHashedString sWidth("u_width");
 
-                mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4);
-                
-                glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, i, 0, 0, probe->mCubeIndex * 6 + cf, 0, 0, res, res);
-            }
+                gRadianceGenProgram.uniform1f(sRoughness, (F32)i / (F32)(mMipChain.size() - 1));
+                gRadianceGenProgram.uniform1f(sMipLevel, i);
+                gRadianceGenProgram.uniform1i(sWidth, mMipChain[i].getWidth());
 
-            if (i != mMipChain.size() - 1)
-            {
-                res /= 2;
-                glViewport(0, 0, res, res);
-            }
-        }
+                for (int cf = 0; cf < 6; ++cf)
+                { // for each cube face
+                    LLCoordFrame frame;
+                    frame.lookAt(LLVector3(0, 0, 0), LLCubeMapArray::sClipToCubeLookVecs[cf], LLCubeMapArray::sClipToCubeUpVecs[cf]);
+
+                    F32 mat[16];
+                    frame.getOpenGLRotation(mat);
+                    gGL.loadMatrix(mat);
+
+                    mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4);
 
-        gRadianceGenProgram.unbind();
+                    glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, i, 0, 0, probe->mCubeIndex * 6 + cf, 0, 0, res, res);
+                }
 
-        //generate irradiance map
-        gIrradianceGenProgram.bind();
-        channel = gIrradianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
-        mTexture->bind(channel);
+                if (i != mMipChain.size() - 1)
+                {
+                    res /= 2;
+                    glViewport(0, 0, res, res);
+                }
+            }
 
-        gIrradianceGenProgram.uniform1i(sSourceIdx, targetIdx);
-        mVertexBuffer->setBuffer();
-        int start_mip = 0;
-        // find the mip target to start with based on irradiance map resolution
-        for (start_mip = 0; start_mip < mMipChain.size(); ++start_mip)
+            gRadianceGenProgram.unbind();
+        }
+        else if (!mRadiancePass)
         {
-            if (mMipChain[start_mip].getWidth() == LL_IRRADIANCE_MAP_RESOLUTION)
+            //generate irradiance map
+            gIrradianceGenProgram.bind();
+            S32 channel = gIrradianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
+            mTexture->bind(channel);
+
+            gIrradianceGenProgram.uniform1i(sSourceIdx, sourceIdx);
+            gIrradianceGenProgram.uniform1f(LLShaderMgr::REFLECTION_PROBE_MAX_LOD, mMaxProbeLOD);
+
+            mVertexBuffer->setBuffer();
+            int start_mip = 0;
+            // find the mip target to start with based on irradiance map resolution
+            for (start_mip = 0; start_mip < mMipChain.size(); ++start_mip)
             {
-                break;
+                if (mMipChain[start_mip].getWidth() == LL_IRRADIANCE_MAP_RESOLUTION)
+                {
+                    break;
+                }
             }
-        }
 
-        //for (int i = start_mip; i < mMipChain.size(); ++i)
-        {
-            int i = start_mip;
-            LL_PROFILE_GPU_ZONE("probe irradiance gen");
-            glViewport(0, 0, mMipChain[i].getWidth(), mMipChain[i].getHeight());
-            for (int cf = 0; cf < 6; ++cf)
-            { // for each cube face
-                LLCoordFrame frame;
-                frame.lookAt(LLVector3(0, 0, 0), LLCubeMapArray::sClipToCubeLookVecs[cf], LLCubeMapArray::sClipToCubeUpVecs[cf]);
-
-                F32 mat[16];
-                frame.getOpenGLRotation(mat);
-                gGL.loadMatrix(mat);
-
-                mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4);
-
-                S32 res = mMipChain[i].getWidth();
-                mIrradianceMaps->bind(channel);
-                glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, i - start_mip, 0, 0, probe->mCubeIndex * 6 + cf, 0, 0, res, res);
-                mTexture->bind(channel);
+            //for (int i = start_mip; i < mMipChain.size(); ++i)
+            {
+                int i = start_mip;
+                LL_PROFILE_GPU_ZONE("probe irradiance gen");
+                glViewport(0, 0, mMipChain[i].getWidth(), mMipChain[i].getHeight());
+                for (int cf = 0; cf < 6; ++cf)
+                { // for each cube face
+                    LLCoordFrame frame;
+                    frame.lookAt(LLVector3(0, 0, 0), LLCubeMapArray::sClipToCubeLookVecs[cf], LLCubeMapArray::sClipToCubeUpVecs[cf]);
+
+                    F32 mat[16];
+                    frame.getOpenGLRotation(mat);
+                    gGL.loadMatrix(mat);
+
+                    mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4);
+
+                    S32 res = mMipChain[i].getWidth();
+                    mIrradianceMaps->bind(channel);
+                    glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, i - start_mip, 0, 0, probe->mCubeIndex * 6 + cf, 0, 0, res, res);
+                    mTexture->bind(channel);
+                }
             }
         }
 
@@ -722,7 +751,7 @@ void LLReflectionMapManager::updateUniforms()
     LLSettingsSky::ptr_t psky = environment.getCurrentSky();
 
     F32 minimum_ambiance = psky->getTotalReflectionProbeAmbiance();
-    F32 ambscale = gCubeSnapshot ? 0.5f : 1.f;
+    F32 ambscale = gCubeSnapshot && !mRadiancePass ? 0.f : 1.f;
 
     for (auto* refmap : mReflectionMaps)
     {
@@ -912,12 +941,14 @@ void LLReflectionMapManager::initReflectionMaps()
 {
     if (mTexture.isNull())
     {
+        mProbeResolution = nhpo2(llclamp(gSavedSettings.getU32("RenderReflectionProbeResolution"), (U32)64, (U32)512));
+        mMaxProbeLOD = log2f(mProbeResolution) - 1.f; // number of mips - 1
         mReflectionProbeCount = llclamp(gSavedSettings.getS32("RenderReflectionProbeCount"), 1, LL_MAX_REFLECTION_PROBE_COUNT);
 
         mTexture = new LLCubeMapArray();
 
         // store mReflectionProbeCount+2 cube maps, final two cube maps are used for render target and radiance map generation source)
-        mTexture->allocate(LL_REFLECTION_PROBE_RESOLUTION, 4, mReflectionProbeCount + 2);
+        mTexture->allocate(mProbeResolution, 4, mReflectionProbeCount + 2);
 
         mIrradianceMaps = new LLCubeMapArray();
         mIrradianceMaps->allocate(LL_IRRADIANCE_MAP_RESOLUTION, 4, mReflectionProbeCount, FALSE);
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index 14a6c089da8fbae5dea0600d5efec07d16687bd6..5936b26b88979163dc0b837a8340cc96a4d967e9 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -38,7 +38,6 @@ class LLViewerObject;
 #define LL_MAX_REFLECTION_PROBE_COUNT 256
 
 // reflection probe resolution
-#define LL_REFLECTION_PROBE_RESOLUTION 128
 #define LL_IRRADIANCE_MAP_RESOLUTION 64
 
 // reflection probe mininum scale
@@ -94,6 +93,9 @@ class alignas(16) LLReflectionMapManager
     // call once at startup to allocate cubemap arrays
     void initReflectionMaps();
 
+    // True if currently updating a radiance map, false if currently updating an irradiance map
+    bool isRadiancePass() { return mRadiancePass; }
+
 private:
     friend class LLPipeline;
 
@@ -161,9 +163,21 @@ class alignas(16) LLReflectionMapManager
     LLReflectionMap* mUpdatingProbe = nullptr;
     U32 mUpdatingFace = 0;
 
+    // if true, we're generating the radiance map for the current probe, otherwise we're generating the irradiance map.
+    // Update sequence should be to generate the irradiance map from render of the world that has no irradiance,
+    // then generate the radiance map from a render of the world that includes irradiance.
+    // This should avoid feedback loops and ensure that the colors in the radiance maps match the colors in the environment.
+    bool mRadiancePass = false;
+
     LLPointer<LLReflectionMap> mDefaultProbe;  // default reflection probe to fall back to for pixels with no probe influences (should always be at cube index 0)
 
     // number of reflection probes to use for rendering (based on saved setting RenderReflectionProbeCount)
     U32 mReflectionProbeCount;
+
+    // resolution of reflection probes
+    U32 mProbeResolution = 128;
+
+    // maximum LoD of reflection probes (mip levels - 1)
+    F32 mMaxProbeLOD = 6.f;
 };
 
diff --git a/indra/newview/llsettingsvo.cpp b/indra/newview/llsettingsvo.cpp
index 870ac6bd5a5d4d940f3a225bdd7fc7d7c39d9b3f..a49bd11ffdcd7a804b70e954aa3244ee6c30a64f 100644
--- a/indra/newview/llsettingsvo.cpp
+++ b/indra/newview/llsettingsvo.cpp
@@ -68,6 +68,8 @@
 
 #undef  VERIFY_LEGACY_CONVERSION
 
+extern BOOL gCubeSnapshot;
+
 //=========================================================================
 namespace 
 {
@@ -714,7 +716,26 @@ void LLSettingsVOSky::applySpecial(void *ptarget, bool force)
     LLColor3 ambient(getTotalAmbient());
 
     shader->uniform3fv(LLShaderMgr::AMBIENT, LLVector3(ambient.mV));
-    shader->uniform3fv(LLShaderMgr::AMBIENT_LINEAR, linearColor3v(getAmbientColor()/3.f)); // note magic number 3.f comes from SLIDER_SCALE_SUN_AMBIENT
+
+    if (gCubeSnapshot && !gPipeline.mReflectionMapManager.isRadiancePass())
+    { // during an irradiance map update, disable ambient lighting (direct lighting only) and desaturate sky color (avoid tinting the world blue)
+        shader->uniform3fv(LLShaderMgr::AMBIENT_LINEAR, LLVector3::zero.mV);
+
+        auto max_vec = [](LLVector3 col)
+        {
+            col.mV[0] = col.mV[1] = col.mV[2] = llmax(llmax(col.mV[0], col.mV[1]), col.mV[2]);
+            return col;
+        };
+        shader->uniform3fv(LLShaderMgr::BLUE_HORIZON_LINEAR, max_vec(linearColor3v(getBlueHorizon() / 2.f))); // note magic number of 2.f comes from SLIDER_SCALE_BLUE_HORIZON_DENSITY
+        shader->uniform3fv(LLShaderMgr::BLUE_DENSITY_LINEAR, max_vec(linearColor3v(getBlueDensity() / 2.f)));
+    }
+    else
+    {
+        shader->uniform3fv(LLShaderMgr::AMBIENT_LINEAR, linearColor3v(getAmbientColor() / 3.f)); // note magic number 3.f comes from SLIDER_SCALE_SUN_AMBIENT
+        shader->uniform3fv(LLShaderMgr::BLUE_HORIZON_LINEAR, linearColor3v(getBlueHorizon() / 2.f)); // note magic number of 2.f comes from SLIDER_SCALE_BLUE_HORIZON_DENSITY
+        shader->uniform3fv(LLShaderMgr::BLUE_DENSITY_LINEAR, linearColor3v(getBlueDensity() / 2.f));
+    }
+
     shader->uniform3fv(LLShaderMgr::SUNLIGHT_LINEAR, linearColor3v(getSunlightColor()));
     shader->uniform3fv(LLShaderMgr::MOONLIGHT_LINEAR,linearColor3v(getMoonlightColor()));
 
@@ -724,16 +745,14 @@ void LLSettingsVOSky::applySpecial(void *ptarget, bool force)
     shader->uniform1f(LLShaderMgr::SUN_MOON_GLOW_FACTOR, getSunMoonGlowFactor());
     shader->uniform1f(LLShaderMgr::DENSITY_MULTIPLIER, getDensityMultiplier());
     shader->uniform1f(LLShaderMgr::DISTANCE_MULTIPLIER, getDistanceMultiplier());
-    
+
+    shader->uniform1f(LLShaderMgr::HAZE_DENSITY_LINEAR, sRGBtoLinear(getHazeDensity()));
+
     F32 g             = getGamma();
     F32 display_gamma = gSavedSettings.getF32("RenderDeferredDisplayGamma");
 
     shader->uniform1f(LLShaderMgr::GAMMA, g);
     shader->uniform1f(LLShaderMgr::DISPLAY_GAMMA, display_gamma);
-
-    shader->uniform3fv(LLShaderMgr::BLUE_HORIZON_LINEAR, linearColor3v(getBlueHorizon()/2.f)); // note magic number of 2.f comes from SLIDER_SCALE_BLUE_HORIZON_DENSITY
-    shader->uniform3fv(LLShaderMgr::BLUE_DENSITY_LINEAR, linearColor3v(getBlueDensity()/2.f));
-    shader->uniform1f(LLShaderMgr::HAZE_DENSITY_LINEAR, sRGBtoLinear(getHazeDensity()));
 }
 
 LLSettingsSky::parammapping_t LLSettingsVOSky::getParameterMap() const
diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp
index 497b2373dad611f80427983bd25b8041898b172f..28d6267029aba08ef63bfc69ec1cea41bb4052aa 100644
--- a/indra/newview/llviewerdisplay.cpp
+++ b/indra/newview/llviewerdisplay.cpp
@@ -1017,6 +1017,12 @@ void display_cube_face()
 
     display_update_camera();
 
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Env Update");
+        // update all the sky/atmospheric/water settings
+        LLEnvironment::instance().update(LLViewerCamera::getInstance());
+    }
+
     LLSpatialGroup::sNoDelete = TRUE;
         
     S32 occlusion = LLPipeline::sUseOcclusion;
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index 64f7535f05f48ad282f7aaf9191d082bb2590c68..03ffe5da488f2248b0d1bff1b71d23d1e8d1a777 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -806,11 +806,12 @@ LLPipeline::eFBOStatus LLPipeline::doAllocateScreenBuffer(U32 resX, U32 resY)
 bool LLPipeline::allocateScreenBuffer(U32 resX, U32 resY, U32 samples)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
-    if (mRT == &mMainRT)
+    if (mRT == &mMainRT && sReflectionProbesEnabled)
     { // hacky -- allocate auxillary buffer
         gCubeSnapshot = TRUE;
+        mReflectionMapManager.initReflectionMaps();
         mRT = &mAuxillaryRT;
-        U32 res = LL_REFLECTION_PROBE_RESOLUTION * 2;
+        U32 res = mReflectionMapManager.mProbeResolution * 2;  //multiply by 2 because probes will be super sampled
         allocateScreenBuffer(res, res, samples);
         mRT = &mMainRT;
         gCubeSnapshot = FALSE;
@@ -4172,10 +4173,16 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera)
 	calcNearbyLights(camera);
 	setupHWLights(NULL);
 
+    gGL.setSceneBlendType(LLRender::BT_ALPHA);
 	gGL.setColorMask(true, false);
 
 	pool_set_t::iterator iter1 = mPools.begin();
 
+    if (gDebugGL || gDebugPipeline)
+    {
+        LLGLState::checkStates(GL_FALSE);
+    }
+
 	while ( iter1 != mPools.end() )
 	{
 		LLDrawPool *poolp = *iter1;
@@ -8104,6 +8111,8 @@ void LLPipeline::bindDeferredShader(LLGLSLShader& shader, LLRenderTarget* light_
 
     shader.uniform3fv(LLShaderMgr::SUNLIGHT_COLOR, 1, mSunDiffuse.mV);
     shader.uniform3fv(LLShaderMgr::MOONLIGHT_COLOR, 1, mMoonDiffuse.mV);
+
+    shader.uniform1f(LLShaderMgr::REFLECTION_PROBE_MAX_LOD, mReflectionMapManager.mMaxProbeLOD);
 }