diff --git a/autobuild.xml b/autobuild.xml
index 11b2783bd72c29f0f93bed460517bf77a4b842eb..f0038f6532bb84ae7e632611464eae5026fbbf56 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -2198,6 +2198,62 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>description</key>
         <string>OpenAL Soft is a software implementation of the OpenAL 3D audio API.</string>
       </map>
+      <key>openexr</key>
+      <map>
+        <key>canonical_repo</key>
+        <string>https://github.com/secondlife/3p-openexr</string>
+        <key>copyright</key>
+        <string>Copyright (c) Contributors to the OpenEXR Project. All rights reserved.</string>
+        <key>description</key>
+        <string>OpenEXR provides the specification and reference implementation of the EXR file format, the professional-grade image storage format of the motion picture industry.</string>
+        <key>license</key>
+        <string>OpenEXR</string>
+        <key>license_file</key>
+        <string>LICENSES/openexr.txt</string>
+        <key>name</key>
+        <string>openexr</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>158cbe79bef4ecafb870052bbaca541e07107228</string>
+              <key>hash_algorithm</key>
+              <string>sha1</string>
+              <key>url</key>
+              <string>https://github.com/secondlife/3p-openexr/releases/download/v1.8/openexr-3.2.2-darwin64-6501c65.tar.zst</string>
+            </map>
+            <key>name</key>
+            <string>darwin64</string>
+          </map>
+          <key>windows64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>924f6ddf6669af023d1f3832cb79b50b913ae0ca</string>
+              <key>hash_algorithm</key>
+              <string>sha1</string>
+              <key>url</key>
+              <string>https://github.com/secondlife/3p-openexr/releases/download/v1.8/openexr-3.2.2-windows64-6501c65.tar.zst</string>
+            </map>
+            <key>name</key>
+            <string>windows64</string>
+          </map>
+        </map>
+        <key>source_type</key>
+        <string>git</string>
+        <key>vcs_branch</key>
+        <string>debug_autobuild</string>
+        <key>vcs_revision</key>
+        <string>5cd1075295c17b5f7085e83d5c16b13c7ecb2eb1</string>
+        <key>vcs_url</key>
+        <string>https://github.com/secondlife/3p-openexr</string>
+        <key>version</key>
+        <string>3.2.2</string>
+      </map>
       <key>openjpeg</key>
       <map>
         <key>platforms</key>
diff --git a/indra/cmake/Copy3rdPartyLibs.cmake b/indra/cmake/Copy3rdPartyLibs.cmake
index 9f79c13a97b33be19aecc024bd55c450b05533c1..415641f65f0b75403b79db7f351b78857d6c77ec 100644
--- a/indra/cmake/Copy3rdPartyLibs.cmake
+++ b/indra/cmake/Copy3rdPartyLibs.cmake
@@ -60,6 +60,12 @@ if(WINDOWS)
         nghttp2.dll
         libhunspell.dll
         uriparser.dll
+        Iex-3_2.dll
+        IlmThread-3_2.dll
+        Imath-3_1.dll
+        OpenEXR-3_2.dll
+        OpenEXRCore-3_2.dll
+        OpenEXRUtil-3_2.dll
         )
 
     # ICU4C (same filenames for 32 and 64 bit builds)
@@ -184,6 +190,12 @@ elseif(DARWIN)
         liburiparser.dylib
         liburiparser.1.dylib
         liburiparser.1.0.27.dylib
+        libIex-3_2.dylib
+        libIlmThread-3_2.dylib
+        libImath-3_1.dylib
+        libOpenEXR-3_2.dylib
+        libOpenEXRCore-3_2.dylib
+        libOpenEXRUtil-3_2.dylib
        )
 
     if (TARGET ll::fmodstudio)
diff --git a/indra/cmake/OpenEXR.cmake b/indra/cmake/OpenEXR.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..ee21fac541ae378622cd809299a98ed0550ac669
--- /dev/null
+++ b/indra/cmake/OpenEXR.cmake
@@ -0,0 +1,18 @@
+# -*- cmake -*-
+
+include(Prebuilt)
+
+include_guard()
+add_library( ll::openexr INTERFACE IMPORTED )
+
+if(USE_CONAN )
+  target_link_libraries( ll::openexr INTERFACE CONAN_PKG::openexr )
+  return()
+endif()
+
+use_prebuilt_binary(openexr)
+
+target_link_libraries( ll::openexr INTERFACE Iex-3_2 IlmThread-3_2 Imath-3_1 OpenEXR-3_2 OpenEXRCore-3_2 OpenEXRUtil-3_2)
+
+target_include_directories( ll::openexr SYSTEM INTERFACE ${LIBS_PREBUILT_DIR}/include/OpenEXR ${LIBS_PREBUILT_DIR}/include/Imath)
+
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index 53841332206419ec4756eb297a80ae1faa69989f..6cfe0653552c0acf3291b7dfe9a7ffc64a097d89 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -618,7 +618,7 @@ GLuint LLShaderMgr::loadShaderFile(const std::string& filename, S32 & shader_lev
     extra_code_text[extra_code_count++] = strdup("#define GBUFFER_FLAG_SKIP_ATMOS   0.0 \n"); // atmo kill
     extra_code_text[extra_code_count++] = strdup("#define GBUFFER_FLAG_HAS_ATMOS    0.34\n"); // bit 0
     extra_code_text[extra_code_count++] = strdup("#define GBUFFER_FLAG_HAS_PBR      0.67\n"); // bit 1
-    extra_code_text[extra_code_count++] = strdup("#define GBUFFER_FLAG_HAS_MIRROR      1.0\n");  // bit 2
+    extra_code_text[extra_code_count++] = strdup("#define GBUFFER_FLAG_HAS_HDRI      1.0\n");  // bit 2
     extra_code_text[extra_code_count++] = strdup("#define GET_GBUFFER_FLAG(flag)    (abs(norm.w-flag)< 0.1)\n");
 
 	if (defines)
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 6b15e847a32ad086ebe04ddffed6d80f51f746cb..8494ba5b4918fdacedff1f3d11eb5d854766fbc1 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -34,6 +34,7 @@ include(LLWindow)
 include(NDOF)
 include(NVAPI)
 include(OPENAL)
+include(OpenEXR)
 include(OpenGL)
 include(OpenSSL)
 include(PNG)
@@ -72,7 +73,6 @@ if (NOT HAVOK_TPV)
    endif()
 endif (NOT HAVOK_TPV)
 
-
 set(viewer_SOURCE_FILES
     groupchatlistener.cpp
     llaccountingcostmanager.cpp
@@ -1744,6 +1744,12 @@ if (WINDOWS)
       media_plugin_cef
       media_plugin_libvlc
       media_plugin_example
+      ${SHARED_LIB_STAGING_DIR}/Iex-3_2.dll
+      ${SHARED_LIB_STAGING_DIR}/IlmThread-3_2.dll
+      ${SHARED_LIB_STAGING_DIR}/Imath-3_1.dll
+      ${SHARED_LIB_STAGING_DIR}/OpenEXR-3_2.dll
+      ${SHARED_LIB_STAGING_DIR}/OpenEXRCore-3_2.dll
+      ${SHARED_LIB_STAGING_DIR}/OpenEXRUtil-3_2.dll
       )
 
     if (ADDRESS_SIZE EQUAL 64)
@@ -1937,6 +1943,7 @@ target_link_libraries(${VIEWER_BINARY_NAME}
         ll::bugsplat
         ll::tracy
         ll::icu4c
+        ll::openexr
         )
 
 if( TARGET ll::intel_memops )
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 9d4f4cf91142f03a9b8ff15ba276e824259ef30a..8420f32db858124c4dc4e8ae8db648c4eb2e4945 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -9274,6 +9274,17 @@
       <key>Value</key>
       <integer>1</integer>
     </map>
+    <key>RenderDesaturateIrradiance</key>
+    <map>
+      <key>Comment</key>
+      <string>Desaturate irradiance to remove blue tint</string>
+      <key>Persist</key>
+      <integer>1</integer>
+      <key>Type</key>
+      <string>Boolean</string>
+      <key>Value</key>
+      <integer>1</integer>
+    </map>
     <key>RenderDebugAlphaMask</key>
     <map>
       <key>Comment</key>
@@ -9340,6 +9351,28 @@
     <key>Value</key>
     <integer>0</integer>
   </map>
+  <key>RenderHDRIExposure</key>
+  <map>
+    <key>Comment</key>
+    <string>Exposure adjustment of HDRI when previewing an HDRI.  Units are EV.  Sane values would be -10 to 10.</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>F32</string>
+    <key>Value</key>
+    <real>0.0</real>
+  </map>
+  <key>RenderHDRIRotation</key>
+  <map>
+    <key>Comment</key>
+    <string>Rotation (in degrees) of environment when previewing an HDRI.</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>F32</string>
+    <key>Value</key>
+    <real>0.0</real>
+  </map>
   <key>RenderMaxOpenGLVersion</key>
   <map>
     <key>Comment</key>
diff --git a/indra/newview/app_settings/shaders/class1/deferred/postDeferredGammaCorrect.glsl b/indra/newview/app_settings/shaders/class1/deferred/postDeferredGammaCorrect.glsl
index 3443785e1adb82743d789034a11f57a482c0f221..d89377326e0e74f857fc979449cd072d796ec89a 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/postDeferredGammaCorrect.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/postDeferredGammaCorrect.glsl
@@ -97,6 +97,7 @@ vec3 toneMapACES_Hill(vec3 color)
 
 uniform float exposure;
 uniform float gamma;
+uniform float aces_mix;
 
 vec3 toneMap(vec3 color)
 {
@@ -106,7 +107,7 @@ vec3 toneMap(vec3 color)
     color *= exposure * exp_scale;
 
     // mix ACES and Linear here as a compromise to avoid over-darkening legacy content
-    color = mix(toneMapACES_Hill(color), color, 0.3);
+    color = mix(toneMapACES_Hill(color), color, aces_mix);
 #endif
 
     return color;
diff --git a/indra/newview/app_settings/shaders/class1/deferred/skyF.glsl b/indra/newview/app_settings/shaders/class1/deferred/skyF.glsl
index 9d9ba49d821a811aca69287dc29f6d5796121d08..cc5280d9293dd2fa81785995f99a1379c5cdbb70 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/skyF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/skyF.glsl
@@ -27,6 +27,13 @@
 in vec3 vary_HazeColor;
 in float vary_LightNormPosDot;
 
+#ifdef HAS_HDRI
+in vec3 vary_position;
+uniform float sky_hdr_scale;
+uniform mat3 env_mat;
+uniform sampler2D environmentMap;
+#endif
+
 uniform sampler2D rainbow_map;
 uniform sampler2D halo_map;
 
@@ -37,6 +44,9 @@ uniform float ice_level;
 out vec4 frag_data[4];
 
 vec3 srgb_to_linear(vec3 c);
+vec3 linear_to_srgb(vec3 c);
+
+#define PI 3.14159265
 
 /////////////////////////////////////////////////////////////////////////
 // The fragment shader for the sky
@@ -71,6 +81,14 @@ vec3 halo22(float d)
 
 void main()
 {
+#ifdef HAS_HDRI
+    vec3 pos = normalize(vary_position);
+    pos = env_mat * pos;
+    vec2 texCoord = vec2(atan(pos.z, pos.x) + PI, acos(pos.y)) / vec2(2.0 * PI, PI);
+    vec3 color = textureLod(environmentMap, texCoord.xy, 0).rgb * sky_hdr_scale;
+    color = min(color, vec3(8192*8192*16));
+#else
+
     // Potential Fill-rate optimization.  Add cloud calculation 
     // back in and output alpha of 0 (so that alpha culling kills 
     // the fragment) if the sky wouldn't show up because the clouds 
@@ -86,9 +104,12 @@ void main()
     color.rgb *= 2.;
     color.rgb = clamp(color.rgb, vec3(0), vec3(5));
 
+#endif
+
     frag_data[0] = vec4(0);
     frag_data[1] = vec4(0);
-    frag_data[2] = vec4(0.0,0.0,0.0,GBUFFER_FLAG_SKIP_ATMOS); //1.0 in norm.w masks off fog
+    frag_data[2] = vec4(0.0,0.0,0.0,GBUFFER_FLAG_SKIP_ATMOS);
     frag_data[3] = vec4(color.rgb, 1.0);
+
 }
 
diff --git a/indra/newview/app_settings/shaders/class1/deferred/skyV.glsl b/indra/newview/app_settings/shaders/class1/deferred/skyV.glsl
index 17ce2dee5b1dd639e08d323c2424de9816f7afb2..bbe9a5a838aa4df28dba742cd688892a639ddea4 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/skyV.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/skyV.glsl
@@ -35,6 +35,10 @@ in vec3 position;
 out vec3 vary_HazeColor;
 out float vary_LightNormPosDot;
 
+#ifdef HAS_HDRI
+out vec3 vary_position;
+#endif
+
 // Inputs
 uniform vec3 camPosLocal;
 
@@ -72,6 +76,10 @@ void main()
     // Get relative position
     vec3 rel_pos = position.xyz - camPosLocal.xyz + vec3(0, 50, 0);
 
+#ifdef HAS_HDRI
+    vary_position = rel_pos;
+#endif
+
     // Adj position vector to clamp altitude
     if (rel_pos.y > 0.)
     {
diff --git a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
index 2f90249169abae7715bcd50495848ba387d7a0ba..5cc7ea698a73a3e2ca1ce69cecc005856a413209 100644
--- a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
+++ b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
@@ -189,6 +189,10 @@ void main()
         vec3 v = -normalize(pos.xyz);
         color = pbrBaseLight(diffuseColor, specularColor, metallic, v, norm.xyz, perceptualRoughness, light_dir, sunlit_linear, scol, radiance, irradiance, colorEmissive, ao, additive, atten);
     }
+    else if (GET_GBUFFER_FLAG(GBUFFER_FLAG_HAS_HDRI))
+    {
+        color = texture(emissiveRect, tc).rgb;
+    }
     else if (!GET_GBUFFER_FLAG(GBUFFER_FLAG_HAS_ATMOS))
     {
         //should only be true of WL sky, just port over base color value
diff --git a/indra/newview/lldrawpoolwlsky.cpp b/indra/newview/lldrawpoolwlsky.cpp
index b14235f25c041f5eedc554f80645e1ef08a3bfed..a9cc138549ef9f8d05ecbf2ecba44aebfb673cc2 100644
--- a/indra/newview/lldrawpoolwlsky.cpp
+++ b/indra/newview/lldrawpoolwlsky.cpp
@@ -44,6 +44,7 @@
 #include "llsky.h"
 #include "llvowlsky.h"
 #include "llsettingsvo.h"
+#include "llviewercontrol.h"
 
 extern BOOL gCubeSnapshot;
 
@@ -127,6 +128,8 @@ void LLDrawPoolWLSky::renderDome(const LLVector3& camPosLocal, F32 camHeightLoca
 	gGL.popMatrix();
 }
 
+extern LLPointer<LLImageGL> gEXRImage;
+
 void LLDrawPoolWLSky::renderSkyHazeDeferred(const LLVector3& camPosLocal, F32 camHeightLocal) const
 {
     if (!gSky.mVOSkyp)
@@ -138,9 +141,33 @@ void LLDrawPoolWLSky::renderSkyHazeDeferred(const LLVector3& camPosLocal, F32 ca
 
 	if (gPipeline.canUseWindLightShaders() && gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_SKY))
 	{
+        if (gEXRImage.notNull())
+        {
+            sky_shader = &gEnvironmentMapProgram;
+            sky_shader->bind();
+            S32 idx = sky_shader->enableTexture(LLShaderMgr::ENVIRONMENT_MAP);
+            if (idx > -1)
+            {
+                gGL.getTexUnit(idx)->bind(gEXRImage);
+            }
+
+            static LLCachedControl<F32> hdri_exposure(gSavedSettings, "RenderHDRIExposure", 0.0f);
+            static LLCachedControl<F32> hdri_rotation(gSavedSettings, "RenderHDRIRotation", 0.f);
+            
+            LLMatrix3 rot;
+            rot.setRot(0.f, hdri_rotation*DEG_TO_RAD, 0.f);
+
+            sky_shader->uniform1f(LLShaderMgr::SKY_HDR_SCALE, powf(2.f, hdri_exposure));
+            sky_shader->uniformMatrix3fv(LLShaderMgr::DEFERRED_ENV_MAT, 1, GL_FALSE, (F32*) rot.mMatrix);
+        }
+        else
+        {
+            sky_shader->bind();
+        }
+
         LLGLSPipelineDepthTestSkyBox sky(true, true);
 
-        sky_shader->bind();
+        
 
         sky_shader->uniform1i(LLShaderMgr::CUBE_SNAPSHOT, gCubeSnapshot ? 1 : 0);
 
@@ -180,7 +207,7 @@ void LLDrawPoolWLSky::renderSkyHazeDeferred(const LLVector3& camPosLocal, F32 ca
 
 void LLDrawPoolWLSky::renderStarsDeferred(const LLVector3& camPosLocal) const
 {
-    if (!gSky.mVOSkyp)
+    if (!gSky.mVOSkyp || gEXRImage.notNull())
     {
         return;
     }
@@ -251,6 +278,11 @@ void LLDrawPoolWLSky::renderStarsDeferred(const LLVector3& camPosLocal) const
 
 void LLDrawPoolWLSky::renderSkyCloudsDeferred(const LLVector3& camPosLocal, F32 camHeightLocal, LLGLSLShader* cloudshader) const
 {
+    if (gEXRImage.notNull())
+    {
+        return;
+    }
+
 	if (gPipeline.canUseWindLightShaders() && gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_CLOUDS) && gSky.mVOSkyp && gSky.mVOSkyp->getCloudNoiseTex())
 	{
         LLSettingsSky::ptr_t psky = LLEnvironment::instance().getCurrentSky();
@@ -310,7 +342,7 @@ void LLDrawPoolWLSky::renderSkyCloudsDeferred(const LLVector3& camPosLocal, F32
 
 void LLDrawPoolWLSky::renderHeavenlyBodies()
 {
-    if (!gSky.mVOSkyp) return;
+    if (!gSky.mVOSkyp || gEXRImage.notNull()) return;
 
     LLGLSPipelineBlendSkyBox gls_skybox(true, true); // SL-14113 we need moon to write to depth to clip stars behind
 
diff --git a/indra/newview/llenvironment.cpp b/indra/newview/llenvironment.cpp
index affea3f69c93eab6157fa0ab94a715e1fa87c98f..0b535e15b018f87e30705dd7ec7ad8cf07f72507 100644
--- a/indra/newview/llenvironment.cpp
+++ b/indra/newview/llenvironment.cpp
@@ -1770,8 +1770,10 @@ void LLEnvironment::updateGLVariablesForSettings(LLShaderUniforms* uniforms, con
         case LLSD::TypeArray:
         {
             LLVector4 vect4(value);
+            // always identify as a radiance pass if desaturating irradiance is disabled
+            static LLCachedControl<bool> desaturate_irradiance(gSavedSettings, "RenderDesaturateIrradiance", true);
 
-            if (gCubeSnapshot && !gPipeline.mReflectionMapManager.isRadiancePass())
+            if (desaturate_irradiance && 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)
                 {
@@ -2966,7 +2968,7 @@ void LLEnvironment::DayTransition::animate()
 
 
     // pause probe updates and reset reflection maps on sky change
-    gPipeline.mReflectionMapManager.pause();
+    gPipeline.mReflectionMapManager.pause(mTransitionTime);
     gPipeline.mReflectionMapManager.reset();
 
     mSky = mStartSky->buildClone();
@@ -3569,7 +3571,7 @@ namespace
             mInjectedSky->setSource(target_sky);
 
             // clear reflection probes and pause updates during sky change
-            gPipeline.mReflectionMapManager.pause();
+            gPipeline.mReflectionMapManager.pause(transition);
             gPipeline.mReflectionMapManager.reset();
 
             mBlenderSky = std::make_shared<LLSettingsBlenderTimeDelta>(target_sky, start_sky, psky, transition);
diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp
index 4ad136e13ad99671cbda0faabce3da80bb650b37..b82172c506bb8b882e3123f8ed05b6cb84e1968f 100644
--- a/indra/newview/llfilepicker.cpp
+++ b/indra/newview/llfilepicker.cpp
@@ -61,6 +61,7 @@ LLFilePicker LLFilePicker::sInstance;
 #define RAW_FILTER L"RAW files (*.raw)\0*.raw\0"
 #define MODEL_FILTER L"Model files (*.dae)\0*.dae\0"
 #define MATERIAL_FILTER L"GLTF Files (*.gltf; *.glb)\0*.gltf;*.glb\0"
+#define HDRI_FILTER L"HDRI Files (*.exr)\0*.exr\0"
 #define MATERIAL_TEXTURES_FILTER L"GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)\0*.gltf;*.glb;*.tga;*.bmp;*.jpg;*.jpeg;*.png\0"
 #define SCRIPT_FILTER L"Script files (*.lsl)\0*.lsl\0"
 #define DICTIONARY_FILTER L"Dictionary files (*.dic; *.xcu)\0*.dic;*.xcu\0"
@@ -228,6 +229,10 @@ BOOL LLFilePicker::setupFilter(ELoadFilter filter)
             IMAGE_FILTER \
             L"\0";
         break;
+    case FFLOAD_HDRI:
+        mOFN.lpstrFilter = HDRI_FILTER \
+            L"\0";
+        break;
 	case FFLOAD_SCRIPT:
 		mOFN.lpstrFilter = SCRIPT_FILTER \
 			L"\0";
@@ -663,6 +668,8 @@ std::unique_ptr<std::vector<std::string>> LLFilePicker::navOpenFilterProc(ELoadF
             allowedv->push_back("gltf");
             allowedv->push_back("glb");
             break;
+        case FFLOAD_HDRI:
+            allowedv->push_back("exr");
         case FFLOAD_COLLADA:
             allowedv->push_back("dae");
             break;
diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h
index 38daff99379af7056b36e45fd1b4cbe48bef7484..891c0c048253954ebed278b8f7b939439a000f2b 100644
--- a/indra/newview/llfilepicker.h
+++ b/indra/newview/llfilepicker.h
@@ -89,6 +89,7 @@ class LLFilePicker
         FFLOAD_EXE = 14,          // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin
         FFLOAD_MATERIAL = 15,
         FFLOAD_MATERIAL_TEXTURE = 16,
+        FFLOAD_HDRI = 17,
 	};
 
 	enum ESaveFilter
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index ce389a5cad13394831b4297339673b9cbb6daa52..f9c5421866ac71ac22ed5554b2b3d913a1388cfd 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -38,6 +38,126 @@
 #include "llviewercontrol.h"
 #include "llenvironment.h"
 #include "llstartup.h"
+#include "llviewermenufile.h"
+#include "llnotificationsutil.h"
+
+
+// load an OpenEXR image from a file
+#define IMATH_HALF_NO_LOOKUP_TABLE 1
+#include <ImfInputFile.h>
+#include <ImfArray.h>
+#include <ImfHeader.h>
+#include <ImfFrameBuffer.h>
+#include <iostream>
+
+LLPointer<LLImageGL> gEXRImage;
+
+void load_exr(const std::string& filename)
+{
+    // reset reflection maps when previewing a new HDRI
+    gPipeline.mReflectionMapManager.reset();
+    gPipeline.mReflectionMapManager.initReflectionMaps();
+
+    try {
+        Imf::InputFile file(filename.c_str());
+        Imath::Box2i       dw = file.header().dataWindow();
+        int                width = dw.max.x - dw.min.x + 1;
+        int                height = dw.max.y - dw.min.y + 1;
+
+        Imf::Array2D<Imath::half> rPixels;
+        Imf::Array2D<Imath::half> gPixels;
+        Imf::Array2D<Imath::half> bPixels;
+
+        rPixels.resizeErase(height, width);
+        gPixels.resizeErase(height, width);
+        bPixels.resizeErase(height, width);
+
+        Imf::FrameBuffer frameBuffer;
+
+        frameBuffer.insert("R",                                    // name
+            Imf::Slice(Imf::HALF,                            // type
+                (char*)(&rPixels[0][0] -      // base
+                    dw.min.x -
+                    dw.min.y * width),
+                sizeof(rPixels[0][0]) * 1,     // xStride
+                sizeof(rPixels[0][0]) * width, // yStride
+                1, 1,                            // x/y sampling
+                0.0));                           // fillValue
+
+        frameBuffer.insert("G",                                    // name
+            Imf::Slice(Imf::HALF,                            // type
+                (char*)(&gPixels[0][0] -      // base
+                    dw.min.x -
+                    dw.min.y * width),
+                sizeof(gPixels[0][0]) * 1,     // xStride
+                sizeof(gPixels[0][0]) * width, // yStride
+                1, 1,                            // x/y sampling
+                0.0));                           // fillValue
+
+        frameBuffer.insert("B",                                    // name
+            Imf::Slice(Imf::HALF,                           // type
+                (char*)(&bPixels[0][0] -      // base
+                    dw.min.x -
+                    dw.min.y * width),
+                sizeof(bPixels[0][0]) * 1,     // xStride
+                sizeof(bPixels[0][0]) * width, // yStride
+                1, 1,                            // x/y sampling
+                FLT_MAX));                       // fillValue
+
+        file.setFrameBuffer(frameBuffer);
+        file.readPixels(dw.min.y, dw.max.y);
+
+        U32 texName = 0;
+        LLImageGL::generateTextures(1, &texName);
+
+        gEXRImage = new LLImageGL(texName, 4, GL_TEXTURE_2D, GL_RGB16F, GL_RGB16F, GL_FLOAT, LLTexUnit::TAM_CLAMP);
+        gEXRImage->setHasMipMaps(TRUE);
+        gEXRImage->setUseMipMaps(TRUE);
+        gEXRImage->setFilteringOption(LLTexUnit::TFO_TRILINEAR);
+
+        gGL.getTexUnit(0)->bind(gEXRImage);
+
+        std::vector<F32> data(width * height * 3);
+        for (int i = 0; i < width * height; ++i)
+        {
+            data[i * 3 + 0] = rPixels[i / width][i % width];
+            data[i * 3 + 1] = gPixels[i / width][i % width];
+            data[i * 3 + 2] = bPixels[i / width][i % width];
+        }
+
+        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data.data());
+        
+        glGenerateMipmap(GL_TEXTURE_2D);
+
+        gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
+
+    }
+    catch (const std::exception& e) {
+        LLSD notif_args;
+        notif_args["WHAT"] = filename;
+        notif_args["REASON"] = e.what();
+        LLNotificationsUtil::add("CannotLoad", notif_args);
+        return;
+    }
+}
+
+void hdri_preview()
+{
+    LLFilePickerReplyThread::startPicker(
+        [](const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter)
+        {
+            if (LLAppViewer::instance()->quitRequested())
+            {
+                return;
+            }
+            if (filenames.size() > 0)
+            {
+                load_exr(filenames[0]);
+            }
+        },
+        LLFilePicker::FFLOAD_HDRI,
+        true);
+}
 
 extern BOOL gCubeSnapshot;
 extern BOOL gTeleportDisplay;
@@ -133,6 +253,11 @@ void LLReflectionMapManager::update()
         return;
     }
 
+    if (mPaused && gFrameTimeSeconds > mResumeTime)
+    {
+        resume();
+    }
+
     initReflectionMaps();
 
     if (!mRenderTarget.isComplete())
@@ -831,9 +956,10 @@ void LLReflectionMapManager::reset()
     mReset = true;
 }
 
-void LLReflectionMapManager::pause()
+void LLReflectionMapManager::pause(F32 duration)
 {
     mPaused = true;
+    mResumeTime = gFrameTimeSeconds + duration;
 }
 
 void LLReflectionMapManager::resume()
@@ -1283,6 +1409,8 @@ void LLReflectionMapManager::initReflectionMaps()
 
     if (mTexture.isNull() || mReflectionProbeCount != count || mReset)
     {
+        gEXRImage = nullptr;
+
         mReset = false;
         mReflectionProbeCount = count;
         mProbeResolution = nhpo2(llclamp(gSavedSettings.getU32("RenderReflectionProbeResolution"), (U32)64, (U32)512));
@@ -1340,7 +1468,6 @@ void LLReflectionMapManager::initReflectionMaps()
         mDefaultProbe->mComplete = default_complete;
 
         touch_default_probe(mDefaultProbe);
-
     }
 
     if (mVertexBuffer.isNull())
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index 0fee99eefcd7b0444ee42ef462ea426c5686929d..5c0651bc248a1cf65e3e1a6c8a1a2c89d56d9fa4 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -87,7 +87,8 @@ class alignas(16) LLReflectionMapManager
     void reset();
 
     // pause all updates other than the default probe
-    void pause();
+    // duration - number of seconds to pause (default 10)
+    void pause(F32 duration = 10.f);
 
     // unpause (see pause)
     void resume();
@@ -208,5 +209,6 @@ class alignas(16) LLReflectionMapManager
 
     // if true, only update the default probe
     bool mPaused = false;
+    F32 mResumeTime = 0.f;
 };
 
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index d5e4de03a964138296122a7edb83331bc89c9879..c50ae2e15336f47143cdb44b33b89e706337f9af 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -7890,6 +7890,19 @@ class LLAdvancedClickRenderBenchmark: public view_listener_t
 	}
 };
 
+void hdri_preview();
+
+class LLAdvancedClickHDRIPreview: public view_listener_t
+{
+    bool handleEvent(const LLSD& userdata)
+    {
+        // open personal lighting floater when previewing an HDRI (keeps HDRI from implicitly unloading when opening build tools)
+        LLFloaterReg::showInstance("env_adjust_snapshot");
+        hdri_preview();
+        return true;
+    }
+};
+
 // these are used in the gl menus to set control values that require shader recompilation
 class LLToggleShaderControl : public view_listener_t
 {
@@ -9529,6 +9542,7 @@ void initialize_menus()
 	view_listener_t::addMenu(new LLAdvancedClickRenderShadowOption(), "Advanced.ClickRenderShadowOption");
 	view_listener_t::addMenu(new LLAdvancedClickRenderProfile(), "Advanced.ClickRenderProfile");
 	view_listener_t::addMenu(new LLAdvancedClickRenderBenchmark(), "Advanced.ClickRenderBenchmark");
+    view_listener_t::addMenu(new LLAdvancedClickHDRIPreview(), "Advanced.ClickHDRIPreview");
 	view_listener_t::addMenu(new LLAdvancedPurgeShaderCache(), "Advanced.ClearShaderCache");
     view_listener_t::addMenu(new LLAdvancedRebuildTerrain(), "Advanced.RebuildTerrain");
 
diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp
index 354cc790367a544671947700b31a7480bd7740df..dc20f035c9b882149820864c2bea63b5f136e990 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -201,6 +201,7 @@ LLGLSLShader			gLuminanceProgram;
 LLGLSLShader			gFXAAProgram;
 LLGLSLShader			gDeferredPostNoDoFProgram;
 LLGLSLShader			gDeferredWLSkyProgram;
+LLGLSLShader            gEnvironmentMapProgram;
 LLGLSLShader			gDeferredWLCloudProgram;
 LLGLSLShader			gDeferredWLSunProgram;
 LLGLSLShader			gDeferredWLMoonProgram;
@@ -315,6 +316,7 @@ void LLViewerShaderMgr::finalizeShaderList()
     mShaderList.push_back(&gDeferredEmissiveProgram);
     mShaderList.push_back(&gDeferredAvatarEyesProgram);
     mShaderList.push_back(&gDeferredAvatarAlphaProgram);
+    mShaderList.push_back(&gEnvironmentMapProgram);
     mShaderList.push_back(&gDeferredWLSkyProgram);
     mShaderList.push_back(&gDeferredWLCloudProgram);
     mShaderList.push_back(&gDeferredWLMoonProgram);
@@ -987,6 +989,7 @@ BOOL LLViewerShaderMgr::loadShadersDeferred()
         gNoPostGammaCorrectProgram.unload();
         gLegacyPostGammaCorrectProgram.unload();
 		gFXAAProgram.unload();
+        gEnvironmentMapProgram.unload();
 		gDeferredWLSkyProgram.unload();
 		gDeferredWLCloudProgram.unload();
         gDeferredWLSunProgram.unload();
@@ -2268,6 +2271,26 @@ BOOL LLViewerShaderMgr::loadShadersDeferred()
 		llassert(success);
 	}
 
+    if (success)
+    {
+        gEnvironmentMapProgram.mName = "Environment Map Program";
+        gEnvironmentMapProgram.mShaderFiles.clear();
+        gEnvironmentMapProgram.mFeatures.calculatesAtmospherics = true;
+        gEnvironmentMapProgram.mFeatures.hasAtmospherics = true;
+        gEnvironmentMapProgram.mFeatures.hasGamma = true;
+        gEnvironmentMapProgram.mFeatures.hasSrgb = true;
+
+        gEnvironmentMapProgram.clearPermutations();
+        gEnvironmentMapProgram.addPermutation("HAS_HDRI", "1");
+        gEnvironmentMapProgram.mShaderFiles.push_back(make_pair("deferred/skyV.glsl", GL_VERTEX_SHADER));
+        gEnvironmentMapProgram.mShaderFiles.push_back(make_pair("deferred/skyF.glsl", GL_FRAGMENT_SHADER));
+        gEnvironmentMapProgram.mShaderLevel = mShaderLevel[SHADER_DEFERRED];
+        gEnvironmentMapProgram.mShaderGroup = LLGLSLShader::SG_SKY;
+
+        success = gEnvironmentMapProgram.createShader(NULL, NULL);
+        llassert(success);
+    }
+
 	if (success)
 	{
 		gDeferredWLSkyProgram.mName = "Deferred Windlight Sky Shader";
diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h
index 2502be6bb14f47f7c2d197f56cab53a64f3588af..c51f583ebc5a5a2298b5eb1693410c592a8dca5f 100644
--- a/indra/newview/llviewershadermgr.h
+++ b/indra/newview/llviewershadermgr.h
@@ -266,6 +266,7 @@ extern LLGLSLShader			gHUDFullbrightAlphaMaskAlphaProgram;
 extern LLGLSLShader			gDeferredEmissiveProgram;
 extern LLGLSLShader			gDeferredAvatarEyesProgram;
 extern LLGLSLShader			gDeferredAvatarAlphaProgram;
+extern LLGLSLShader         gEnvironmentMapProgram;
 extern LLGLSLShader			gDeferredWLSkyProgram;
 extern LLGLSLShader			gDeferredWLCloudProgram;
 extern LLGLSLShader			gDeferredWLSunProgram;
diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp
index fee00eb6f42459796f9b561662afc1f109f94eab..35e45c6cd9b8413dfe3dd6f2dd300832798c2486 100644
--- a/indra/newview/llvoavatar.cpp
+++ b/indra/newview/llvoavatar.cpp
@@ -5233,9 +5233,6 @@ U32 LLVOAvatar::renderRigid()
 		return 0;
 	}
 
-	bool should_alpha_mask = shouldAlphaMask();
-	LLGLState test(GL_ALPHA_TEST, should_alpha_mask);
-
 	if (isTextureVisible(TEX_EYES_BAKED) || (getOverallAppearance() == AOA_JELLYDOLL && !isControlAvatar()) || isUIAvatar())
 	{
 		LLViewerJoint* eyeball_left = getViewerJoint(MESH_ID_EYEBALL_LEFT);
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index 94ec5c0817bda33a5bd33986f1c84dfaa661f660..780912974396ee493e78db2aff58e9e091766b98 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -6869,6 +6869,8 @@ void LLPipeline::generateExposure(LLRenderTarget* src, LLRenderTarget* dst, bool
 	}
 }
 
+extern LLPointer<LLImageGL> gEXRImage;
+
 void LLPipeline::gammaCorrect(LLRenderTarget* src, LLRenderTarget* dst) {
 	dst->bindTarget();
 	// gamma correct lighting
@@ -6905,8 +6907,10 @@ void LLPipeline::gammaCorrect(LLRenderTarget* src, LLRenderTarget* dst) {
 		F32 e = llclamp(exposure(), 0.5f, 4.f);
 
 		static LLStaticHashedString s_exposure("exposure");
+        static LLStaticHashedString aces_mix("aces_mix");
 
         shader.uniform1f(s_exposure, e);
+        shader.uniform1f(aces_mix, gEXRImage.notNull() ? 0.f : 0.3f);
 
 		mScreenTriangleVB->setBuffer();
 		mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3);
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index 4719b091ab4f89b57f049c5c294440f1058da2a4..591b5537c7c5bd7f9a3fc931f3d89e964e4bd5ee 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -2824,6 +2824,12 @@ function="World.EnvPreset"
               <menu_item_call.on_click
                function="Advanced.ClickRenderBenchmark" />
           </menu_item_call>
+          <menu_item_call
+           label="HDRI Preview"
+           name="HDRI Preview">
+            <menu_item_call.on_click
+             function="Advanced.ClickHDRIPreview" />
+          </menu_item_call>
         </menu>
       <menu
         create_jump_keys="true"
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index df9f53686e2b197340791d4c23ccfdae14ed389e..1a6cadf43e0482b1ad22f7edef7652612e591f8c 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -9259,6 +9259,15 @@ Unable to upload texture.
   <tag>fail</tag>  
   </notification>
 
+  <notification
+ icon="alertmodal.tga"
+ name="CannotLoad"
+ type="alertmodal">
+    Unable to load [WHAT].
+    [REASON]
+    <tag>fail</tag>
+  </notification>
+
   <notification
    icon="alertmodal.tga"
    name="CannotUploadMaterial"
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index c7f32d0da9320f375991154df8f755764d418adb..70121ecc6496d8e47b8a427bb9ddd2d1b3ec9159 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -587,6 +587,14 @@ def construct(self):
             self.path("libcrypto-1_1-x64.dll")
             self.path("libssl-1_1-x64.dll")
 
+            # OpenEXR
+            self.path("Iex-3_2.dll")
+            self.path("IlmThread-3_2.dll")
+            self.path("Imath-3_1.dll")
+            self.path("OpenEXR-3_2.dll")
+            self.path("OpenEXRCore-3_2.dll")
+            self.path("OpenEXRUtil-3_2.dll")
+
             # HTTP/2
             self.path("nghttp2.dll")
 
@@ -934,6 +942,12 @@ def construct(self):
                 with self.prefix(src=relpkgdir, dst=""):
                     self.path("libndofdev.dylib")
                     self.path("libhunspell-*.dylib")   
+                    self.path("libIex-3_2.dylib")
+                    self.path("libIlmThread-3_2.dylib")
+                    self.path("libImath-3_1.dylib")
+                    self.path("libOpenEXR-3_2.dylib")
+                    self.path("libOpenEXRCore-3_2.dylib")
+                    self.path("libOpenEXRUtil-3_2.dylib")
 
                 with self.prefix(src_dst="cursors_mac"):
                     self.path("*.tif")