diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index e48ef4564b..303b019ca8 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 f1bf366e05..61d3bdeca7 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 58f57749e4..f1f39b55e3 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/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 26a3d46300..f292cb703b 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 65d7c7ca2c..ce7346a0ca 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/llrender/tests/llglslshader_gl_test.cpp b/indra/llrender/tests/llglslshader_gl_test.cpp new file mode 100644 index 0000000000..85f44fd9a3 --- /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)); + } +} diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 63cb616c64..f48df0a9e9 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/class1/deferred/shadowUtil.glsl b/indra/newview/app_settings/shaders/class1/deferred/shadowUtil.glsl index def92dd470..7d847fdbfc 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 { diff --git a/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl b/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl index 390cc88122..0dc0394be6 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 88b76d82b8..bf43a61f40 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 75b3bcf080..d23b433bea 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); }