From ef6c8622a05b9e3a670628982fbaa151e4e30068 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 08:56:15 -0400 Subject: [PATCH 1/3] The last shadow cascade fades its receivers to lit, not past it sampleDirectionalShadow faded the last cascade out toward its far split by adding the fade to the filtered shadow, so a lit receiver there came to twice lit at the split and dropped back to lit past it: a hard line across the far view that moved with the draw distance and the camera's angle. Opaque surfaces never showed the line, as sunLightF clamps the shadow it writes to the light map at 1, but the water, alpha and PBR alpha shaders read the shadow raw, and the water's sun glint drew the line. The fade now mixes the filtered shadow toward lit. On opaque surfaces lit and fully shadowed receivers fade as they did; a penumbra in the far half of the cascade, which the clamped sum took to lit sooner, now fades in proportion and is a little darker than it was. sampleSpotShadow carried the same addition and mixes too. It also weighted its one map as a cascade and divided by the weight, which is zero three quarters of the way to the sun's third split, so a receiver at that distance came out NaN. With one map there is nothing to weight, and the division goes. Co-Authored-By: Claude Opus 5.5 --- .../shaders/class1/deferred/shadowUtil.glsl | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/indra/newview/app_settings/shaders/class1/deferred/shadowUtil.glsl b/indra/newview/app_settings/shaders/class1/deferred/shadowUtil.glsl index def92dd470c..7d847fdbfc6 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/shadowUtil.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/shadowUtil.glsl @@ -530,9 +530,12 @@ float sampleDirectionalShadow(vec3 pos, vec3 norm, vec2 pos_screen) float w = 1.0; w -= max(spos.z - far_split.z, 0.0) / transition_domain.z; - shadow += filterShadow(shadowMap3, lpos, dLdx, dLdy, shadow_res, const_bias, pos_screen) * w; + // Toward the far split the last cascade fades to lit, where the shadows end. A blend: added on top, it + // lifted a lit receiver up to twice its light there and dropped it back at the split, which sunLightF's + // clamp hides on opaque surfaces and the water and alpha, reading this raw, showed as a line. + float to_lit = clamp((pos.z + shadow_clip.z) / (shadow_clip.z - shadow_clip.w) * 2.0 - 1.0, 0.0, 1.0); + shadow += mix(filterShadow(shadowMap3, lpos, dLdx, dLdy, shadow_res, const_bias, pos_screen), 1.0, to_lit) * w; weight += w; - shadow += max((pos.z + shadow_clip.z) / (shadow_clip.z - shadow_clip.w) * 2.0 - 1.0, 0.0); } if (spos.z < near_split.y && spos.z > far_split.z) @@ -603,37 +606,29 @@ float sampleSpotShadow(vec3 pos, vec3 norm, int index, vec2 pos_screen) { vec4 lpos; - vec4 near_split = shadow_clip * -0.75; - vec4 far_split = shadow_clip * -1.25; - vec4 transition_domain = near_split - far_split; - float weight = 0.0; - + // A sampler2DShadow cannot live in a local or a ternary (GLSL opaque type), so pick + // the map by branch and pass it straight in. + float filtered; + if (index == 0) { - float w = 1.0; - w -= max(spos.z - far_split.z, 0.0) / transition_domain.z; - - // A sampler2DShadow cannot live in a local or a ternary (GLSL opaque type), so pick - // the map by branch and pass it straight in. - if (index == 0) - { - lpos = shadow_matrix[4] * spos; - vec4 dLdx = shadow_matrix[4] * ddx4; - vec4 dLdy = shadow_matrix[4] * ddy4; - shadow += filterShadow(shadowMap4, lpos, dLdx, dLdy, proj_shadow_res, const_bias, pos_screen) * w; - } - else - { - lpos = shadow_matrix[5] * spos; - vec4 dLdx = shadow_matrix[5] * ddx4; - vec4 dLdy = shadow_matrix[5] * ddy4; - shadow += filterShadow(shadowMap5, lpos, dLdx, dLdy, proj_shadow_res, const_bias, pos_screen) * w; - } - - weight += w; - shadow += max((pos.z + shadow_clip.z) / (shadow_clip.z - shadow_clip.w) * 2.0 - 1.0, 0.0); + lpos = shadow_matrix[4] * spos; + vec4 dLdx = shadow_matrix[4] * ddx4; + vec4 dLdy = shadow_matrix[4] * ddy4; + filtered = filterShadow(shadowMap4, lpos, dLdx, dLdy, proj_shadow_res, const_bias, pos_screen); + } + else + { + lpos = shadow_matrix[5] * spos; + vec4 dLdx = shadow_matrix[5] * ddx4; + vec4 dLdy = shadow_matrix[5] * ddy4; + filtered = filterShadow(shadowMap5, lpos, dLdx, dLdy, proj_shadow_res, const_bias, pos_screen); } - shadow /= weight; + // One map, so no cascade weight to blend by: a cascade's weight here would cancel, and is zero three quarters + // of the way to shadow_clip.z, where dividing by it gives NaN. Faded to lit toward the far split, as the sun's + // last cascade is (sampleDirectionalShadow). + float to_lit = clamp((pos.z + shadow_clip.z) / (shadow_clip.z - shadow_clip.w) * 2.0 - 1.0, 0.0, 1.0); + shadow = mix(filtered, 1.0, to_lit); } else { From 79a9341e5867fe3626dd046f8e90e095cf944bd0 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 00:39:50 -0400 Subject: [PATCH 2/3] LLGLSLShader's uniform cache holds the bits it uploaded, and uniform4iv uploads all four ints The cache that lets a uniform setter skip a value the program already holds kept a float LLVector4 per location in a boost::unordered_map. An int or uint went through a float on the way in, so past 2^24 two values compared equal and the change was dropped: uniform1ui(FRAME_ID) after about 77 hours at 60 fps, and the CAS parameters, uploaded as the bits of floats through uniform4uiv, whenever they moved by less than about 64 ulps. Values of every type shared one slot, the fast setters uploaded without telling it, and a bare link() kept values the relink had reset. Every set paid a hash and a bucket walk. The cache is a flat array indexed by uniform location, sized at mapUniforms past the largest location a setter can reach. A slot holds the kind of upload that wrote it (the GL component type and count) and the uploaded components' bit patterns; a set matches only when both do, so ints and uints compare as themselves and floats by their bits, and a location written through one kind and set through another uploads. The reserved-index setters and the named one share a location's slot. A set costs a bounds compare, one 20-byte slot and an OR of XORs; there is no hashing and no allocation. Arrays (count != 1) always upload and forget their location's slot; matrices neither read nor write it. The fast setters record what they upload, and link, mapUniforms and unload empty the cache. uniform4iv uploaded with glUniform1iv, which on an ivec4 uniform is GL_INVALID_OPERATION and sets nothing; nothing in the tree called it yet. It uploads with glUniform4iv, cached by its four ints. llglslshader_gl reads back what the program holds: ints, ivec4s, uints and uvec4s set past 2^24 and one ulp apart; set A, B, A again; a slot empty after link against a value put there behind the cache; link and remapping forgetting; a bool set through int and float; the named and reserved setters sharing a slot; a fast set between two normal ones; arrays forgetting. Taking an int through a float again fails it. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/CMakeLists.txt | 2 + indra/llrender/llglslshader.cpp | 291 ++++++----- indra/llrender/llglslshader.h | 115 ++++- indra/llrender/tests/llglslshader_gl_test.cpp | 472 ++++++++++++++++++ 4 files changed, 756 insertions(+), 124 deletions(-) create mode 100644 indra/llrender/tests/llglslshader_gl_test.cpp diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index e48ef4564b7..303b019ca8a 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -178,4 +178,6 @@ if(AL_BUILD_TESTS) al_add_test(llrendertarget_gl GL PROJECT llrender LIBRARIES llrender) # LLRender's cached clear colour and scissor box, and LLGLSScissor. al_add_test(alrenderstate_gl GL PROJECT llrender LIBRARIES llrender) + # LLGLSLShader's uniform value cache: what a program holds after each kind of set, and across a relink. + al_add_test(llglslshader_gl GL PROJECT llrender LIBRARIES llrender) endif() diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index f1bf366e05e..61d3bdeca79 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -38,6 +38,8 @@ #include "hbxxh.h" #include "llsdserialize.h" +#include + #if LL_DARWIN #include "OpenGL/OpenGL.h" #endif @@ -92,10 +94,13 @@ const std::string gShaderConstsVal[LLGLSLShader::NUM_SHADER_CONSTS] = , "0.99999" // SHADER_CONST_STAR_DEPTH // SL-14113 }; - -bool shouldChange(const LLVector4& v1, const LLVector4& v2) +namespace { - return v1 != v2; + // The bits a component is uploaded as, which is what the uniform value cache compares: an int or a uint as + // itself, a float by its bits, never by value. + inline U32 uniform_bits(GLint v) { return std::bit_cast(v); } + inline U32 uniform_bits(GLuint v) { return v; } + inline U32 uniform_bits(GLfloat v) { return std::bit_cast(v); } } #if !LL_RELEASE_FOR_DOWNLOAD @@ -662,7 +667,9 @@ void LLGLSLShader::unloadInternal() mAttribute.clear(); mTexture.clear(); mUniform.clear(); - mMinimumAlpha = MINIMUM_ALPHA_UNSET; // program state is going away with the program + // program state is going away with the program + mUniformCache.clear(); + mMinimumAlpha = MINIMUM_ALPHA_UNSET; if (mProgramObject) { @@ -1168,8 +1175,10 @@ bool LLGLSLShader::mapUniforms() mUniform.clear(); mUniformMap.clear(); mTexture.clear(); - mValue.clear(); - mMinimumAlpha = MINIMUM_ALPHA_UNSET; // fresh mapping: GPU-side value is the default again + // Fresh mapping: GPU-side values are the defaults again. The cache stays empty while the samplers below are + // given their units, and is sized once every location is known. + mUniformCache.clear(); + mMinimumAlpha = MINIMUM_ALPHA_UNSET; //initialize arrays mUniform.resize(LLShaderMgr::instance()->mReservedUniforms.size(), -1); mTexture.resize(LLShaderMgr::instance()->mReservedUniforms.size(), -1); @@ -1258,6 +1267,24 @@ bool LLGLSLShader::mapUniforms() // acts as a canary for adding textures and breaking machines limited to 32. llassert(max_index == 0 || mActiveTextureChannels <= 32); + // Every location a setter can reach is an active uniform's, each one in mUniformMap by name, the reserved ones + // included; the cache covers all of them up to its cap. + size_t locations = 0; + for (const auto& named : mUniformMap) + { + if (named.second >= 0) + { + locations = llmax(locations, (size_t)named.second + 1); + } + } + if (locations > UniformCache::MAX_LOCATIONS) + { + LL_WARNS_ONCE("Shader") << mName << " has a uniform at location " << locations - 1 + << "; uniforms past " << UniformCache::MAX_LOCATIONS + << " upload on every set" << LL_ENDL; + } + mUniformCache.reset(locations); + // Set up block binding, in a way supported by Apple (rather than binding = 1 in .glsl). // See slide 35 and more of https://docs.huihoo.com/apple/wwdc/2011/session_420__advances_in_opengl_for_mac_os_x_lion.pdf for (U32 i = 0; i < NUM_UNIFORM_BLOCKS; ++i) @@ -1307,6 +1334,11 @@ bool LLGLSLShader::link(bool suppress_errors) bool success = LLShaderMgr::instance()->linkProgramObject(mProgramObject, suppress_errors, mName); + // A link resets every uniform to its initializer and may move its location; nothing cached for the old link + // holds. Sets upload uncached until mapUniforms sizes the cache for this one. + mUniformCache.clear(); + mMinimumAlpha = MINIMUM_ALPHA_UNSET; + if (!success && !suppress_errors) { LLShaderMgr::instance()->dumpObjectLog(mProgramObject, !success, mName); @@ -1618,14 +1650,10 @@ void LLGLSLShader::uniform1i(U32 index, GLint x) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 && mUniformCache.changed(location, UniformCache::Kind::Int, uniform_bits(x))) { - const auto& iter = mValue.find(mUniform[index]); - if (iter == mValue.end() || iter->second.mV[0] != x) - { - glUniform1i(mUniform[index], x); - mValue[mUniform[index]] = LLVector4((F32)x, 0.f, 0.f, 0.f); - } + glUniform1i(location, x); } } } @@ -1644,18 +1672,16 @@ void LLGLSLShader::uniform1f(U32 index, GLfloat x) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 && mUniformCache.changed(location, UniformCache::Kind::Float, uniform_bits(x))) { - const auto& iter = mValue.find(mUniform[index]); - if (iter == mValue.end() || iter->second.mV[0] != x) - { - glUniform1f(mUniform[index], x); - mValue[mUniform[index]] = LLVector4(x, 0.f, 0.f, 0.f); - } + glUniform1f(location, x); } } } +// Uploads without comparing, and records what it uploaded: a cached setter writing the same location afterwards +// compares against what GL holds. void LLGLSLShader::fastUniform1f(U32 index, GLfloat x) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; @@ -1663,7 +1689,9 @@ void LLGLSLShader::fastUniform1f(U32 index, GLfloat x) llassert(mProgramObject); llassert(index < mUniform.size()); llassert(mUniform[index] >= 0); - glUniform1f(mUniform[index], x); + const GLint location = mUniform[index]; + glUniform1f(location, x); + mUniformCache.uploaded(location, UniformCache::Kind::Float, uniform_bits(x)); } void LLGLSLShader::uniform1ui(U32 index, GLuint x) @@ -1679,14 +1707,10 @@ void LLGLSLShader::uniform1ui(U32 index, GLuint x) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 && mUniformCache.changed(location, UniformCache::Kind::UInt, uniform_bits(x))) { - const auto& iter = mValue.find(mUniform[index]); - if (iter == mValue.end() || iter->second.mV[0] != (F32)x) - { - glUniform1ui(mUniform[index], x); - mValue[mUniform[index]] = LLVector4((F32)x, 0.f, 0.f, 0.f); - } + glUniform1ui(location, x); } } } @@ -1705,15 +1729,10 @@ void LLGLSLShader::uniform2f(U32 index, GLfloat x, GLfloat y) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 && mUniformCache.changed(location, UniformCache::Kind::Vec2, uniform_bits(x), uniform_bits(y))) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(x, y, 0.f, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec)) - { - glUniform2f(mUniform[index], x, y); - mValue[mUniform[index]] = vec; - } + glUniform2f(location, x, y); } } } @@ -1732,15 +1751,11 @@ void LLGLSLShader::uniform3f(U32 index, GLfloat x, GLfloat y, GLfloat z) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 + && mUniformCache.changed(location, UniformCache::Kind::Vec3, uniform_bits(x), uniform_bits(y), uniform_bits(z))) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(x, y, z, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec)) - { - glUniform3f(mUniform[index], x, y, z); - mValue[mUniform[index]] = vec; - } + glUniform3f(location, x, y, z); } } } @@ -1759,15 +1774,12 @@ void LLGLSLShader::uniform4f(U32 index, GLfloat x, GLfloat y, GLfloat z, GLfloat return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location >= 0 + && mUniformCache.changed(location, UniformCache::Kind::Vec4, + uniform_bits(x), uniform_bits(y), uniform_bits(z), uniform_bits(w))) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(x, y, z, w); - if (iter == mValue.end() || shouldChange(iter->second, vec)) - { - glUniform4f(mUniform[index], x, y, z, w); - mValue[mUniform[index]] = vec; - } + glUniform4f(location, x, y, z, w); } } } @@ -1786,15 +1798,19 @@ void LLGLSLShader::uniform1iv(U32 index, U32 count, const GLint* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec((F32)v[0], 0.f, 0.f, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - glUniform1iv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + mUniformCache.forget(location); + glUniform1iv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::Int, uniform_bits(v[0]))) + { + glUniform1iv(location, 1, v); } } } @@ -1813,15 +1829,20 @@ void LLGLSLShader::uniform4iv(U32 index, U32 count, const GLint* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec((F32)v[0], (F32)v[1], (F32)v[2], (F32)v[3]); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - glUniform1iv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + mUniformCache.forget(location); + glUniform4iv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::IVec4, + uniform_bits(v[0]), uniform_bits(v[1]), uniform_bits(v[2]), uniform_bits(v[3]))) + { + glUniform4iv(location, 1, v); } } } @@ -1841,15 +1862,19 @@ void LLGLSLShader::uniform1fv(U32 index, U32 count, const GLfloat* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(v[0], 0.f, 0.f, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - glUniform1fv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + mUniformCache.forget(location); + glUniform1fv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::Float, uniform_bits(v[0]))) + { + glUniform1fv(location, 1, v); } } } @@ -1868,15 +1893,19 @@ void LLGLSLShader::uniform2fv(U32 index, U32 count, const GLfloat* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(v[0], v[1], 0.f, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - glUniform2fv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + mUniformCache.forget(location); + glUniform2fv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::Vec2, uniform_bits(v[0]), uniform_bits(v[1]))) + { + glUniform2fv(location, 1, v); } } } @@ -1895,15 +1924,20 @@ void LLGLSLShader::uniform3fv(U32 index, U32 count, const GLfloat* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec(v[0], v[1], v[2], 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - glUniform3fv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + mUniformCache.forget(location); + glUniform3fv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::Vec3, + uniform_bits(v[0]), uniform_bits(v[1]), uniform_bits(v[2]))) + { + glUniform3fv(location, 1, v); } } } @@ -1922,20 +1956,27 @@ void LLGLSLShader::uniform4fv(U32 index, U32 count, const GLfloat* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 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) - { - LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; - glUniform4fv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; + mUniformCache.forget(location); + glUniform4fv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::Vec4, + uniform_bits(v[0]), uniform_bits(v[1]), uniform_bits(v[2]), uniform_bits(v[3]))) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; + glUniform4fv(location, 1, v); } } } +// Uploads without comparing, and records what it uploaded; see fastUniform1f. void LLGLSLShader::fastUniform4fv(U32 index, U32 count, const GLfloat* v) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; @@ -1943,7 +1984,17 @@ void LLGLSLShader::fastUniform4fv(U32 index, U32 count, const GLfloat* v) llassert(mProgramObject); llassert(index < mUniform.size()); llassert(mUniform[index] >= 0); - glUniform4fv(mUniform[index], count, v); + const GLint location = mUniform[index]; + glUniform4fv(location, count, v); + if (count == 1) + { + mUniformCache.uploaded(location, UniformCache::Kind::Vec4, + uniform_bits(v[0]), uniform_bits(v[1]), uniform_bits(v[2]), uniform_bits(v[3])); + } + else + { + mUniformCache.forget(location); + } } void LLGLSLShader::uniform4uiv(U32 index, U32 count, const GLuint* v) @@ -1960,16 +2011,22 @@ void LLGLSLShader::uniform4uiv(U32 index, U32 count, const GLuint* v) return; } - if (mUniform[index] >= 0) + const GLint location = mUniform[index]; + if (location < 0) { - const auto& iter = mValue.find(mUniform[index]); - LLVector4 vec((F32)v[0], (F32)v[1], (F32)v[2], (F32)v[3]); - if (iter == mValue.end() || shouldChange(iter->second, vec) || count != 1) - { - LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; - glUniform4uiv(mUniform[index], count, v); - mValue[mUniform[index]] = vec; - } + return; + } + if (count != 1) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; + mUniformCache.forget(location); + glUniform4uiv(location, count, v); + } + else if (mUniformCache.changed(location, UniformCache::Kind::UVec4, + uniform_bits(v[0]), uniform_bits(v[1]), uniform_bits(v[2]), uniform_bits(v[3]))) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; + glUniform4uiv(location, 1, v); } } } @@ -2138,15 +2195,11 @@ void LLGLSLShader::uniform1i(const LLStaticHashedString& uniform, GLint v) LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER; GLint location = getUniformLocation(uniform); - if (location >= 0) + // The slot a location has is shared with the reserved-index setters, so a name and an index for one uniform + // never disagree about what it holds. + if (location >= 0 && mUniformCache.changed(location, UniformCache::Kind::Int, uniform_bits(v))) { - const auto& iter = mValue.find(location); - LLVector4 vec((F32)v, 0.f, 0.f, 0.f); - if (iter == mValue.end() || shouldChange(iter->second, vec)) - { - glUniform1i(location, v); - mValue[location] = vec; - } + glUniform1i(location, v); } } void LLGLSLShader::vertexAttrib4f(U32 index, GLfloat x, GLfloat y, GLfloat z, GLfloat w) diff --git a/indra/llrender/llglslshader.h b/indra/llrender/llglslshader.h index 58f57749e40..f1f39b55e3e 100644 --- a/indra/llrender/llglslshader.h +++ b/indra/llrender/llglslshader.h @@ -31,7 +31,6 @@ #include "llrender.h" #include "llsd.h" #include "llstaticstringtable.h" -#include class LLShaderFeatures { @@ -455,16 +454,15 @@ class LLGLSLShader U32 mAttributeMask; //mask of which reserved attributes are set (lines up with LLVertexBuffer::getTypeMask()) std::vector mUniform; //lookup table of uniform enum to uniform location LLStaticStringTable mUniformMap; //lookup map of uniform name to uniform location - typedef boost::unordered_map uniform_value_map_t; - uniform_value_map_t mValue; //lookup map of uniform location to last known value std::vector mTexture; // Last value routed to MINIMUM_ALPHA on this program, so setMinimumAlpha can skip the // batch flush and the whole uniform path when unchanged (the alpha pool re-applies it // per batch; GLTF material binds re-push it per material). EVERY writer of that // uniform must go through setMinimumAlpha to keep this authoritative -- do not write // it via uniform1f/fastUniform1f directly. -1 is a live value (GLTF "no masking"), - // so the unset sentinel sits far outside the usable range; reset alongside mValue - // (mapUniforms/unloadInternal), since both shadow GPU-side program state. + // so the unset sentinel sits far outside the usable range; reset alongside the + // uniform value cache (link/mapUniforms/unloadInternal), since both shadow GPU-side + // program state. static constexpr F32 MINIMUM_ALPHA_UNSET = -1e30f; F32 mMinimumAlpha = MINIMUM_ALPHA_UNSET; @@ -597,6 +595,113 @@ class LLGLSLShader private: void unloadInternal(); + // What this program holds at each uniform location, as the setters last uploaded it, so a set of the value a + // location already holds skips its glUniform* call. Every setter but the matrices' comes through here, on the + // path of every draw. + // + // A slot holds the bit patterns of the components uploaded and the kind of upload that wrote them: the GL + // component type and count. A set matches only when both agree exactly, so ints and uints are compared as + // themselves and floats by their bits -- +0 against -0, or two NaN payloads, cost an upload and never skip one. + // A location written through one kind and then set through another uploads: what GL holds there was converted + // from the first kind's bytes. + // + // Indexed by location. Implementations number a program's uniforms from 0 at link, an array taking a location + // an element, so a flat array sized past the largest location a setter can reach -- every active uniform's, + // found by name in mapUniforms -- costs one slot a location and no hashing, and the reserved-index setters and + // the named one share the slot each location has. A location at or past MAX_LOCATIONS uploads every time. + // + // Arrays (count != 1) upload every time and forget their location's slot, which holds element 0 as a single + // set wrote it. Matrices neither consult nor touch the cache: GL takes only the matrix setters at a matrix's + // location, so no kind a slot describes can be held there. + // + // Empty until mapUniforms sizes it, and emptied by every link and unload: a link resets each uniform to its + // initializer, and a value cached for an older link would skip a set the new one needs. + class UniformCache + { + public: + // The uploads a slot can describe: one value of each type a setter writes. + enum class Kind : U32 + { + Empty, // holds nothing, so matches no set + Int, // glUniform1i, glUniform1iv with count 1 + IVec4, // glUniform4iv with count 1 + UInt, // glUniform1ui + UVec4, // glUniform4uiv with count 1 + Float, // glUniform1f, glUniform1fv with count 1 + Vec2, // glUniform2f, glUniform2fv with count 1 + Vec3, // glUniform3f, glUniform3fv with count 1 + Vec4, // glUniform4f, glUniform4fv with count 1 + }; + + // GL 4.3's minimum GL_MAX_UNIFORM_LOCATIONS. A program's own locations stay well below it; a driver that + // numbered them sparsely would cost uploads past it, not memory. + static constexpr size_t MAX_LOCATIONS = 1024; + + // Cache locations 0 to `locations` - 1, every slot empty. + void reset(size_t locations) + { + mSlots.assign(llmin(locations, MAX_LOCATIONS), Slot()); + mCount = (U32)mSlots.size(); + } + + // Cache nothing: every set uploads. + void clear() + { + mSlots.clear(); + mCount = 0; + } + + // Whether a set must reach GL: its location is not cached, or its slot holds another kind or other bits. + // Records the set as uploaded, so the caller uploads whenever this says so. Components past the kind's + // count are zero on both sides. + bool changed(GLint location, Kind kind, U32 x, U32 y = 0, U32 z = 0, U32 w = 0) + { + if ((U32)location >= mCount) + { + return true; + } + Slot& slot = mSlots[(U32)location]; + // One branch on the whole comparison rather than one a word. + const U32 diff = ((U32)slot.mKind ^ (U32)kind) + | (slot.mBits[0] ^ x) | (slot.mBits[1] ^ y) | (slot.mBits[2] ^ z) | (slot.mBits[3] ^ w); + if (diff == 0) + { + return false; + } + slot = Slot{ kind, { x, y, z, w } }; + return true; + } + + // Record a set its caller uploaded regardless. + void uploaded(GLint location, Kind kind, U32 x, U32 y = 0, U32 z = 0, U32 w = 0) + { + if ((U32)location < mCount) + { + mSlots[(U32)location] = Slot{ kind, { x, y, z, w } }; + } + } + + // Forget what a location holds, after an upload no slot describes. + void forget(GLint location) + { + if ((U32)location < mCount) + { + mSlots[(U32)location].mKind = Kind::Empty; + } + } + + private: + struct Slot + { + Kind mKind = Kind::Empty; + U32 mBits[4] = {}; // the uploaded components' bit patterns, zero past the kind's count + }; + + std::vector mSlots; + U32 mCount = 0; // mSlots.size(), kept beside it so the bounds check is one compare + }; + UniformCache mUniformCache; + // ---- compile-time variant construction (see EVariant / createShader) ---- // build one axis's corner set into the matching member; see the definition bool createVariant(EVariant axis); diff --git a/indra/llrender/tests/llglslshader_gl_test.cpp b/indra/llrender/tests/llglslshader_gl_test.cpp new file mode 100644 index 00000000000..85f44fd9a38 --- /dev/null +++ b/indra/llrender/tests/llglslshader_gl_test.cpp @@ -0,0 +1,472 @@ +/** + * @file llglslshader_gl_test.cpp + * @brief LLGLSLShader's uniform value cache on the hidden window: what a program holds, read back from GL, after + * each kind of set, across a relink, and where two setters write one location. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../llgl.h" +#include "../llglheaders.h" +#include "../llglslshader.h" +#include "../llshadermgr.h" + +#include "llheadlessgl_fixture.h" + +#include "../test/lltut.h" + +#include +#include +#include +#include +#include + +namespace tut +{ + namespace + { + const char* kVertex = + "#version 330\n" + "void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }\n"; + + // One uniform of each type a setter writes, under reserved names, all folded into the output so each stays + // active. uGrainAnimate is a bool, which takes the int setters and the float ones alike. The types are this + // program's own, as the shader manager matches a reserved uniform by name alone: sun_up_factor, an int in the + // viewer's shaders, is the ivec4 here. + const char* kFragment = + "#version 330\n" + "uniform int light_count;\n" + "uniform ivec4 sun_up_factor;\n" + "uniform uint uFrameId;\n" + "uniform uvec4 cas_param_0;\n" + "uniform float env_intensity;\n" + "uniform float minimum_alpha;\n" + "uniform vec2 screen_res;\n" + "uniform vec3 sun_dir;\n" + "uniform vec4 color;\n" + "uniform vec4 specular_color;\n" + "uniform vec4 light_diffuse[4];\n" + "uniform bool uGrainAnimate;\n" + "out vec4 frag_color;\n" + "void main()\n" + "{\n" + " vec4 sum = vec4(float(light_count)) + vec4(sun_up_factor) + vec4(float(uFrameId)) + vec4(cas_param_0);\n" + " sum += vec4(env_intensity, minimum_alpha, screen_res) + vec4(sun_dir, 0.0) + color + specular_color;\n" + " for (int i = 0; i < 4; ++i)\n" + " {\n" + " sum += light_diffuse[i];\n" + " }\n" + " frag_color = uGrainAnimate ? sum : -sum;\n" + "}\n"; + + // 2^24: past it a float cannot hold every int, and BIG + 1 rounds to BIG. + constexpr GLint BIG = 1 << 24; + + // The program the test binds goes back with mGL, and the test's shader manager hands back the shared context's. + struct llglslshader_data + { + ~llglslshader_data() + { + LLGLSLShader::unbind(); + mShader.unload(); + } + + // The test program, compiled and linked through LLGLSLShader's own path, mapped and bound. + void setUp() + { + // An error an earlier test left on the shared context is its own: each test ends asserting none of its + // calls raised one. + while (glGetError() != GL_NO_ERROR) + { + } + + const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); + const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); + ensure("shaders compile", vs && fs); + mShader.mProgramObject = glCreateProgram(); + mShader.mName = "uniform cache (test)"; + mShader.attachObject(vs); + mShader.attachObject(fs); + const bool linked = mShader.mapAttributes(); + glDeleteShader(vs); + glDeleteShader(fs); + ensure("program links", linked); + ensure("program maps", mShader.mapUniforms()); + mShader.bind(); + } + + // The same program linked by GL alone, never through LLGLSLShader::link. 0 if it does not link. + static GLuint linkRaw() + { + const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); + const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); + if (!vs || !fs) + { + if (vs) glDeleteShader(vs); + if (fs) glDeleteShader(fs); + return 0; + } + const GLuint program = glCreateProgram(); + glAttachShader(program, vs); + glAttachShader(program, fs); + glLinkProgram(program); + glDeleteShader(vs); + glDeleteShader(fs); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) + { + glDeleteProgram(program); + return 0; + } + return program; + } + + GLint location(const char* name) const + { + return glGetUniformLocation(mShader.mProgramObject, name); + } + + // What the program holds at a uniform, as GL reads it back, against what a check expects of its first + // components. Floats are compared by their bits, so a check is exact. + void ensureInts(const std::string& what, const char* name, std::initializer_list expected) const + { + std::array held = {}; + glGetUniformiv(mShader.mProgramObject, location(name), held.data()); + size_t i = 0; + for (const GLint value : expected) + { + ensure_equals(what + ", component " + std::to_string(i), held[i], value); + ++i; + } + } + + void ensureUints(const std::string& what, const char* name, std::initializer_list expected) const + { + std::array held = {}; + glGetUniformuiv(mShader.mProgramObject, location(name), held.data()); + size_t i = 0; + for (const GLuint value : expected) + { + ensure_equals(what + ", component " + std::to_string(i), held[i], value); + ++i; + } + } + + void ensureFloats(const std::string& what, const char* name, std::initializer_list expected) const + { + std::array held = {}; + glGetUniformfv(mShader.mProgramObject, location(name), held.data()); + size_t i = 0; + for (const F32 value : expected) + { + ensure_equals(what + ", component " + std::to_string(i) + " bits", + std::bit_cast(held[i]), std::bit_cast(value)); + ++i; + } + } + + ll_test::SharedGLScope mGL; + // Made before the program and gone after it: mapUniforms reads its reserved names. + ll_test::TestShaderMgr mShaders; + LLGLSLShader mShader; + }; + + typedef test_group llglslshader_t; + typedef llglslshader_t::object llglslshader_object_t; + tut::llglslshader_t tut_llglslshader("llglslshader_gl"); + } + + // Ints and uints arrive exactly past 2^24: each set below differs from the one before only in values that + // convert to the same float, and only in some of its components. + template<> template<> + void llglslshader_object_t::test<1>() + { + setUp(); + + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG); + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG + 1); + ensureInts("int", "light_count", { BIG + 1 }); + + const GLint first[4] = { BIG, -BIG, BIG, BIG }; + const GLint second[4] = { BIG, -BIG, BIG, BIG + 1 }; + const GLint third[4] = { BIG + 1, -BIG - 1, BIG, BIG + 1 }; + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, first); + ensureInts("ivec4", "sun_up_factor", { BIG, -BIG, BIG, BIG }); + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, second); + ensureInts("ivec4, w alone changed", "sun_up_factor", { BIG, -BIG, BIG, BIG + 1 }); + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, third); + ensureInts("ivec4, x and y changed", "sun_up_factor", { BIG + 1, -BIG - 1, BIG, BIG + 1 }); + + mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG); + mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG + 1); + ensureUints("uint", "uFrameId", { (GLuint)BIG + 1 }); + + // The sharpening constants are floats' bits. 1.0's and the next float up's are both 1065353216 as floats. + const GLuint one[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u }; + const GLuint next[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }; + mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, one); + mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, next); + ensureUints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // A, then B, then A again holds A, for every kind of set: each upload replaces what its location's slot holds, + // so the third set is compared with B. The pairs mix a setter's value and array forms, which share a kind. + template<> template<> + void llglslshader_object_t::test<2>() + { + setUp(); + + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 0.5f); + ensureFloats("vec4 B", "color", { 0.25f, 0.5f, 0.75f, 0.5f }); + const F32 color[4] = { 0.25f, 0.5f, 0.75f, 1.f }; + mShader.uniform4fv(LLShaderMgr::DIFFUSE_COLOR, 1, color); + ensureFloats("vec4 A again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + + mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); + const F32 sun[3] = { 0.f, 0.5f, 0.25f }; + mShader.uniform3fv(LLShaderMgr::DEFERRED_SUN_DIR, 1, sun); + ensureFloats("vec3 B", "sun_dir", { 0.f, 0.5f, 0.25f }); + mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); + ensureFloats("vec3 A again", "sun_dir", { 0.f, 0.5f, 1.f }); + + mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); + const F32 res[2] = { 640.f, 360.f }; + mShader.uniform2fv(LLShaderMgr::DEFERRED_SCREEN_RES, 1, res); + ensureFloats("vec2 B", "screen_res", { 640.f, 360.f }); + mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); + ensureFloats("vec2 A again", "screen_res", { 640.f, 480.f }); + + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); + const F32 intensity = 0.5f; + mShader.uniform1fv(LLShaderMgr::ENVIRONMENT_INTENSITY, 1, &intensity); + ensureFloats("float B", "env_intensity", { 0.5f }); + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); + ensureFloats("float A again", "env_intensity", { 0.25f }); + + const GLint ivec_a[4] = { 1, 2, 3, 4 }; + const GLint ivec_b[4] = { 1, 2, 3, 5 }; + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_b); + mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); + ensureInts("ivec4 A again", "sun_up_factor", { 1, 2, 3, 4 }); + + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); + const GLint count = 4; + mShader.uniform1iv(LLShaderMgr::MULTI_LIGHT_COUNT, 1, &count); + ensureInts("int B", "light_count", { 4 }); + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); + ensureInts("int A again", "light_count", { 3 }); + + mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); + mShader.uniform1ui(LLShaderMgr::FRAME_ID, 8u); + mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); + ensureUints("uint A again", "uFrameId", { 7u }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // A float set twice holds its value, and a new value replaces it. Raw uploads behind the cache's back show + // which sets reached GL: just after a link the cache holds nothing, not a zero, so a set of 0 uploads; and a set + // of the value last uploaded does not. + template<> template<> + void llglslshader_object_t::test<3>() + { + setUp(); + const GLint loc = location("env_intensity"); + ensure("env_intensity is active", loc >= 0); + + glUniform1f(loc, 5.f); + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.f); + ensureFloats("the first set uploads, a zero too", "env_intensity", { 0.f }); + + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); + ensureFloats("set twice", "env_intensity", { 0.5f }); + + glUniform1f(loc, 2.f); + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); + ensureFloats("the value last uploaded is not uploaded again", "env_intensity", { 2.f }); + + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.75f); + ensureFloats("a new value replaces it", "env_intensity", { 0.75f }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // LLGLSLShader::link forgets the cache: the relink resets every uniform to 0, and the value set before it, + // set again, arrives. + template<> template<> + void llglslshader_object_t::test<4>() + { + setUp(); + const GLint before = location("color"); + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); + ensureFloats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + + ensure("program relinks", mShader.link()); + if (location("color") != before) + { + skip("the relink moved the uniform, so a stale slot would not be consulted"); + } + ensureFloats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); + + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); + ensureFloats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // mapUniforms forgets the cache, and setMinimumAlpha's own: a new program, linked by GL alone and mapped, holds + // the values set again after it, though they are the ones set on the program it replaced. + template<> template<> + void llglslshader_object_t::test<5>() + { + setUp(); + const GLint before = location("color"); + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); + mShader.setMinimumAlpha(0.5f); + ensureFloats("colour on the old program", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + ensureFloats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); + + const GLuint fresh = linkRaw(); + ensure("the new program links", fresh != 0); + LLGLSLShader::unbind(); + const GLuint old = mShader.mProgramObject; + mShader.mProgramObject = fresh; + forget_program_samplers(old); + glDeleteProgram(old); + ensure("the new program maps", mShader.mapUniforms()); + mShader.bind(); + if (location("color") != before) + { + skip("the new program put the uniform elsewhere, so a stale slot would not be consulted"); + } + ensureFloats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); + + mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); + ensureFloats("colour set again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + mShader.setMinimumAlpha(0.5f); + ensureFloats("minimum alpha set again", "minimum_alpha", { 0.5f }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // A bool takes the int setters and the float ones, and holds whichever set it last, the int set after a float + // one included. INT_MIN and -0.0 share their bits: true as an int, false as a float, so only the kind a slot + // records tells the float set from the int one before it. + template<> template<> + void llglslshader_object_t::test<6>() + { + setUp(); + + mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); + ensureInts("true by int", "uGrainAnimate", { 1 }); + mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, 0.f); + ensureInts("false by float", "uGrainAnimate", { 0 }); + mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); + ensureInts("true by int again", "uGrainAnimate", { 1 }); + + // Read at run time, so nothing folds the sign of the zero away. + volatile U32 sign_bit = 0x80000000u; + const F32 negative_zero = std::bit_cast(static_cast(sign_bit)); + mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, INT_MIN); + ensureInts("true by INT_MIN", "uGrainAnimate", { 1 }); + mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, negative_zero); + ensureInts("false by -0.0, the same bits", "uGrainAnimate", { 0 }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // The reserved index and the name of one uniform share its slot: A by index, B by name, then A by index holds A. + template<> template<> + void llglslshader_object_t::test<7>() + { + setUp(); + const LLStaticHashedString light_count("light_count"); + + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); + mShader.uniform1i(light_count, 4); + ensureInts("by name", "light_count", { 4 }); + mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); + ensureInts("by index after the name", "light_count", { 3 }); + mShader.uniform1i(light_count, 4); + ensureInts("by name after the index", "light_count", { 4 }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // The fast setters upload without comparing and leave the cache describing what they uploaded: a cached set of + // the value before a fast one arrives. + template<> template<> + void llglslshader_object_t::test<8>() + { + setUp(); + + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); + mShader.fastUniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); + ensureFloats("fast float", "env_intensity", { 0.25f }); + mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); + ensureFloats("cached float after the fast one", "env_intensity", { 0.5f }); + + const F32 a[4] = { 0.25f, 0.5f, 0.75f, 1.f }; + const F32 b[4] = { 1.f, 0.75f, 0.5f, 0.25f }; + mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); + mShader.fastUniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, b); + ensureFloats("fast vec4", "specular_color", { 1.f, 0.75f, 0.5f, 0.25f }); + mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); + ensureFloats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } + + // An array set uploads every time and forgets what its location held: element 0 set alone, then the array over + // it, then element 0 alone again holds element 0's value; and an array whose element 0 is unchanged still + // reaches GL. + template<> template<> + void llglslshader_object_t::test<9>() + { + setUp(); + + const F32 single[4] = { 0.25f, 0.5f, 0.75f, 1.f }; + const F32 two[8] = { 1.f, 1.f, 1.f, 1.f, 0.5f, 0.5f, 0.5f, 0.5f }; + const F32 two_again[8] = { 1.f, 1.f, 1.f, 1.f, 0.25f, 0.25f, 0.25f, 0.25f }; + + mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); + ensureFloats("element 0 alone", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); + ensureFloats("element 0 by the array", "light_diffuse[0]", { 1.f, 1.f, 1.f, 1.f }); + ensureFloats("element 1 by the array", "light_diffuse[1]", { 0.5f, 0.5f, 0.5f, 0.5f }); + mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); + ensureFloats("element 0 alone again", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + + mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); + mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two_again); + ensureFloats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", + { 0.25f, 0.25f, 0.25f, 0.25f }); + + ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + } +} From 2c5aa4554b0ba01ad87295f7b3e206e5ed87303d Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 00:39:52 -0400 Subject: [PATCH 3/3] The water's sun glint, its glow and its ripples are WindLight's again The sun's reflection on the water was a hard, narrow line, from three faults stacked in its specular. The wave normal was pulled flat with the distance, so far water was a mirror. Its roughness was the environment's blur multiplier, around 0.04, which blurs the reflections and has nothing to do with the sun. And its F0 was the pool's legacy sun colour, scaled by magic numbers to around 110 at a low sun, with a metallic of 1: the glint was clamped flat at its ceiling wherever it showed. It is lit as WindLight's water lit it, from 2010 to the PBR viewer. The wave normal's slopes are steepened as the view grazes the surface, by up to ten times, so seen low the faces turned toward the sun fill more of each pixel and the path breaks into a field of glints. The lobe is WindLight's Phong about the mirror direction, normalized, its exponent 2 / alpha^2 - 2 from AlchemyRenderWaterGlintRoughness, whose default of 0.35 is WindLight's 128. A GGX lobe on the steepened slopes was tried: its long tail lit every face within a few tens of degrees of the sun and drew each crest across the path as a streak. Water is a dielectric: the glint is the light's colour at the Fresnel reflectance of the angle between the view and the light, worked from v.l alone, as the half vector of a view and a setting sun at the horizon would not normalize. The pool's magic numbers and the specular uniform go, and underWaterF's unread specular with them. The glint writes its glow, as WindLight's did: the lobe, by 0.1 rising to 0.3 with the light's elevation, unless shadowed, into the screen's alpha, which the legacy glow and the bloom both read. The pool adds it to the glow already there, so what glows beneath the surface keeps its own. Only the class 3 shader writes it, as class 1's placeholder writes 1, and not from below or in probe captures. What is seen through the surface is displaced as WindLight displaced it: by the steepened wave, shrinking with the square root of the distance along the surface, and less where the surface mostly reflects. It was displaced by six times the plain wave, as strongly at a grazing view as looking straight down into it. The probes' reflection stays a lookup by the wave normal's direction, as the PBR viewer has it; WindLight displaced a mirrored render of the scene on the screen, which the probes are not. Two things in that lookup change. The water settings' normal scale holds one value per wave layer, as WindLight used it; the PBR water multiplied the composite normal's x, y and z by the three values and then normalized, so equal values, the default among them, cancelled and the setting did nothing. Each layer's slopes are scaled by its own value, against the default of 2, which leaves the reflection as it was. And a wave tilted across a low view reflected the view ray downward, so the probes returned their lower faces, the ground seen from above, on water where the next wave would hide anything below the horizon. The reflection is held on the horizon, and the probes and screen space reflections take the normal that reflects the view ray there; reflections above the horizon are untouched. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llshadermgr.cpp | 2 + indra/llrender/llshadermgr.h | 2 + .../newview/app_settings/settings_alchemy.xml | 11 ++ .../class3/environment/underWaterF.glsl | 1 - .../shaders/class3/environment/waterF.glsl | 107 +++++++++--------- indra/newview/lldrawpoolwater.cpp | 38 +++---- 6 files changed, 85 insertions(+), 76 deletions(-) diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 26a3d463003..f292cb703bd 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -2151,6 +2151,8 @@ void LLShaderMgr::initAttribsAndUniforms() mReservedUniforms.push_back("selectionGlowReachMap"); mReservedUniforms.push_back("selectionGlowRowMap"); + mReservedUniforms.push_back("waterGlintRoughness"); + // The enum and this list are parallel, and an entry added or removed on one side only // shifts every later uniform index for every shader in the viewer -- silently, since a // wrong index still resolves to some other real uniform. Fatal, like the duplicate check diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 65d7c7ca2cd..ce7346a0cac 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -634,6 +634,8 @@ class LLShaderMgr SELECTION_GLOW_REACH_MAP, // "selectionGlowReachMap" the glowing ids within the glow's reach of each tile SELECTION_GLOW_ROW_MAP, // "selectionGlowRowMap" the nearest glowing texel along each texel's row + WATER_GLINT_ROUGHNESS, // "waterGlintRoughness" the roughness of the sun's reflection on each wave face + END_RESERVED_UNIFORMS } eGLSLReservedUniforms; // clang-format on diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 63cb616c64b..f48df0a9e96 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2207,6 +2207,17 @@ Value 64 + AlchemyRenderWaterGlintRoughness + + Comment + Roughness of the sun's reflection on water, which sets how far each wave face's glint spreads: low gives small, hard sparkles, high soft ones that run together into a broad path. 0.35 matches WindLight's water. 0.05 to 1. + Persist + 1 + Type + F32 + Value + 0.35 + AlchemyRenderTonemapType Comment diff --git a/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl b/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl index 390cc881228..0dc0394be62 100644 --- a/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl +++ b/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl @@ -34,7 +34,6 @@ uniform sampler2D screenTex; uniform vec4 fogCol; uniform vec3 lightDir; -uniform vec3 specular; uniform float lightExp; uniform vec2 fbScale; uniform float refScale; diff --git a/indra/newview/app_settings/shaders/class3/environment/waterF.glsl b/indra/newview/app_settings/shaders/class3/environment/waterF.glsl index 88b76d82b80..bf43a61f40e 100644 --- a/indra/newview/app_settings/shaders/class3/environment/waterF.glsl +++ b/indra/newview/app_settings/shaders/class3/environment/waterF.glsl @@ -41,17 +41,6 @@ void mirrorClip(vec3 pos); // PBR interface vec2 BRDF(float NoV, float roughness); -void calcDiffuseSpecular(vec3 baseColor, float metallic, out vec3 diffuseColor, out vec3 specularColor); - -void pbrPunctual(vec3 diffuseColor, vec3 specularColor, - float perceptualRoughness, - float metallic, - vec3 n, // normal - vec3 v, // surface point to camera - vec3 l, // surface point to light - out float nl, - out vec3 diff, - out vec3 spec); uniform sampler2D bumpMap; uniform sampler2D bumpMap2; @@ -73,12 +62,13 @@ uniform sampler2D exclusionTex; #define classic_mode 0 #endif uniform vec3 lightDir; -uniform vec3 specular; uniform float blurMultiplier; uniform float refScale; uniform float kd; uniform vec3 normScale; uniform float fresnelScale; +// The roughness of the sun's reflection on each wave face (AlchemyRenderWaterGlintRoughness). +uniform float waterGlintRoughness; uniform float fresnelOffset; // waterFar.x is the wave clamp distance and .y the edge water stretch, 0 unless the projection is infinite @@ -192,8 +182,6 @@ void main() vec3 pos = vary_position.xyz; float linear_depth = 1 / -pos.z; - float dist = length(pos.xyz); - //normalize view vector vec3 viewVec = normalize(pos.xyz); @@ -205,7 +193,6 @@ void main() generateWaveNormals(wave1, wave2, wave3); - float dmod = sqrt(dist); vec2 distort = (refCoord.xy/refCoord.z) * 0.5 + 0.5; vec3 wavef = (wave1 + wave2 * 0.4 + wave3 * 0.6) * 0.5; @@ -221,30 +208,42 @@ void main() calculateFresnelFactors(df3, df2, normalize(view.xyz), wave1, wave2, wave3, wavef); - vec3 waver = wavef*3; - vec3 up = transform_normal(vN, vT, vB, vec3(0,0,1)); - float vdu = -dot(viewVec, up)*2; - vec3 wave_ibl = wavef * normScale; - wave_ibl.z *= 2.0; + // The reflection's normal: each wave layer's slopes by its own normal scale, as the water settings name the three, + // where 2, the default, leaves them as they are; the composite's height doubled, halving its slopes. On the axes + // of the composite, equal scales cancelled in the normalize, and the setting did nothing. + vec3 wave_ibl = vec3((wave1.xy * normScale.x + wave2.xy * (0.4 * normScale.y) + wave3.xy * (0.6 * normScale.z)) * 0.25, wavef.z * 2.0); wave_ibl = transform_normal(vN, vT, vB, normalize(wave_ibl)); - vec3 norm = transform_normal(vN, vT, vB, normalize(wavef)); - - vdu = clamp(vdu, 0, 1); - //wavef.z *= max(vdu*vdu*vdu, 0.1); + // Never below the horizon. A wave tilted across a low view reflects the ray down, into the probes' lower faces: + // the ground seen from above, where the next wave would hide it. Such a reflection is held on the horizon, and + // its normal becomes the one that reflects the view ray there, which the probes and screen space reflections take. + float ref_below = dot(reflect(viewVec, wave_ibl), up); + if (ref_below < 0.0) + { + vec3 ref_horizon = reflect(viewVec, wave_ibl) - up * ref_below; + vec3 ref_half = ref_horizon * inversesqrt(max(dot(ref_horizon, ref_horizon), 1e-12)) - viewVec; + wave_ibl = dot(ref_half, ref_half) > 1e-12 ? normalize(ref_half) : up; + } - wavef = normalize(wavef); + vec3 norm = transform_normal(vN, vT, vB, normalize(wavef)); - //wavef = vec3(0, 0, 1); - wavef = transform_normal(vN, vT, vB, wavef); + // The wave normal, its slopes steepened as the view grazes the surface, by one over the sine of the angle the view + // looks down at, up to ten times, as WindLight's water had it (wavef.z *= max(-viewVec.z, 0.1)). Seen low, the + // waves' faces turned toward the viewer fill more of each pixel: the sun's path breaks into a wide field of glints, + // and what is seen through the surface ripples harder. + vec3 steep_wave = wavef; + steep_wave.z *= max(-normalize(view.xyz).z, 0.1); + steep_wave = normalize(steep_wave); + vec3 sun_norm = transform_normal(vN, vT, vB, steep_wave); - float dist2 = dist; - dist = max(dist, 5.0); + // WindLight's ripples are offsets on the screen, which shrink with the distance along the surface, 5 m at least. + float flat_dist = max(length(view.xy), 5.0); + float flat_dmod = sqrt(flat_dist); - //figure out distortion vector (ripply) - vec2 distort2 = distort + waver.xy * refScale / max(dmod, 1.0) * 2; + // What is seen through the surface, displaced by the steepened wave, and less where the surface mostly reflects. + vec2 distort2 = distort + steep_wave.xy * refScale / max(flat_dmod * df2.x, 1.0); distort2 = clamp(distort2, vec2(0), vec2(0.999)); @@ -289,7 +288,7 @@ void main() discard; #endif - float metallic = 1.0; + // The environment's blur, for the reflections and refractions alone. float perceptualRoughness = blurMultiplier; float gloss = 1 - perceptualRoughness; @@ -304,11 +303,6 @@ void main() sampleReflectionProbes(irradiance, radiance, distort2, pos.xyz, wave_ibl.xyz, gloss, false, amblit); #endif - vec3 diffuseColor = vec3(0); - vec3 specularColor = vec3(0); - vec3 specular_linear = srgb_to_linear(specular); - calcDiffuseSpecular(specular_linear, metallic, diffuseColor, specularColor); - vec3 v = -normalize(pos.xyz); vec3 colorEmissive = vec3(0); @@ -317,18 +311,27 @@ void main() float NdotV = clamp(abs(dot(norm, v)), 0.001, 1.0); - float nl = 0; - vec3 diffPunc = vec3(0); - vec3 specPunc = vec3(0); - - pbrPunctual(diffuseColor, specularColor, perceptualRoughness, metallic, normalize(wavef+up*max(dist, 32.0)/32.0*(1.0-vdu)), v, normalize(light_dir), nl, diffPunc, specPunc); + // The sun on the steepened wave normal, as WindLight's water lit it: a Phong lobe about the mirror direction, + // normalized, its exponent 2 / alpha^2 - 2 from the roughness (0.35 is WindLight's 128). Its tail falls off fast + // enough that only the faces turned toward the sun glint and the faces between them stay dark; a microfacet lobe's + // long tail lit every face within a few tens of degrees, and on the steepened slopes drew each crest across the path + // as a streak. Water is a dielectric with no diffuse surface of its own: the glint is the light's own colour at the + // Fresnel reflectance of the angle between the view and the light, 2% looking down at it and most of it at grazing. + vec3 l = normalize(light_dir); + float glint_alpha = waterGlintRoughness * waterGlintRoughness; + float glint_exp = max(2.0 / (glint_alpha * glint_alpha) - 2.0, 1.0); + float lobe = pow(clamp(dot(reflect(-v, sun_norm), l), 0.0, 1.0), glint_exp); + float glint = lobe * (glint_exp + 2.0) / 6.2831853; + // The half vector's cosine with the view, sqrt((1 + v.l) / 2), which needs no half vector: v and l are opposite at + // the horizon under a setting sun, and theirs would not normalize. + float vh = sqrt(max(0.5 + 0.5 * dot(v, l), 0.0)); + float fresnel = 0.02 + 0.98 * pow(1.0 - vh, 5.0); + vec3 punctual = min(glint * fresnel, 10.0) * sunlit_linear * shadow * atten; + + // The glint's glow, as WindLight's water wrote it: the lobe at its peak of 1, by 0.1 rising to 0.3 as the light + // climbs, which the glow and bloom spread into a soft halo about the sparkles. Shadowed glints have none. + float glint_glow = lobe * (0.1 + 0.2 * max(lightDir.z, 0.0)) * shadow; - // The sun's specular divides by NdotV, which reaches zero on the far water at the horizon, so it fades out there. - if (waterFar.y > 0.0) - { - specPunc *= smoothstep(0.0, 0.02, dot(up, v)); - } - vec3 punctual = clamp(nl * (diffPunc + specPunc), vec3(0), vec3(10)) * sunlit_linear * shadow * atten; radiance *= df2.y; vec3 color = vec3(0); color = mix(fb.rgb, radiance, min(1, df2.x)) + punctual.rgb; @@ -346,6 +349,7 @@ void main() fade *= 60; fade = min(1, fade); color = mix(fb.rgb, color, fade); + glint_glow *= fade; // Over the last 15% of the edge water the sky behind it shows through; the pass is unblended, so the water // fades to the probes' sky along the view ray, which is what the missing water would have revealed. @@ -361,12 +365,13 @@ void main() vec3 sky = vec3(0); sampleReflectionProbesWater(sky_amb, sky, distort, pos.xyz, up, 1.0, amblit); color = mix(color, sky, gone); + glint_glow *= 1.0 - gone; } } // Not capped at 1: the glint and bright reflections are HDR and have to reach white and bloom, and - // what shows through the surface must match the shore beside it. The alpha is not written (the pool - // masks it), so the glow of what lies beneath survives. - frag_color = vec4(max(color.rgb, vec3(0)), 0.0); + // what shows through the surface must match the shore beside it. The alpha is the glint's glow, which + // the pool adds to the glow of what lies beneath. + frag_color = vec4(max(color.rgb, vec3(0)), glint_glow); } diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp index 75b3bcf080a..d23b433beab 100644 --- a/indra/newview/lldrawpoolwater.cpp +++ b/indra/newview/lldrawpoolwater.cpp @@ -128,43 +128,29 @@ void LLDrawPoolWater::beginPostDeferredPass(S32 pass) void LLDrawPoolWater::renderPostDeferred(S32 pass) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL; - LLGLDisable blend(GL_BLEND); - - // Leave the alpha alone, as WindLight's deferred water did: it is the glow channel, and the - // water's zero there wiped the glow of whatever lies beneath the surface. - gGL.setColorMask(true, false); - - LLColor3 light_diffuse(0, 0, 0); LLEnvironment& environment = LLEnvironment::instance(); LLSettingsWater::ptr_t pwater = environment.getCurrentWater(); LLSettingsSky::ptr_t psky = environment.getCurrentSky(); LLVector3 light_dir = environment.getLightDirection(); bool sun_up = environment.getIsSunUp(); - bool moon_up = environment.getIsMoonUp(); bool has_normal_mips = gSavedSettings.getBOOL("RenderWaterMipNormal"); bool underwater = LLViewerCamera::getInstance()->cameraUnderWater(); LLColor4 fog_color = LLColor4(pwater->getWaterFogColor(), 0.f); LLColor3 fog_color_linear = linearColor3(fog_color); - if (sun_up) - { - light_diffuse += psky->getSunlightColor(); - } - // moonlight is several orders of magnitude less bright than sunlight, - // so only use this color when the moon alone is showing - else if (moon_up) + // The alpha is the glow channel. The surface adds its sun glint's glow to it, as WindLight's water did, over the + // glow of whatever lies beneath, which it would otherwise wipe; seen from below it leaves the alpha alone. Only + // the class 3 shader writes a glow: class 1's is a placeholder that writes 1. Captures take no glow. + const bool glint_glow = !underwater && mShaderLevel > 2 && !gCubeSnapshot; + LLGLState blend(GL_BLEND, glint_glow ? GL_TRUE : GL_FALSE); + if (glint_glow) { - light_diffuse += psky->getMoonlightColor(); + gGL.blendFunc(LLRender::BF_ONE, LLRender::BF_ZERO, LLRender::BF_ONE, LLRender::BF_ONE); } + gGL.setColorMask(true, glint_glow); - // Apply magic numbers translating light direction into intensities light_dir.normalize(); - F32 ground_proj_sq = light_dir.mV[0] * light_dir.mV[0] + light_dir.mV[1] * light_dir.mV[1]; - if (0.f < light_diffuse.normalize()) // Normalizing a color? Puzzling... - { - light_diffuse *= (1.5f + (6.f * ground_proj_sq)); - } // Named at the bind rather than written onto the normal maps. // @@ -175,7 +161,6 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) const ALSampler water_normal_sampler = has_normal_mips ? ALSamplers::AnisoWrap : ALSamplers::PointWrap; - LLColor4 specular(sun_up ? psky->getSunlightColor() : psky->getMoonlightColor()); LLGLSLShader *shader = nullptr; // Each wave layer's scroll, the wave direction times the time times the layer's rate, taken @@ -249,7 +234,6 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) shader->uniform1f(LLShaderMgr::WATER_WATERHEIGHT, camera_height - water_height); shader->uniform3fv(LLShaderMgr::WATER_EYEVEC, 1, LLViewerCamera::getInstance()->getOrigin().mV); - shader->uniform3fv(LLShaderMgr::WATER_SPECULAR, 1, light_diffuse.mV); shader->uniform2fv(LLShaderMgr::WATER_BIG_WAVE_SCROLL, 1, big_wave_scroll.mV); shader->uniform4fv(LLShaderMgr::WATER_LITTLE_WAVE_SCROLL, 1, little_wave_scroll.mV); @@ -257,6 +241,8 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) shader->uniform3fv(LLShaderMgr::WATER_LIGHT_DIR, 1, light_dir.mV); shader->uniform3fv(LLShaderMgr::WATER_NORM_SCALE, 1, pwater->getNormalScale().mV); + static LLCachedControl glint_roughness(gSavedSettings, "AlchemyRenderWaterGlintRoughness", 0.35f); + shader->uniform1f(LLShaderMgr::WATER_GLINT_ROUGHNESS, llclamp((F32)glint_roughness, 0.05f, 1.f)); shader->uniform1f(LLShaderMgr::WATER_FRESNEL_SCALE, pwater->getFresnelScale()); shader->uniform1f(LLShaderMgr::WATER_FRESNEL_OFFSET, pwater->getFresnelOffset()); shader->uniform1f(LLShaderMgr::WATER_BLUR_MULTIPLIER, fmaxf(0, pwater->getBlurMultiplier()) * 2); @@ -319,6 +305,10 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) // clean up gPipeline.unbindDeferredShader(*shader); + if (glint_glow) + { + gGL.setSceneBlendType(LLRender::BT_ALPHA); + } gGL.setColorMask(true, false); }