From e88380e464d7a0275556f99e38bd0f67358bcd4e Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 21 May 2026 22:50:34 +0300 Subject: #5858 Crash at LLFontFreetype::renderGlyph --- indra/llrender/llfontfreetype.cpp | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index 075c496ec5..3c5682dfc9 100644 --- a/indra/llrender/llfontfreetype.cpp +++ b/indra/llrender/llfontfreetype.cpp @@ -720,7 +720,50 @@ void LLFontFreetype::renderGlyph(EFontGlyphType bitmap_type, U32 glyph_index, ll llassert_always_msg(FT_Err_Ok == error, message.c_str()); } - llassert_always(! FT_Render_Glyph(mFTFace->glyph, gFontRenderMode) ); + // TODO: Make this more sturdy, make asserts/ll_errs conditional + // to non-critical characters. + // Temporarily leaving them for data gathering, but unicode chars + // like emojis should not cause the app to crash and should either + // fallback to some predetermined bitmap or simply return. + + // Verify glyph slot is valid + if (!mFTFace->glyph) + { + LL_ERRS() << "FT_Load_Glyph succeeded but glyph slot is null for wchar " << llformat("U+%xu", U32(wch)) << LL_ENDL; + return; + } + + // Check if bitmap buffer is already allocated + // It can potentially be preallocated for: + // 1. SVG/color glyphs rendered by FreeType's SVG_RendererHooks + // 2. Embedded bitmap fonts + // 3. Some Color emoji that use FT_LOAD_COLOR + if (!mFTFace->glyph->bitmap.buffer) + { + error = FT_Render_Glyph(mFTFace->glyph, gFontRenderMode); + if (error != FT_Err_Ok) + { + std::string render_message = llformat( + "Error %d (%s) rendering wchar %u glyph %u: format=%lu, pixel_mode=%d, render_mode=%d", + error, FT_Error_String(error), wch, glyph_index, + (unsigned long)mFTFace->glyph->format, mFTFace->glyph->bitmap.pixel_mode, gFontRenderMode); + + // Try with FT_RENDER_MODE_NORMAL as fallback + if (gFontRenderMode != FT_RENDER_MODE_NORMAL) + { + LL_WARNS_ONCE() << render_message << LL_ENDL; + error = FT_Render_Glyph(mFTFace->glyph, FT_RENDER_MODE_NORMAL); + if (error != FT_Err_Ok) + { + LL_ERRS() << "Fallback to FT_RENDER_MODE_NORMAL failed. " << render_message << LL_ENDL; + } + } + else + { + LL_ERRS() << render_message << LL_ENDL; + } + } + } mRenderGlyphCount++; } -- cgit v1.3 From 9f05efc7618fb5c2ea0d6cb3ac1a21ba38184987 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:31:26 +0300 Subject: #4298 Handle OOM in loadCachedProgramBinary sanity check size, catch exceptions. Cache isn't mission critical. --- indra/llrender/llshadermgr.cpp | 57 +++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index b8545b3ed9..c77a7ff8f1 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1142,34 +1142,55 @@ bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader) { std::string in_path = gDirUtilp->add(mShaderCacheDir, shader->mShaderHash.asString() + ".shaderbin"); auto& shader_info = binary_iter->second; - if (shader_info.mBinaryLength > 0) - { - std::vector in_data; - in_data.resize(shader_info.mBinaryLength); - LLUniqueFile filep = LLFile::fopen(in_path, "rb"); - if (filep) + try + { + constexpr GLsizei MAX_SHADER_BINARY_SIZE = 1024 * 1024; // 1 MB, normally around 10KB + if (shader_info.mBinaryLength > 0 && shader_info.mBinaryLength <= MAX_SHADER_BINARY_SIZE) { - size_t result = fread(in_data.data(), sizeof(U8), in_data.size(), filep); - filep.close(); + std::vector in_data; + in_data.resize(shader_info.mBinaryLength); - if (result == in_data.size()) + LLUniqueFile filep = LLFile::fopen(in_path, "rb"); + if (filep) { - GLenum error = glGetError(); // Clear current error - glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + size_t result = fread(in_data.data(), sizeof(U8), in_data.size(), filep); + filep.close(); - error = glGetError(); - GLint success = GL_TRUE; - glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); - if (error == GL_NO_ERROR && success == GL_TRUE) + if (result == in_data.size()) { - binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); - LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; - return true; + GLenum error = glGetError(); // Clear current error + glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + + error = glGetError(); + GLint success = GL_TRUE; + glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); + if (error == GL_NO_ERROR && success == GL_TRUE) + { + binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); + LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; + return true; + } + } + else + { + LL_WARNS("ShaderMgr") << "Incomplete read of shader binary. Expected: " + << in_data.size() << ", read: " << result << LL_ENDL; } } } } + catch (const std::bad_alloc&) + { + LL_WARNS("ShaderMgr") << "Failed to allocate memory for shader binary (" + << shader_info.mBinaryLength << " bytes) for: " + << shader->mName << LL_ENDL; + } + catch (const std::exception& err) + { + LL_WARNS("ShaderMgr") << "Caught exception " << err.what() << " while loading shader binary for: " << shader->mName << LL_ENDL; + } + //an error occured, normally we would print log but in this case it means the shader needs recompiling. LL_INFOS() << "Failed to load cached binary for shader: " << shader->mName << " falling back to compilation" << LL_ENDL; LLFile::remove(in_path); -- cgit v1.3 From 14262bf5e91b801e64943470cdc62e586759d40f Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:03:34 +0300 Subject: p#388 Improve protections and loggiong around shader cache saving --- indra/llrender/llshadermgr.cpp | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index c77a7ff8f1..1a0fb1aeb9 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1087,7 +1087,14 @@ void LLShaderMgr::persistShaderCacheMetadata() return; } - LL_INFOS("ShaderMgr") << "Persisting shader cache metadata to disk" << LL_ENDL; + if (mShaderCacheDir.empty() || !LLFile::isdir(mShaderCacheDir)) + { + LL_WARNS("ShaderMgr") << "Invalid shader cache directory: " << mShaderCacheDir << LL_ENDL; + return; + } + + size_t total_entries = mShaderBinaryCache.size(); + LL_INFOS("ShaderMgr") << "Persisting shader " << (S32)total_entries << " cache metadata entries to disk" << LL_ENDL; LLSD out; // Settings and shader cache get saved at different time, thus making @@ -1098,6 +1105,8 @@ void LLShaderMgr::persistShaderCacheMetadata() out["shaders"] = LLSD::emptyMap(); LLSD &shaders = out["shaders"]; + size_t removed = 0; + static const F32 LRU_TIME = (60.f * 60.f) * 24.f * 7.f; // 14 days const F32 current_time = (F32)LLTimer::getTotalSeconds(); for (auto it = mShaderBinaryCache.begin(); it != mShaderBinaryCache.end();) @@ -1106,8 +1115,9 @@ void LLShaderMgr::persistShaderCacheMetadata() if ((shader_metadata.mLastUsedTime + LRU_TIME) < current_time) { std::string shader_path = gDirUtilp->add(mShaderCacheDir, it->first.asString() + ".shaderbin"); - LLFile::remove(shader_path); + LLFile::remove(shader_path, ENOENT); it = mShaderBinaryCache.erase(it); + removed++; } else { @@ -1121,14 +1131,32 @@ void LLShaderMgr::persistShaderCacheMetadata() } std::string meta_out_path = gDirUtilp->add(mShaderCacheDir, "shaderdata.llsd"); + if (shaders.size() == 0) + { + LL_WARNS("ShaderMgr") << "No shader cache entries to persist, removing cache metadata file" << LL_ENDL; + LLFile::remove(meta_out_path); + return; + } + llofstream outstream(meta_out_path, std::ios_base::out | std::ios_base::binary); if (!outstream.is_open()) { LL_WARNS("ShaderMgr") << "Failed to open file. Unable to save shader cache to: " << mShaderCacheDir << LL_ENDL; return; } + LLSDSerialize::toBinary(out, outstream); + if (outstream.fail()) + { + LL_WARNS("ShaderMgr") << "Failed to serialize shader cache metadata" << LL_ENDL; + outstream.close(); + LLFile::remove(meta_out_path); // Clean up partial write + return; + } outstream.close(); + + LL_INFOS("ShaderMgr") << "Persisted " << (S32)shaders.size() + << " entries. Removed " << (S32)removed << " entries." << LL_ENDL; } bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader) -- cgit v1.3 From 4e0283133e87035c16fe2341748a2d68aa1a0302 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:05:13 +0300 Subject: #5962 Optimize writeWearablesToAvatar --- indra/llappearance/llwearable.cpp | 19 ++++++++++++------- indra/llcharacter/llcharacter.cpp | 25 +++++++++++++++++++++++++ indra/llcharacter/llcharacter.h | 1 + indra/llcharacter/llvisualparam.h | 1 + indra/llrender/llvertexbuffer.cpp | 13 +++++++++++++ indra/newview/llvoavatarself.cpp | 21 +++++++++++++++++++++ indra/newview/llvoavatarself.h | 1 + 7 files changed, 74 insertions(+), 7 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llappearance/llwearable.cpp b/indra/llappearance/llwearable.cpp index 4acb0ef3d4..5758f79791 100644 --- a/indra/llappearance/llwearable.cpp +++ b/indra/llappearance/llwearable.cpp @@ -727,17 +727,22 @@ void LLWearable::writeToAvatar(LLAvatarAppearance* avatarp) if (!avatarp) return; // Pull params - for( const LLVisualParam* param = avatarp->getFirstVisualParam(); param; param = avatarp->getNextVisualParam() ) + // Iterate over wearable's params first since they should be fewer than avatar params. + // Wearables are cloned from avatar params by mType, so they should be always be a + // subset of the avatar's params and a situation where mType param doesn't exist in + // a wearable yet exist in the avatar should not happen. + for (const visual_param_index_map_t::value_type& vp_pair : mVisualParamIndexMap) { + LLVisualParam* wearable_param = vp_pair.second; + // cross-wearable parameters are not authoritative, as they are driven by a different wearable. So don't copy the values to the // avatar object if cross wearable. Cross wearable params get their values from the avatar, they shouldn't write the other way. - if( (((LLViewerVisualParam*)param)->getWearableType() == mType) && (!((LLViewerVisualParam*)param)->getCrossWearable()) ) - { - S32 param_id = param->getID(); - F32 weight = getVisualParamWeight(param_id); + if (((LLViewerVisualParam*)wearable_param)->getCrossWearable()) + continue; - avatarp->setVisualParamWeight( param_id, weight); - } + F32 weight = wearable_param->getWeight(); + // Silently fails if param was not found + avatarp->setVisualParamWeight(vp_pair.first, mType, weight); } } diff --git a/indra/llcharacter/llcharacter.cpp b/indra/llcharacter/llcharacter.cpp index 8efcd9dd29..6a66c06b62 100644 --- a/indra/llcharacter/llcharacter.cpp +++ b/indra/llcharacter/llcharacter.cpp @@ -306,6 +306,31 @@ bool LLCharacter::setVisualParamWeight(S32 index, F32 weight) return false; } +//----------------------------------------------------------------------------- +// setVisualParamWeight() +//----------------------------------------------------------------------------- +bool LLCharacter::setVisualParamWeight(S32 index, S32 type, F32 weight) +{ + visual_param_index_map_t::iterator index_iter = mVisualParamIndexMap.find(index); + if (index_iter != mVisualParamIndexMap.end()) + { + LLVisualParam* param = index_iter->second; + if (param->getWearableType() == type) + { + param->setWeight(weight); + return true; + } + else + { + // setVisualParamWeight at the moment is only used in writeToAvatar. + // The type is supposed to match since wearable is a subset of avatar by type. + llassert(false); + LL_WARNS() << "Visual param index " << index << " is not of type " << type << LL_ENDL; + } + } + return false; +} + //----------------------------------------------------------------------------- // getVisualParamWeight() //----------------------------------------------------------------------------- diff --git a/indra/llcharacter/llcharacter.h b/indra/llcharacter/llcharacter.h index 079bcd132a..689e3f7d7c 100644 --- a/indra/llcharacter/llcharacter.h +++ b/indra/llcharacter/llcharacter.h @@ -195,6 +195,7 @@ public: virtual bool setVisualParamWeight(const LLVisualParam *which_param, F32 weight); virtual bool setVisualParamWeight(const char* param_name, F32 weight); virtual bool setVisualParamWeight(S32 index, F32 weight); + virtual bool setVisualParamWeight(S32 index, S32 type, F32 weight); // get visual param weight by param or name F32 getVisualParamWeight(LLVisualParam *distortion); diff --git a/indra/llcharacter/llvisualparam.h b/indra/llcharacter/llvisualparam.h index 8d2c4a3723..0ed47be064 100644 --- a/indra/llcharacter/llvisualparam.h +++ b/indra/llcharacter/llvisualparam.h @@ -120,6 +120,7 @@ public: // Pure virtuals //virtual bool parseData( LLXmlTreeNode *node ) = 0; virtual void apply( ESex avatar_sex ) = 0; + virtual S32 getWearableType() const = 0; // Default functions virtual void setWeight(F32 weight); virtual void setAnimationTarget( F32 target_value); diff --git a/indra/llrender/llvertexbuffer.cpp b/indra/llrender/llvertexbuffer.cpp index d59ddd0fec..9b839350ae 100644 --- a/indra/llrender/llvertexbuffer.cpp +++ b/indra/llrender/llvertexbuffer.cpp @@ -939,12 +939,14 @@ void LLVertexBuffer::initClass(LLWindow* window) { llassert(sVBOPool == nullptr); +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { LL_INFOS() << "VBO Pooling Disabled" << LL_ENDL; sVBOPool = new LLAppleVBOPool(); } else +#endif { LL_INFOS() << "VBO Pooling Enabled" << LL_ENDL; sVBOPool = new LLDefaultVBOPool(); @@ -1282,7 +1284,12 @@ U8* LLVertexBuffer::mapVertexBuffer(LLVertexBuffer::AttributeType type, U32 inde count = mNumVerts - index; } +#if LL_DARWIN || LL_ARM64 + // Region tracking not needed on apple silicon - it recreates entire buffer + // While mIsApple can be encountered under windows, this is a + // macOS OpenGL behavior workaround. LL_ARM64 check might be not needed if (!gGLManager.mIsApple) +#endif { U32 start = mOffsets[type] + sTypeSize[type] * index; U32 end = start + sTypeSize[type] * count-1; @@ -1319,7 +1326,9 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count) count = mNumIndices-index; } +#if LL_DARWIN || LL_ARM64 if (!gGLManager.mIsApple) +#endif { U32 start = sizeof(U16) * index; U32 end = start + sizeof(U16) * count-1; @@ -1354,6 +1363,7 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count) // dst -- mMappedData or mMappedIndexData void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8* dst) { +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { // on OS X, flush_vbo doesn't actually write to the GL buffer, so be sure to call @@ -1365,6 +1375,7 @@ void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8 memcpy(dst+start, data, end-start+1); } else +#endif { llassert(target == GL_ARRAY_BUFFER ? sGLRenderBuffer == mGLBuffer : sGLRenderIndices == mGLIndices); @@ -1420,6 +1431,7 @@ void LLVertexBuffer::_unmapBuffer() } }; +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { STOP_GLERROR; @@ -1462,6 +1474,7 @@ void LLVertexBuffer::_unmapBuffer() STOP_GLERROR; } else +#endif // LL_DARWIN || LL_ARM64 { if (!mMappedVertexRegions.empty()) { diff --git a/indra/newview/llvoavatarself.cpp b/indra/newview/llvoavatarself.cpp index e5c14a34a5..42cec5854c 100644 --- a/indra/newview/llvoavatarself.cpp +++ b/indra/newview/llvoavatarself.cpp @@ -754,6 +754,27 @@ bool LLVOAvatarSelf::setVisualParamWeight(S32 index, F32 weight) return setParamWeight(param,weight); } +bool LLVOAvatarSelf::setVisualParamWeight(S32 index, S32 type, F32 weight) +{ + LLViewerVisualParam* param = (LLViewerVisualParam*)LLCharacter::getVisualParam(index); + if (!param) + { + return false; + } + if (param->getWearableType() == type) + { + return setParamWeight(param, weight); + } + else + { + // setVisualParamWeight at the moment is only used in writeToAvatar. + // The type is supposed to match since wearable is a subset of avatar by type. + llassert(false); + LL_WARNS() << "Visual param index " << index << " is not of type " << type << LL_ENDL; + } + return false; +} + bool LLVOAvatarSelf::setParamWeight(const LLViewerVisualParam *param, F32 weight) { if (!param) diff --git a/indra/newview/llvoavatarself.h b/indra/newview/llvoavatarself.h index 45985b2a80..ff8cbc20a1 100644 --- a/indra/newview/llvoavatarself.h +++ b/indra/newview/llvoavatarself.h @@ -97,6 +97,7 @@ public: /*virtual*/ bool setVisualParamWeight(const LLVisualParam *which_param, F32 weight); /*virtual*/ bool setVisualParamWeight(const char* param_name, F32 weight); /*virtual*/ bool setVisualParamWeight(S32 index, F32 weight); + /*virtual*/ bool setVisualParamWeight(S32 index, S32 type, F32 weight); /*virtual*/ void updateVisualParams(); void writeWearablesToAvatar(); /*virtual*/ void idleUpdateAppearanceAnimation(); -- cgit v1.3 From 97b0c99e29a60e858f7328094de7e59e5ad02278 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:08:09 +0300 Subject: #5966 Fixed width font's numbers Dejavu used fixed width numbers by default, tweaked fonts to do the same for inter (when weight > 0). --- indra/llrender/llfontfreetype.cpp | 48 ++++++++++++++++++++++++++++++++++++--- indra/llrender/llfontfreetype.h | 6 +++++ indra/llrender/llfontgl.cpp | 14 +++++++++--- 3 files changed, 62 insertions(+), 6 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index 3c5682dfc9..bc4020a06b 100644 --- a/indra/llrender/llfontfreetype.cpp +++ b/indra/llrender/llfontfreetype.cpp @@ -109,6 +109,7 @@ LLFontManager::~LLFontManager() LLFontGlyphInfo::LLFontGlyphInfo(U32 index, EFontGlyphType glyph_type) : mGlyphIndex(index), mGlyphType(glyph_type), + mChar(0), mWidth(0), // In pixels mHeight(0), // In pixels mXAdvance(0.f), // In pixels @@ -126,6 +127,7 @@ LLFontGlyphInfo::LLFontGlyphInfo(U32 index, EFontGlyphType glyph_type) LLFontGlyphInfo::LLFontGlyphInfo(const LLFontGlyphInfo& fgi) : mGlyphIndex(fgi.mGlyphIndex) , mGlyphType(fgi.mGlyphType) + , mChar(fgi.mChar) , mWidth(fgi.mWidth) , mHeight(fgi.mHeight) , mXAdvance(fgi.mXAdvance) @@ -150,7 +152,8 @@ LLFontFreetype::LLFontFreetype() mFTFace(nullptr), mRenderGlyphCount(0), mStyle(0), - mPointSize(0) + mPointSize(0), + mMaxDigitWidth(0.0f) { } @@ -343,6 +346,10 @@ F32 LLFontFreetype::getXAdvance(llwchar wch) const LLFontGlyphInfo* gi = getGlyphInfo(wch, EFontGlyphType::Unspecified); if (gi) { + if (wch >= '0' && wch <= '9' && mMaxDigitWidth > 0.0f) + { + return mMaxDigitWidth; + } return gi->mXAdvance; } else @@ -363,6 +370,12 @@ F32 LLFontFreetype::getXAdvance(const LLFontGlyphInfo* glyph) const if (mFTFace == nullptr) return 0.0; + // Use max digit width for tabular numbers + if (mWeight > 0 && glyph->mChar >= '0' && glyph->mChar <= '9' && mMaxDigitWidth > 0.0f) + { + return mMaxDigitWidth; + } + return glyph->mXAdvance; } @@ -384,8 +397,27 @@ F32 LLFontFreetype::getXKerning(const LLFontGlyphInfo* left_glyph_info, const LL if (mFTFace == nullptr) return 0.0; - U32 left_glyph = left_glyph_info ? left_glyph_info->mGlyphIndex : 0; - U32 right_glyph = right_glyph_info ? right_glyph_info->mGlyphIndex : 0; + U32 left_glyph = 0; + U32 right_glyph = 0; + + if (left_glyph_info) + { + if (mWeight > 0 && left_glyph_info->mChar >= '0' && left_glyph_info->mChar <= '9') + { + // Disable kerning for digits when using tabular numbers + return 0.0; + } + left_glyph = left_glyph_info->mGlyphIndex; + } + if (right_glyph_info) + { + if (mWeight > 0 && right_glyph_info->mChar >= '0' && right_glyph_info->mChar <= '9') + { + // Disable kerning for digits when using tabular numbers + return 0.0; + } + right_glyph = right_glyph_info->mGlyphIndex; + } FT_Vector delta; @@ -548,6 +580,7 @@ LLFontGlyphInfo* LLFontFreetype::addGlyphFromFont(const LLFontFreetype *fontp, l mFontBitmapCachep->nextOpenPos(width, pos_x, pos_y, bitmap_glyph_type, bitmap_num); LLFontGlyphInfo* gi = new LLFontGlyphInfo(glyph_index, requested_glyph_type); + gi->mChar = wch; gi->mXBitmapOffset = pos_x; gi->mYBitmapOffset = pos_y; gi->mBitmapEntry = std::make_pair(bitmap_glyph_type, bitmap_num); @@ -564,6 +597,14 @@ LLFontGlyphInfo* LLFontFreetype::addGlyphFromFont(const LLFontFreetype *fontp, l gi->mXAdvance = fontp->mFTFace->glyph->advance.x / 64.f; gi->mYAdvance = fontp->mFTFace->glyph->advance.y / 64.f; + if (mWeight > 0 && wch >= '0' && wch <= '9') + { + // Digits are supposed to be preloaded, and buffers + // refresh when new chars get added, so this lazy load + // should not cause any issues. + mMaxDigitWidth = llmax(mMaxDigitWidth, gi->mXAdvance); + } + insertGlyphInfo(wch, gi); if (requested_glyph_type != bitmap_glyph_type) @@ -799,6 +840,7 @@ void LLFontFreetype::resetBitmapCache() } mCharGlyphInfoMap.clear(); mFontBitmapCachep->reset(); + mMaxDigitWidth = 0.0f; // Adding default glyph is skipped for fallback fonts here as well as in loadFace(). // This if was added as fix for EXT-4971. diff --git a/indra/llrender/llfontfreetype.h b/indra/llrender/llfontfreetype.h index d2164e8fa2..461e064766 100644 --- a/indra/llrender/llfontfreetype.h +++ b/indra/llrender/llfontfreetype.h @@ -76,6 +76,8 @@ struct LLFontGlyphInfo U32 mGlyphIndex; EFontGlyphType mGlyphType; + llwchar mChar; + // Metrics S32 mWidth; // In pixels @@ -147,6 +149,9 @@ public: F32 getXKerning(llwchar char_left, llwchar char_right) const; // Get the kerning between the two characters F32 getXKerning(const LLFontGlyphInfo* left_glyph_info, const LLFontGlyphInfo* right_glyph_info) const; // Get the kerning between the two characters + F32 getMaxDigitWidth() const { return mMaxDigitWidth; } + S32 getFontWeight() const { return mWeight; } + LLFontGlyphInfo* getGlyphInfo(llwchar wch, EFontGlyphType glyph_type) const; void reset(F32 vert_dpi, F32 horz_dpi); @@ -184,6 +189,7 @@ private: F32 mAscender; F32 mDescender; F32 mLineHeight; + mutable F32 mMaxDigitWidth; LLFT_Face mFTFace; diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp index 5d99c35047..bb5a12375e 100644 --- a/indra/llrender/llfontgl.cpp +++ b/indra/llrender/llfontgl.cpp @@ -345,6 +345,14 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons break; } + // Calculate horizontal offset for tabular numbers (center narrow digits) + F32 x_offset = 0.0f; + if (mFontFreetype->getFontWeight() > 0 && fgi->mChar >= '0' && fgi->mChar <= '9' && mFontFreetype->getMaxDigitWidth() > 0.0f) + { + // use mXAdvance directly here, since we don't want to get max width instead. + x_offset = (mFontFreetype->getMaxDigitWidth() - fgi->mXAdvance) * 0.5f; + } + // Draw the text at the appropriate location //Specify vertices and texture coordinates LLRectf uv_rect((fgi->mXBitmapOffset) * inv_width, @@ -352,9 +360,9 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons (fgi->mXBitmapOffset + fgi->mWidth) * inv_width, (fgi->mYBitmapOffset - PAD_UVY) * inv_height); // snap glyph origin to whole screen pixel - LLRectf screen_rect((F32)ll_round(cur_render_x + (F32)fgi->mXBearing), + LLRectf screen_rect((F32)ll_round(cur_render_x + (F32)fgi->mXBearing + x_offset), (F32)ll_round(cur_render_y + (F32)fgi->mYBearing), - (F32)ll_round(cur_render_x + (F32)fgi->mXBearing) + (F32)fgi->mWidth, + (F32)ll_round(cur_render_x + (F32)fgi->mXBearing + x_offset) + (F32)fgi->mWidth, (F32)ll_round(cur_render_y + (F32)fgi->mYBearing) - (F32)fgi->mHeight); if (glyph_count >= GLYPH_BATCH_SIZE) @@ -375,7 +383,7 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons col, style_to_add, shadow, drop_shadow_strength); chars_drawn++; - cur_x += fgi->mXAdvance; + cur_x += mFontFreetype->getXAdvance(fgi); cur_y += fgi->mYAdvance; llwchar next_char = wstr[i+1]; -- cgit v1.3 From 07c58edf61c8eb1e3daaa17759206bbd4edd1af8 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:13:47 +0300 Subject: #5972 LLUIImage based buffer cache --- indra/llrender/lluiimage.cpp | 179 ++++++++++++++++++++- indra/llrender/lluiimage.h | 163 ++++++++++++++++++- indra/llrender/lluiimage.inl | 80 ++++++--- indra/newview/app_settings/settings.xml | 11 ++ indra/newview/llappviewer.cpp | 1 + indra/newview/llstatusbar.cpp | 1 + indra/newview/llviewertexturelist.cpp | 1 + indra/newview/pipeline.cpp | 12 ++ indra/newview/skins/default/xui/en/menu_viewer.xml | 10 ++ 9 files changed, 436 insertions(+), 22 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index dc18bf16bf..b1394a07bc 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,6 +31,13 @@ // Project includes #include "lluiimage.h" +#include +#include + +// Static member initialization +std::vector > LLUIImage::sImageList; +size_t LLUIImage::sCleanupIndex = 0; +bool LLUIImage::sEnableDisplayListsCollection = true; LLUIImage::LLUIImage(const std::string& name, LLPointer image) : mName(name), @@ -49,6 +56,23 @@ LLUIImage::LLUIImage(const std::string& name, LLPointer image) LLUIImage::~LLUIImage() { delete mImageLoaded; + + if (!mDisplayLists.empty()) + { + llassert(false); + // Unregister from global cleanup list (sanity check) + // But it's supposed to be cleared already, else we wouldn't + // be destructing this opbject. + auto it = std::find(sImageList.begin(), sImageList.end(), this); + if (it != sImageList.end()) + { + // Swap with last element and pop (O(1) removal) + *it = sImageList.back(); + sImageList.pop_back(); + } + } + + mDisplayLists.clear(); } S32 LLUIImage::getWidth() const @@ -63,6 +87,155 @@ S32 LLUIImage::getHeight() const return ll_round((F32)mImage->getHeight(0) * mClipRegion.getHeight()); } +buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + auto it = mDisplayLists.find(key); + if (it != mDisplayLists.end()) + { + // Found cached display list, update last used time + it->second.last_used = std::chrono::steady_clock::now(); + return &it->second.list; + } + return nullptr; +} + +buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LL_PROFILE_ZONE_SCOPED; + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + CachedDisplayList cached; + cached.last_used = std::chrono::steady_clock::now(); + + // Generate the display list by capturing the draw commands + gGL.beginList(&cached.list); + + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + + gGL.endList(); + + // Insert into cache + auto result = mDisplayLists.emplace(key, std::move(cached)); + + // Register for cleanup on first buffer creation + if (mDisplayLists.size() == 1) + { + sImageList.push_back(const_cast(this)); + } + + return &result.first->second.list; +} + +void LLUIImage::invalidateDisplayLists() +{ + mDisplayLists.clear(); + + unregisterFromGlobalCleanup(); +} + +void LLUIImage::cleanupDisplayLists() +{ + if (mDisplayLists.empty()) + { + llassert(false); //it shouldn't be in this list + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + sCleanupIndex--; + return; + } + + // Time threshold for cleaning up unused display lists (global cleanup) + constexpr std::chrono::seconds DISPLAY_LIST_TIMEOUT{ 2 }; + auto now = std::chrono::steady_clock::now(); + + // Remove display lists that haven't been used recently + for (auto it = mDisplayLists.begin(); it != mDisplayLists.end(); ) + { + if (now - it->second.last_used > DISPLAY_LIST_TIMEOUT) + { + it = mDisplayLists.erase(it); + } + else + { + ++it; + } + } + + // Unregister from cleanup list if all display lists were removed + if (mDisplayLists.empty()) + { + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + sCleanupIndex--; + } +} + +void LLUIImage::unregisterFromGlobalCleanup() +{ + auto list_it = std::find(sImageList.begin(), sImageList.end(), this); + if (list_it != sImageList.end()) + { + // Swap with last element and pop (O(1) removal) + *list_it = sImageList.back(); + sImageList.pop_back(); + } +} + +// static +void LLUIImage::updateClass() +{ + if (sImageList.empty()) + { + return; + } + + // Clean up a batch of images each frame to amortize the cost + // ensuring all images are checked regularly + // Note: buffers often get obsolete in batches, perhaps + // increase rate of cleanup after a buffer was removed? + // and decrease rate if no buffer were removed and creates + // for a while? + constexpr size_t BATCH_SIZE = 8; + size_t images_to_process = std::min(BATCH_SIZE, sImageList.size()); + + for (size_t i = 0; i < images_to_process; ++i) + { + if (sCleanupIndex >= sImageList.size()) + { + sCleanupIndex = 0; + } + + sImageList[sCleanupIndex]->cleanupDisplayLists(); + ++sCleanupIndex; + } +} + +void LLUIImage::cleanupClass() +{ + std::vector > list_copy(sImageList); + sImageList.clear(); + for (LLUIImage* image : list_copy) + { + // invalidateDisplayLists will attempt to clear sImageList + image->invalidateDisplayLists(); + } + sCleanupIndex = 0; +} + void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color) { @@ -77,7 +250,7 @@ void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, c } else { - border_scale = (F32)rect.getWidth() / border_width; + border_scale = (F32)rect.getWidth() / border_width; } } @@ -124,6 +297,8 @@ void LLUIImage::onImageLoaded() { (*mImageLoaded)(); } + + invalidateDisplayLists(); } namespace LLInitParam diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 7dde84f295..9e481b6615 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -34,6 +34,7 @@ #include "llinitparam.h" #include "lltexture.h" #include "llrender2dutils.h" +#include "llvertexbuffer.h" #include @@ -58,21 +59,28 @@ public: LL_FORCE_INLINE void setClipRegion(const LLRectf& region) { mClipRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleRegion(const LLRectf& region) { mScaleRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleStyle(EScaleStyle style) { mScaleStyle = style; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE LLPointer getImage() { return mImage; } LL_FORCE_INLINE const LLPointer& getImage() const { return mImage; } + LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(S32 x, S32 y, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(const LLRect& rect, const LLColor4& color = UI_VERTEX_COLOR) const { draw(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color); } @@ -85,6 +93,10 @@ public: LL_FORCE_INLINE void drawBorder(const LLRect& rect, const LLColor4& color, S32 border_width) const { drawBorder(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color, border_width); } LL_FORCE_INLINE void drawBorder(S32 x, S32 y, const LLColor4& color, S32 border_width) const { drawBorder(x, y, getWidth(), getHeight(), color, border_width); } + // Note: draw3D is not cached with display lists because it uses world-space rendering + // with dynamic transforms (gl_segmented_rect_3d_tex). These calls are infrequent and + // highly dynamic, making caching ineffective. The 2D UI methods benefit from caching + // because they're called many times per frame with the same dimensions. void draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color); LL_FORCE_INLINE const std::string& getName() const { return mName; } @@ -100,7 +112,145 @@ public: void onImageLoaded(); + // Global cleanup of unused display lists across all LLUIImage instances + // Should be called periodically (e.g., once per frame or when memory pressure is detected) + static void updateClass(); + static void cleanupClass(); + + static void enableDisplayListsCollection(bool enable) { sEnableDisplayListsCollection = enable; } + protected: + // Key for identifying unique display list configurations + struct DisplayListKey + { + S32 x; + S32 y; + S32 width; + S32 height; + LLColor4 color; + bool solid_color; + LLVector3 translate; + LLVector3 scale; + + // Pack for hashing + struct PackedKey + { + uint64_t position; // x and y coordinates + uint64_t color_flags; // RGBA color + solid_color flag + uint64_t dimensions; // width and height + // todo: fix, translation, scale, position, shouldn't be needed + uint64_t translate; // UI offset + uint64_t scale; // UI scale + + constexpr bool operator==(const PackedKey& other) const + { + return position == other.position && + color_flags == other.color_flags && + dimensions == other.dimensions && + translate == other.translate && + scale == other.scale; + } + }; + + constexpr PackedKey pack() const + { + // Convert floats to 8-bit values for packing (0.0-1.0 -> 0-255) + auto float_to_u8 = [](F32 f) -> uint8_t { + return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); + }; + + // Helper to convert float to uint32 preserving bit pattern + auto float_to_bits = [](F32 f) -> uint32_t { + return std::bit_cast(f); + }; + + uint8_t r = float_to_u8(color.mV[VRED]); + uint8_t g = float_to_u8(color.mV[VGREEN]); + uint8_t b = float_to_u8(color.mV[VBLUE]); + uint8_t a = float_to_u8(color.mV[VALPHA]); + + uint64_t pos = (static_cast(static_cast(x)) << 32) | + static_cast(static_cast(y)); + + uint64_t col = (static_cast(r) << 56) | + (static_cast(g) << 48) | + (static_cast(b) << 40) | + (static_cast(a) << 32) | + (solid_color ? 1ULL : 0ULL); + + uint64_t dim = (static_cast(static_cast(width)) << 32) | + static_cast(static_cast(height)); + + uint64_t trns = (static_cast(float_to_bits(translate.mV[VX])) << 32) | + static_cast(float_to_bits(translate.mV[VY])); + + uint64_t scl = (static_cast(float_to_bits(scale.mV[VX])) << 32) | + static_cast(float_to_bits(scale.mV[VY])); + + return PackedKey{ pos, col, dim, trns, scl }; + } + + constexpr bool operator==(const DisplayListKey& other) const + { + return pack() == other.pack(); + } + + struct Hash + { + using is_transparent = void; // Enable transparent lookup + + std::size_t operator()(const DisplayListKey& key) const + { + auto packed = key.pack(); + return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); + } + + std::size_t operator()(const PackedKey& packed) const + { + return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); + } + }; + + struct KeyEqual + { + using is_transparent = void; // Enable transparent lookup + + bool operator()(const DisplayListKey& lhs, const DisplayListKey& rhs) const + { + return lhs == rhs; + } + + bool operator()(const DisplayListKey& lhs, const PackedKey& rhs) const + { + return lhs.pack() == rhs; + } + + bool operator()(const PackedKey& lhs, const DisplayListKey& rhs) const + { + return lhs == rhs.pack(); + } + }; + }; + + // Cached display list for a specific configuration + struct CachedDisplayList + { + buffer_data_list_t list; + std::chrono::steady_clock::time_point last_used; + }; + + // Get a display list for the given configuration + buffer_data_list_t* findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + // Generate a new display list for the given configuration, draws immediately. + buffer_data_list_t* genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + + // Invalidate all cached display lists (called when image properties change) + void invalidateDisplayLists(); + + // Clean up old display lists for this image (called by updateClass) + void cleanupDisplayLists(); + void unregisterFromGlobalCleanup(); + image_loaded_signal_t* mImageLoaded; std::string mName; @@ -110,6 +260,17 @@ protected: EScaleStyle mScaleStyle; mutable S32 mCachedW; mutable S32 mCachedH; + + // Display list cache + // const member functions promise not to modify the object's logical state, but + // cache does not modify logical state, mutabale to permit const correctness + // (standard C++ pattern for transparent caching). + mutable std::unordered_map mDisplayLists; + + // Track all LLUIImage cache instances for global cleanup + static std::vector > sImageList; + static size_t sCleanupIndex; // Round-robin cleanup position + static bool sEnableDisplayListsCollection; }; #include "lluiimage.inl" diff --git a/indra/llrender/lluiimage.inl b/indra/llrender/lluiimage.inl index dff1fcdfcc..1915d0e7ce 100644 --- a/indra/llrender/lluiimage.inl +++ b/indra/llrender/lluiimage.inl @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,30 +29,72 @@ void LLUIImage::draw(S32 x, S32 y, const LLColor4& color) const draw(x, y, getWidth(), getHeight(), color); } +void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + if (sEnableDisplayListsCollection) + { + // Get display list for this configuration + buffer_data_list_t* display_list = findDisplayList(x, y, width, height, color, solid_color); + + if (display_list && !display_list->empty()) + { + // Deliberately empty pending verts. + // They aren't related to the iamge, so don't register them under draw + gGL.flush(); + LL_PROFILE_ZONE_SCOPED; + gGL.getTexUnit(0)->enable(LLTexUnit::TT_TEXTURE); + + //gGL.pushUIMatrix(); + + if (solid_color) + { + gSolidColorProgram.bind(); + } + + gGL.color4fv(color.mV); // for the shader + + // Replay the cached display list + for (LLVertexBufferData& buffer : *display_list) + { + buffer.draw(); + } + + if (solid_color) + { + gUIProgram.bind(); + } + //gGL.popUIMatrix(); + } + else + { + // Create, draw and capture display list. + // Basically a wrapper around gl_draw_scaled_image_with_border + // that records the output into a list. + genDisplayList(x, y, width, height, color, solid_color); + } + } + else + { + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + } +} + void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - false, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, false); } void LLUIImage::drawSolid(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - true, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, true); } void LLUIImage::drawBorder(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, S32 border_width) const diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b1e161182e..43b3fdf1b0 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -9893,6 +9893,17 @@ Value 1 + CollectUIImageVertexBuffers + + Comment + When enabled images will cache buffers and reuse them. When disabled general cahce will be used with a significant overhead for hash, but it regenerates vertices each frame so it's always up to date. + Persist + 0 + Type + Boolean + Value + 1 + ShowMyComplexityChanges Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index b0c13d9818..e2bd8798f4 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -5179,6 +5179,7 @@ void LLAppViewer::idle() static LLCachedControl downscale_method(gSavedSettings, "RenderDownScaleMethod"); gGLManager.mDownScaleMethod = downscale_method; LLImageGL::updateClass(); + LLUIImage::updateClass(); // Service the WorkQueue we use for replies from worker threads. // Use function statics for the timeslice setting so we only have to fetch diff --git a/indra/newview/llstatusbar.cpp b/indra/newview/llstatusbar.cpp index 44bada13c2..0df3c27f3e 100644 --- a/indra/newview/llstatusbar.cpp +++ b/indra/newview/llstatusbar.cpp @@ -143,6 +143,7 @@ LLStatusBar::~LLStatusBar() // virtual void LLStatusBar::draw() { + LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; refresh(); LLPanel::draw(); } diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7dd32074cf..4bee11b6f7 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1641,6 +1641,7 @@ void LLViewerTextureList::processImageNotInDatabase(LLMessageSystem *msg,void ** // guaranteed. void LLUIImageList::cleanUp() { + LLUIImage::cleanupClass(); mUIImages.clear(); mUITextureList.clear() ; } diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 4ef88f5deb..54fa7a2c09 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -614,6 +614,16 @@ void LLPipeline::init() LLFontWidthBuffer::enableBufferCollection(enable_buffers); }); } + + cntrl_ptr = gSavedSettings.getControl("CollectUIImageVertexBuffers"); + if (cntrl_ptr.notNull()) + { + cntrl_ptr->getCommitSignal()->connect([](LLControlVariable* control, const LLSD& value, const LLSD& previous) + { + bool enable_buffers = control->getValue().asBoolean(); + LLUIImage::enableDisplayListsCollection(enable_buffers); + }); + } } LLPipeline::~LLPipeline() @@ -1151,6 +1161,8 @@ void LLPipeline::refreshCachedSettings() bool enable_buffers = gSavedSettings.getBOOL("CollectFontVertexBuffers"); LLFontVertexBuffer::enableBufferCollection(enable_buffers); LLFontWidthBuffer::enableBufferCollection(enable_buffers); + enable_buffers = gSavedSettings.getBOOL("CollectUIImageVertexBuffers"); + LLUIImage::enableDisplayListsCollection(enable_buffers); } void LLPipeline::releaseGLBuffers() diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 3364b831b2..a4b9ab61df 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -3529,6 +3529,16 @@ function="World.EnvPreset" function="ToggleControl" parameter="CollectFontVertexBuffers" /> + + + + Date: Thu, 2 Jul 2026 00:22:21 +0300 Subject: #5972 LLUIImage based buffer cache #2 Simplify key a bit. --- indra/llrender/lluiimage.cpp | 53 +++++++++------- indra/llrender/lluiimage.h | 106 ++++++++++---------------------- indra/llrender/lluiimage.inl | 2 +- indra/newview/app_settings/settings.xml | 2 +- 4 files changed, 65 insertions(+), 98 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index b1394a07bc..85754209a4 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -31,7 +31,6 @@ // Project includes #include "lluiimage.h" -#include #include // Static member initialization @@ -57,21 +56,10 @@ LLUIImage::~LLUIImage() { delete mImageLoaded; - if (!mDisplayLists.empty()) - { - llassert(false); - // Unregister from global cleanup list (sanity check) - // But it's supposed to be cleared already, else we wouldn't - // be destructing this opbject. - auto it = std::find(sImageList.begin(), sImageList.end(), this); - if (it != sImageList.end()) - { - // Swap with last element and pop (O(1) removal) - *it = sImageList.back(); - sImageList.pop_back(); - } - } - + // Unregister from global cleanup list (sanity check) + // But it's supposed to be cleared already, else we wouldn't + // be destructing this object. + unregisterFromGlobalCleanup(); mDisplayLists.clear(); } @@ -91,12 +79,12 @@ buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 heig { LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); auto it = mDisplayLists.find(key); if (it != mDisplayLists.end()) { - // Found cached display list, update last used time it->second.last_used = std::chrono::steady_clock::now(); return &it->second.list; } @@ -108,7 +96,7 @@ buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 heigh LL_PROFILE_ZONE_SCOPED; LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); CachedDisplayList cached; cached.last_used = std::chrono::steady_clock::now(); @@ -129,6 +117,7 @@ buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 heigh gGL.endList(); // Insert into cache + // emplace, since we only call genDisplayList if key was not found. auto result = mDisplayLists.emplace(key, std::move(cached)); // Register for cleanup on first buffer creation @@ -154,7 +143,18 @@ void LLUIImage::cleanupDisplayLists() llassert(false); //it shouldn't be in this list unregisterFromGlobalCleanup(); // marks current position for a recheck. Increments after cleanupDisplayLists. - sCleanupIndex--; + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } return; } @@ -180,7 +180,18 @@ void LLUIImage::cleanupDisplayLists() { unregisterFromGlobalCleanup(); // marks current position for a recheck. Increments after cleanupDisplayLists. - sCleanupIndex--; + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } } } diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 9e481b6615..6f33c31bfe 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -38,7 +38,11 @@ #include +#include +#include #include +#include +#include extern const LLColor4 UI_VERTEX_COLOR; @@ -120,46 +124,42 @@ public: static void enableDisplayListsCollection(bool enable) { sEnableDisplayListsCollection = enable; } protected: - // Key for identifying unique display list configurations - struct DisplayListKey + // Packed key for identifying unique display list configurations + struct PackedKey { - S32 x; - S32 y; - S32 width; - S32 height; - LLColor4 color; - bool solid_color; - LLVector3 translate; - LLVector3 scale; - - // Pack for hashing - struct PackedKey + uint64_t position; // x and y coordinates + uint64_t color_flags; // RGBA color + solid_color flag + uint64_t dimensions; // width and height + uint64_t translate; // UI offset + uint64_t scale; // UI scale + + constexpr bool operator==(const PackedKey& other) const + { + return position == other.position && + color_flags == other.color_flags && + dimensions == other.dimensions && + translate == other.translate && + scale == other.scale; + } + + struct Hash { - uint64_t position; // x and y coordinates - uint64_t color_flags; // RGBA color + solid_color flag - uint64_t dimensions; // width and height - // todo: fix, translation, scale, position, shouldn't be needed - uint64_t translate; // UI offset - uint64_t scale; // UI scale - - constexpr bool operator==(const PackedKey& other) const + std::size_t operator()(const PackedKey& key) const { - return position == other.position && - color_flags == other.color_flags && - dimensions == other.dimensions && - translate == other.translate && - scale == other.scale; + return static_cast(key.position ^ key.color_flags ^ + key.dimensions ^ key.translate ^ key.scale); } }; - constexpr PackedKey pack() const + // Static factory function to create PackedKey from parameters + static constexpr PackedKey create(S32 x, S32 y, S32 width, S32 height, + const LLColor4& color, bool solid_color, + const LLVector3& translate, const LLVector3& scale) { - // Convert floats to 8-bit values for packing (0.0-1.0 -> 0-255) auto float_to_u8 = [](F32 f) -> uint8_t { return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); }; - // Helper to convert float to uint32 preserving bit pattern auto float_to_bits = [](F32 f) -> uint32_t { return std::bit_cast(f); }; @@ -189,47 +189,6 @@ protected: return PackedKey{ pos, col, dim, trns, scl }; } - - constexpr bool operator==(const DisplayListKey& other) const - { - return pack() == other.pack(); - } - - struct Hash - { - using is_transparent = void; // Enable transparent lookup - - std::size_t operator()(const DisplayListKey& key) const - { - auto packed = key.pack(); - return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); - } - - std::size_t operator()(const PackedKey& packed) const - { - return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); - } - }; - - struct KeyEqual - { - using is_transparent = void; // Enable transparent lookup - - bool operator()(const DisplayListKey& lhs, const DisplayListKey& rhs) const - { - return lhs == rhs; - } - - bool operator()(const DisplayListKey& lhs, const PackedKey& rhs) const - { - return lhs.pack() == rhs; - } - - bool operator()(const PackedKey& lhs, const DisplayListKey& rhs) const - { - return lhs == rhs.pack(); - } - }; }; // Cached display list for a specific configuration @@ -261,11 +220,8 @@ protected: mutable S32 mCachedW; mutable S32 mCachedH; - // Display list cache - // const member functions promise not to modify the object's logical state, but - // cache does not modify logical state, mutabale to permit const correctness - // (standard C++ pattern for transparent caching). - mutable std::unordered_map mDisplayLists; + // Display list cache - now using PackedKey directly + mutable std::unordered_map mDisplayLists; // Track all LLUIImage cache instances for global cleanup static std::vector > sImageList; diff --git a/indra/llrender/lluiimage.inl b/indra/llrender/lluiimage.inl index 1915d0e7ce..d40835a0f4 100644 --- a/indra/llrender/lluiimage.inl +++ b/indra/llrender/lluiimage.inl @@ -39,7 +39,7 @@ void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, if (display_list && !display_list->empty()) { // Deliberately empty pending verts. - // They aren't related to the iamge, so don't register them under draw + // They aren't related to the image, so don't register them under draw zone gGL.flush(); LL_PROFILE_ZONE_SCOPED; gGL.getTexUnit(0)->enable(LLTexUnit::TT_TEXTURE); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 43b3fdf1b0..75258cfa79 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -9896,7 +9896,7 @@ CollectUIImageVertexBuffers Comment - When enabled images will cache buffers and reuse them. When disabled general cahce will be used with a significant overhead for hash, but it regenerates vertices each frame so it's always up to date. + When enabled, images will cache vertex buffers and reuse them. When disabled, the general cache will be used with significant hash-lookup overhead, but vertices are regenerated each frame so they are always up to date. Persist 0 Type -- cgit v1.3 From b35e537b5140c2a1d38fe979c2c551f735a6d21e Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:56:54 +0300 Subject: #5972 Track UI texture changes --- indra/llrender/lluiimage.cpp | 25 +++++++++++++++++++++++-- indra/llrender/lluiimage.h | 25 ++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index 85754209a4..d0b6bcbf66 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -77,10 +77,17 @@ S32 LLUIImage::getHeight() const buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const { + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + return nullptr; + } + LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); + LLGLuint tex_name = gl_image->getTexName(); - auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); auto it = mDisplayLists.find(key); if (it != mDisplayLists.end()) @@ -94,9 +101,23 @@ buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 heig buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const { LL_PROFILE_ZONE_SCOPED; + + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + // Don't cache when texture hasn't been created yet + // draw just aborts in this case, so don't draw either. + return nullptr; + } + LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); + + // Get the GL texture name - this uniquely identifies the current texture state + // including discard level changes, texture recreation, etc. + LLGLuint tex_name = gl_image->getTexName(); + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); CachedDisplayList cached; cached.last_used = std::chrono::steady_clock::now(); diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 6f33c31bfe..0998ac17b4 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -127,11 +127,12 @@ protected: // Packed key for identifying unique display list configurations struct PackedKey { - uint64_t position; // x and y coordinates - uint64_t color_flags; // RGBA color + solid_color flag - uint64_t dimensions; // width and height - uint64_t translate; // UI offset - uint64_t scale; // UI scale + uint64_t position; // x and y coordinates (32 bits each) + uint64_t color_flags; // RGBA color (8 bits each) + solid_color flag (1 bit) + uint64_t dimensions; // width and height (32 bits each) + uint64_t translate; // UI translation (32 bits each for X and Y) + uint64_t scale; // UI scale (32 bits each for X and Y) + uint64_t tex_name; // OpenGL texture name (32 bits) + padding constexpr bool operator==(const PackedKey& other) const { @@ -139,7 +140,8 @@ protected: color_flags == other.color_flags && dimensions == other.dimensions && translate == other.translate && - scale == other.scale; + scale == other.scale && + tex_name == other.tex_name; } struct Hash @@ -147,14 +149,16 @@ protected: std::size_t operator()(const PackedKey& key) const { return static_cast(key.position ^ key.color_flags ^ - key.dimensions ^ key.translate ^ key.scale); + key.dimensions ^ key.translate ^ + key.scale ^ key.tex_name); } }; // Static factory function to create PackedKey from parameters static constexpr PackedKey create(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color, - const LLVector3& translate, const LLVector3& scale) + const LLVector3& translate, const LLVector3& scale, + LLGLuint texture_name) { auto float_to_u8 = [](F32 f) -> uint8_t { return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); @@ -187,7 +191,10 @@ protected: uint64_t scl = (static_cast(float_to_bits(scale.mV[VX])) << 32) | static_cast(float_to_bits(scale.mV[VY])); - return PackedKey{ pos, col, dim, trns, scl }; + // Store full 32-bit texture name in lower 32 bits (upper 32 bits unused/zero) + uint64_t tex = static_cast(texture_name); + + return PackedKey{ pos, col, dim, trns, scl, tex }; } }; -- cgit v1.3 From d1796fba2500a30bd3c71b0588efd18c1a8625bf Mon Sep 17 00:00:00 2001 From: McGroarty Date: Sun, 26 Jul 2026 18:53:19 -0700 Subject: Fix rendering on Parallels VMs with graphics set to "low" In LLRender::syncMatrices(), treat uniform location 0 as valid when uploading the inverse model-view, inverse projection, and identity matrices. Only location -1 indicates that a uniform is absent. (This is a zero-based index.) Fix Parallels GLSL compilation by: - renaming the local waterClip() wrapper to avoid an overload conflict - returning float literals (not ints) from a few functions declared to return float --- indra/llrender/llrender.cpp | 6 +++--- .../app_settings/shaders/class3/deferred/materialF.glsl | 10 ++++------ .../shaders/class3/deferred/screenSpaceReflUtil.glsl | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 57be8570af..0a9aa462d1 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -1043,7 +1043,7 @@ void LLRender::syncMatrices() shader->uniformMatrix3fv(LLShaderMgr::NORMAL_MATRIX, 1, GL_FALSE, norm_mat); } - if (shader->getUniformLocation(LLShaderMgr::INVERSE_MODELVIEW_MATRIX)) + if (shader->getUniformLocation(LLShaderMgr::INVERSE_MODELVIEW_MATRIX) >= 0) { shader->uniformMatrix4fv(LLShaderMgr::INVERSE_MODELVIEW_MATRIX, 1, GL_FALSE, glm::value_ptr(cached_inv_mdv)); } @@ -1075,14 +1075,14 @@ void LLRender::syncMatrices() // GZ: This was previously disabled seemingly due to a bug involving the deferred renderer's regular pushing and popping of mats. // We're reenabling this and cleaning up the code around that - that would've been the appropriate course initially. // Anything beyond the standard proj and inv proj mats are special cases. Please setup special uniforms accordingly in the future. - if (shader->getUniformLocation(LLShaderMgr::INVERSE_PROJECTION_MATRIX)) + if (shader->getUniformLocation(LLShaderMgr::INVERSE_PROJECTION_MATRIX) >= 0) { glm::mat4 inv_proj = glm::inverse(mat); shader->uniformMatrix4fv(LLShaderMgr::INVERSE_PROJECTION_MATRIX, 1, false, glm::value_ptr(inv_proj)); } // Used by some full screen effects - such as full screen lights, glow, etc. - if (shader->getUniformLocation(LLShaderMgr::IDENTITY_MATRIX)) + if (shader->getUniformLocation(LLShaderMgr::IDENTITY_MATRIX) >= 0) { shader->uniformMatrix4fv(LLShaderMgr::IDENTITY_MATRIX, 1, GL_FALSE, glm::value_ptr(glm::identity())); } diff --git a/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl b/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl index 5708fc319f..65616e7835 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl @@ -262,7 +262,7 @@ void alphaMask(float alpha) #endif } -void waterClip() +void applyWaterClip() { #if (DIFFUSE_ALPHA_MODE == DIFFUSE_ALPHA_MODE_BLEND) waterClip(vary_position.xyz); @@ -284,17 +284,17 @@ float getShadow(vec3 pos, vec3 norm) #if (DIFFUSE_ALPHA_MODE == DIFFUSE_ALPHA_MODE_BLEND) return sampleDirectionalShadow(pos, norm, vary_texcoord0.xy); #else - return 1; + return 1.0; #endif #else - return 1; + return 1.0; #endif } void main() { mirrorClip(vary_position); - waterClip(); + applyWaterClip(); // diffcol == diffuse map combined with vertex color vec4 diffcol = texture(diffuseMap, vary_texcoord0.xy); @@ -441,5 +441,3 @@ void main() #endif } - - diff --git a/indra/newview/app_settings/shaders/class3/deferred/screenSpaceReflUtil.glsl b/indra/newview/app_settings/shaders/class3/deferred/screenSpaceReflUtil.glsl index e8901c7ba2..c9a19f4278 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/screenSpaceReflUtil.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/screenSpaceReflUtil.glsl @@ -323,7 +323,7 @@ float tapScreenSpaceReflection(int totalSamples, vec2 tc, vec3 viewPos, vec3 n, { #ifdef TRANSPARENT_SURFACE collectedColor = vec4(1, 0, 1, 1); - return 0; + return 0.0; #endif collectedColor = vec4(0); int hits = 0; -- cgit v1.3 From f71fbab66aac13bddd069bdaa0d31156b691911d Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:35:32 +0300 Subject: #6040 Minor warnings fixes #2 --- indra/llrender/llgl.cpp | 9 +--- indra/llui/lltoolbar.cpp | 52 ++++++++++++---------- indra/llui/lltoolbar.h | 1 + indra/newview/llgltfmateriallist.cpp | 2 +- indra/newview/llimprocessing.cpp | 3 +- indra/newview/lllogininstance.cpp | 2 + indra/newview/llpanelgrouproles.cpp | 6 +++ indra/newview/llpanellandmedia.cpp | 7 --- indra/newview/llpanellogin.cpp | 2 + indra/newview/llpanelsnapshot.cpp | 13 +++++- indra/newview/llviewerassetstorage.cpp | 6 +-- indra/newview/llviewernetwork.cpp | 3 +- .../skins/default/xui/en/floater_snapshot.xml | 1 - 13 files changed, 61 insertions(+), 46 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 4584ed1d86..a577a729b0 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -1053,11 +1053,6 @@ void LLGLManager::initWGL() { LL_WARNS("RenderInit") << "No ARB WGL PBuffer extensions" << LL_ENDL; } - - if( !glh_init_extensions("WGL_ARB_render_texture") ) - { - LL_WARNS("RenderInit") << "No ARB WGL render texture extensions" << LL_ENDL; - } #endif } @@ -1201,7 +1196,7 @@ bool LLGLManager::initGL() } if (mVRAM != 0) { - LL_WARNS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL; + LL_INFOS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL; } } else if (mHasNVXGpuMemoryInfo) @@ -1212,7 +1207,7 @@ bool LLGLManager::initGL() if (mVRAM != 0) { - LL_WARNS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL; + LL_INFOS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL; } } #endif diff --git a/indra/llui/lltoolbar.cpp b/indra/llui/lltoolbar.cpp index 785dc85448..2a9bf2585d 100644 --- a/indra/llui/lltoolbar.cpp +++ b/indra/llui/lltoolbar.cpp @@ -792,6 +792,11 @@ void LLToolBar::updateLayoutAsNeeded() mNeedsLayout = false; } +bool LLToolBar::postBuild() +{ + mCaretIcon = getChild("caret"); + return LLUICtrl::postBuild(); +} void LLToolBar::draw() { @@ -835,35 +840,36 @@ void LLToolBar::draw() LLUI::translate((F32)getRect().mLeft, (F32)getRect().mBottom); // Position the caret - if (!mCaretIcon) - { - mCaretIcon = getChild("caret"); - } - - LLIconCtrl* caret = mCaretIcon; - caret->setVisible(false); - if (mDragAndDropTarget && !mButtonCommands.empty()) + // Todo: This shouldn't be on draw, but, as example, on hover + if (mCaretIcon) { - LLRect caret_rect = caret->getRect(); - if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) - { - caret->setRect(LLRect(mDragx-caret_rect.getWidth()/2+1, - mDragy, - mDragx+caret_rect.getWidth()/2+1, - mDragy-mDragGirth)); - } - else + mCaretIcon->setVisible(false); + if (mDragAndDropTarget && !mButtonCommands.empty()) { - caret->setRect(LLRect(mDragx, - mDragy+caret_rect.getHeight()/2, - mDragx+mDragGirth, - mDragy-caret_rect.getHeight()/2)); + LLRect caret_rect = mCaretIcon->getRect(); + if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) + { + mCaretIcon->setRect(LLRect(mDragx - caret_rect.getWidth() / 2 + 1, + mDragy, + mDragx + caret_rect.getWidth() / 2 + 1, + mDragy - mDragGirth)); + } + else + { + mCaretIcon->setRect(LLRect(mDragx, + mDragy + caret_rect.getHeight() / 2, + mDragx + mDragGirth, + mDragy - caret_rect.getHeight() / 2)); + } + mCaretIcon->setVisible(true); } - caret->setVisible(true); } LLUICtrl::draw(); - caret->setVisible(false); + if (mCaretIcon) + { + mCaretIcon->setVisible(false); + } mDragAndDropTarget = false; } diff --git a/indra/llui/lltoolbar.h b/indra/llui/lltoolbar.h index abf44f259a..b4fb3fab02 100644 --- a/indra/llui/lltoolbar.h +++ b/indra/llui/lltoolbar.h @@ -213,6 +213,7 @@ public: }; // virtuals + bool postBuild(); void draw(); void reshape(S32 width, S32 height, bool called_from_parent = true); bool handleRightMouseDown(S32 x, S32 y, MASK mask); diff --git a/indra/newview/llgltfmateriallist.cpp b/indra/newview/llgltfmateriallist.cpp index b784419780..4036544343 100644 --- a/indra/newview/llgltfmateriallist.cpp +++ b/indra/newview/llgltfmateriallist.cpp @@ -525,7 +525,7 @@ void LLGLTFMaterialList::onAssetLoadComplete(const LLUUID& id, LLAssetType::ETyp if (status != LL_ERR_NOERR) { - LL_WARNS("GLTF") << "Error getting material asset data: " << LLAssetStorage::getErrorString(status) << " (" << status << ")" << LL_ENDL; + LL_WARNS("GLTF") << "Error getting material asset data: " << LLAssetStorage::getErrorString(status) << " (" << status << ") for asset " << id << LL_ENDL; asset_data->mMaterial->materialComplete(false); delete asset_data; } diff --git a/indra/newview/llimprocessing.cpp b/indra/newview/llimprocessing.cpp index 3f60dc5d26..9ba939d35b 100644 --- a/indra/newview/llimprocessing.cpp +++ b/indra/newview/llimprocessing.cpp @@ -1582,7 +1582,8 @@ void LLIMProcessing::requestOfflineMessagesCoro(std::string url) if (!contents.size()) { - LL_WARNS("Messaging") << "No contents received for offline messages via capability " << url << LL_ENDL; + // Received no offline messages on login. + LL_INFOS("Messaging") << "No contents received for offline messages via capability " << url << LL_ENDL; return; } diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp index 02f013ad3c..2cdaf40be8 100644 --- a/indra/newview/lllogininstance.cpp +++ b/indra/newview/lllogininstance.cpp @@ -96,6 +96,8 @@ LLLoginInstance::LLLoginInstance() : mDispatcher.add("connect", "", boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1)); mDispatcher.add("disconnect", "", boost::bind(&LLLoginInstance::handleDisconnect, this, _1)); mDispatcher.add("indeterminate", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1)); + // Todo, implement "authenticating"? + mDispatcher.add("authenticating", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1)); } void LLLoginInstance::setPlatformInfo(const std::string platform, diff --git a/indra/newview/llpanelgrouproles.cpp b/indra/newview/llpanelgrouproles.cpp index 426a89fe6c..b4adbe0819 100644 --- a/indra/newview/llpanelgrouproles.cpp +++ b/indra/newview/llpanelgrouproles.cpp @@ -3233,6 +3233,12 @@ void LLPanelGroupBanListSubTab::setBanCount(U32 ban_count) void LLPanelGroupBanListSubTab::populateBanList() { + if (mGroupID.isNull()) + { + mBanList->deleteAllItems(); + return; + } + LLGroupMgrGroupData* gdatap = LLGroupMgr::getInstance()->getGroupData(mGroupID); if(!gdatap) { diff --git a/indra/newview/llpanellandmedia.cpp b/indra/newview/llpanellandmedia.cpp index 294bd4021d..feea97c4a9 100644 --- a/indra/newview/llpanellandmedia.cpp +++ b/indra/newview/llpanellandmedia.cpp @@ -135,8 +135,6 @@ void LLPanelLandMedia::refresh() mMediaURLEdit->setText(parcel->getMediaURL()); mMediaURLEdit->setEnabled( false ); - getChild("current_url")->setValue(parcel->getMediaCurrentURL()); - mMediaDescEdit->setText(parcel->getMediaDesc()); mMediaDescEdit->setEnabled( can_change_media ); @@ -234,12 +232,9 @@ void LLPanelLandMedia::setMediaURL(const std::string& media_url) LLParcel *parcel = mParcel->getParcel(); if(parcel) parcel->setMediaCurrentURL(media_url); - // LLViewerMedia::navigateHome(); mMediaURLEdit->onCommit(); - // LLViewerParcelMedia::sendMediaNavigateMessage(media_url); - getChild("current_url")->setValue(media_url); } std::string LLPanelLandMedia::getMediaURL() { @@ -322,8 +317,6 @@ void LLPanelLandMedia::onResetBtn(void *userdata) LLParcel* parcel = self->mParcel->getParcel(); // LLViewerMedia::navigateHome(); self->refresh(); - self->getChild("current_url")->setValue(parcel->getMediaURL()); - // LLViewerParcelMedia::sendMediaNavigateMessage(parcel->getMediaURL()); } diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp index 15cc8b421c..65520013aa 100644 --- a/indra/newview/llpanellogin.cpp +++ b/indra/newview/llpanellogin.cpp @@ -807,6 +807,8 @@ void LLPanelLogin::onUpdateStartSLURL(const LLSLURL& new_start_slurl) case LLSLURL::HOME_LOCATION: //location_combo->setCurrentByIndex(0); // home location break; + case LLSLURL::LAST_LOCATION: + break; default: LL_WARNS("AppInit")<<"invalid login slurl, using home"<("save_btn")->setLabelArg("[UPLOAD_COST]", std::to_string(LLAgentBenefitsMgr::current().getTextureUploadCost(w, h))); + LLUICtrl *save_btn = findChild("save_btn"); + if (save_btn) + { + // Not all snapshot floaters have a save button + save_btn->setLabelArg("[UPLOAD_COST]", std::to_string(LLAgentBenefitsMgr::current().getTextureUploadCost(w, h))); + } getChild(getImageSizeComboName())->setCommitCallback(boost::bind(&LLPanelSnapshot::onResolutionComboCommit, this, _1)); if (!getWidthSpinnerName().empty()) { @@ -193,7 +198,11 @@ void LLPanelSnapshot::updateImageQualityLevel() quality_lvl = LLTrans::getString("snapshot_quality_very_high"); } - getChild("image_quality_level")->setTextArg("[QLVL]", quality_lvl); + LLTextBox* quality_lvl_ctrl = getChild("image_quality_level"); + if (quality_lvl_ctrl) + { + quality_lvl_ctrl->setTextArg("[QLVL]", quality_lvl); + } } void LLPanelSnapshot::goBack() diff --git a/indra/newview/llviewerassetstorage.cpp b/indra/newview/llviewerassetstorage.cpp index fd462fb225..657fb0ffc1 100644 --- a/indra/newview/llviewerassetstorage.cpp +++ b/indra/newview/llviewerassetstorage.cpp @@ -506,7 +506,7 @@ void LLViewerAssetStorage::assetRequestCoro( if (!gAgent.getRegion()->capabilitiesReceived()) { - LL_WARNS_ONCE("ViewerAsset") << "Waiting for capabilities" << LL_ENDL; + LL_INFOS_ONCE("ViewerAsset") << "Waiting for capabilities" << LL_ENDL; LLEventStream capsRecv("waitForCaps", true); @@ -533,8 +533,8 @@ void LLViewerAssetStorage::assetRequestCoro( return; } - LL_WARNS_ONCE("ViewerAsset") << "capsRecv got event" << LL_ENDL; - LL_WARNS_ONCE("ViewerAsset") << "region " << gAgent.getRegion() << " mViewerAssetUrl " << mViewerAssetUrl << LL_ENDL; + LL_INFOS_ONCE("ViewerAsset") << "capsRecv got event" << LL_ENDL; + LL_INFOS_ONCE("ViewerAsset") << "region " << gAgent.getRegion() << " mViewerAssetUrl " << mViewerAssetUrl << LL_ENDL; } if (mViewerAssetUrl.empty() && gAgent.getRegion()) { diff --git a/indra/newview/llviewernetwork.cpp b/indra/newview/llviewernetwork.cpp index 8ec2a6d661..fb52d0a8e6 100644 --- a/indra/newview/llviewernetwork.cpp +++ b/indra/newview/llviewernetwork.cpp @@ -135,7 +135,8 @@ void LLGridManager::initialize(const std::string& grid_file) LLSD other_grids; llifstream llsd_xml; - if (!grid_file.empty()) + // grids.xml is not supplied by default + if (!grid_file.empty() && LLFile::isfile(grid_file)) { LL_INFOS("GridManager")<<"Grid configuration file '"< Date: Mon, 27 Jul 2026 21:35:52 +0200 Subject: #6055 Move texture upload preparation off the main thread (#6031) --- indra/llrender/CMakeLists.txt | 5 + indra/llrender/llimagegl.cpp | 309 ++++++++++++++++-------- indra/llrender/llimagegl.h | 17 ++ indra/llrender/tests/llimagegl_prepare_test.cpp | 118 +++++++++ indra/newview/llviewertexture.cpp | 52 +++- 5 files changed, 395 insertions(+), 106 deletions(-) create mode 100644 indra/llrender/tests/llimagegl_prepare_test.cpp (limited to 'indra/llrender') diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index fcd287bbb3..149b50e866 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -106,3 +106,8 @@ target_link_libraries(llrender OpenGL::GLU ) +if (LL_TESTS) + include(LLAddBuildTest) + set(test_libs llrender llimage) + LL_ADD_INTEGRATION_TEST(llimagegl_prepare "" "${test_libs}") +endif (LL_TESTS) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ff..31298b03a3 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -42,6 +42,7 @@ #include "llwindow.h" #include "llframetimer.h" #include +#include extern LL_COMMON_API bool on_main_thread(); @@ -541,6 +542,7 @@ void LLImageGL::init(bool usemipmaps, bool allow_compression) mIsMask = false; mNeedsAlphaAndPickMask = true ; + mUploadPreparation.reset(); mAlphaStride = 0 ; mAlphaOffset = 0 ; @@ -588,6 +590,7 @@ void LLImageGL::cleanup() destroyGLTexture(); } freePickMask(); + discardUploadPreparation(); mSaveData = NULL; // deletes data } @@ -744,6 +747,32 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + bool alpha_prepared = false; + bool pick_mask_prepared = false; + if (mUploadPreparation) + { + TextureUploadPreparation preparation = std::move(*mUploadPreparation); + mUploadPreparation.reset(); + alpha_prepared = preparation.mAlphaAnalyzed; + pick_mask_prepared = preparation.mPickMaskPrepared; + if (alpha_prepared) + { + mIsMask = preparation.mIsMask; + } + + if (pick_mask_prepared) + { + freePickMask(); + mPickMaskWidth = preparation.mPickMaskWidth; + mPickMaskHeight = preparation.mPickMaskHeight; + if (!preparation.mPickMask.empty()) + { + mPickMask = new U8[preparation.mPickMask.size()]; + memcpy(mPickMask, preparation.mPickMask.data(), preparation.mPickMask.size()); + } + } + } + const bool is_compressed = isCompressed(); if (mUseMipMaps) @@ -803,11 +832,14 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 } LLImageGL::setManualImage(mTarget, gl_level, mFormatInternal, w, h, mFormatPrimary, GL_UNSIGNED_BYTE, (GLvoid*)data_in, mAllowCompression); - if (gl_level == 0) + if (gl_level == 0 && !alpha_prepared) { analyzeAlpha(data_in, w, h); } - updatePickMask(w, h, data_in); + if (!pick_mask_prepared) + { + updatePickMask(w, h, data_in); + } if(mFormatSwapBytes) { @@ -849,10 +881,16 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 w, h, mFormatPrimary, mFormatType, data_in, mAllowCompression); - analyzeAlpha(data_in, w, h); + if (!alpha_prepared) + { + analyzeAlpha(data_in, w, h); + } stop_glerror(); - updatePickMask(w, h, data_in); + if (!pick_mask_prepared) + { + updatePickMask(w, h, data_in); + } if(mFormatSwapBytes) { @@ -950,12 +988,12 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 } LLImageGL::setManualImage(mTarget, m, mFormatInternal, w, h, mFormatPrimary, mFormatType, cur_mip_data, mAllowCompression); - if (m == 0) + if (m == 0 && !alpha_prepared) { analyzeAlpha(data_in, w, h); } stop_glerror(); - if (m == 0) + if (m == 0 && !pick_mask_prepared) { updatePickMask(w, h, cur_mip_data); } @@ -1007,9 +1045,15 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 LLImageGL::setManualImage(mTarget, 0, mFormatInternal, w, h, mFormatPrimary, mFormatType, (GLvoid *)data_in, mAllowCompression); - analyzeAlpha(data_in, w, h); + if (!alpha_prepared) + { + analyzeAlpha(data_in, w, h); + } - updatePickMask(w, h, data_in); + if (!pick_mask_prepared) + { + updatePickMask(w, h, data_in); + } stop_glerror(); @@ -2114,6 +2158,153 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask) } } +namespace +{ +bool analyze_alpha_mask(const U8* data, U32 width, U32 height, S32 alpha_stride, S32 alpha_offset) +{ + U32 length = width * height; + U32 alpha_total = 0; + U32 sample[16] = {}; + + // Generate a histogram of quantized alpha. + // Also add the histogram of a 2x2 box-sampled version. The idea is + // to mid-skew the data (and thus reduce the chance of treating it as + // a mask) for high-frequency alpha maps, which suffer the worst from + // aliasing when used as alpha masks. + if (width >= 2 && height >= 2 && width % 2 == 0 && height % 2 == 0) + { + const U8* row_start = data + alpha_offset; + for (U32 y = 0; y < height; y += 2) + { + const U8* current = row_start; + for (U32 x = 0; x < width; x += 2) + { + const U32 s1 = current[0]; + alpha_total += s1; + const U32 s2 = current[width * alpha_stride]; + alpha_total += s2; + current += alpha_stride; + const U32 s3 = current[0]; + alpha_total += s3; + const U32 s4 = current[width * alpha_stride]; + alpha_total += s4; + current += alpha_stride; + + ++sample[s1 / 16]; + ++sample[s2 / 16]; + ++sample[s3 / 16]; + ++sample[s4 / 16]; + + const U32 average_sum = s1 + s2 + s3 + s4; + alpha_total += average_sum; + sample[average_sum / (16 * 4)] += 4; + } + + row_start += 2 * width * alpha_stride; + } + length *= 2; // We sampled everything twice, essentially. + } + else + { + const U8* current = data + alpha_offset; + for (U32 i = 0; i < length; ++i) + { + const U32 alpha = *current; + alpha_total += alpha; + ++sample[alpha / 16]; + current += alpha_stride; + } + } + + // Too many mid-range alpha samples make the texture unsuitable for a + // 1-bit mask. Likewise, if all samples are clumped in one half of the + // range (but not at an absolute extreme), treat that as an intentional + // effect rather than a mask. + U32 midrange_total = 0; + for (U32 i = 2; i < 13; ++i) + { + midrange_total += sample[i]; + } + U32 lower_half_total = 0; + for (U32 i = 0; i < 8; ++i) + { + lower_half_total += sample[i]; + } + U32 upper_half_total = 0; + for (U32 i = 8; i < 16; ++i) + { + upper_half_total += sample[i]; + } + + return midrange_total <= length / 48 && + (lower_half_total != length || alpha_total == 0) && + (upper_half_total != length || alpha_total == 255 * length); +} +} + +LLImageGL::TextureUploadPreparation LLImageGL::prepareForUpload(const LLImageRaw* image) +{ + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("prepare texture upload"); + + TextureUploadPreparation result; + if (!image || image->isBufferInvalid()) + { + return result; + } + + const S32 width = image->getWidth(); + const S32 height = image->getHeight(); + const S32 components = image->getComponents(); + const U8* data = image->getData(); + if (!data || (components != 1 && components != 2 && components != 4)) + { + return result; + } + + if (!sSkipAnalyzeAlpha) + { + result.mAlphaAnalyzed = true; + result.mIsMask = analyze_alpha_mask(data, width, height, components, components - 1); + } + + if (components == 4) + { + const U32 pick_width = (static_cast(width) + 1) / 2; + const U32 pick_height = (static_cast(height) + 1) / 2; + result.mPickMaskPrepared = true; + result.mPickMaskWidth = static_cast(pick_width); + result.mPickMaskHeight = static_cast(pick_height); + const U32 bit_count = pick_width * pick_height; + result.mPickMask.resize((bit_count + 7) / 8); + + const S32 alpha_offset = components - 1; + U32 pick_bit = 0; + for (S32 y = 0; y < height; y += 2) + { + for (S32 x = 0; x < width; x += 2) + { + if (data[(y * width + x) * components + alpha_offset] > 32) + { + result.mPickMask[pick_bit / 8] |= 1 << (pick_bit % 8); + } + ++pick_bit; + } + } + } + + return result; +} + +void LLImageGL::applyUploadPreparation(TextureUploadPreparation&& preparation) +{ + mUploadPreparation = std::make_unique(std::move(preparation)); +} + +void LLImageGL::discardUploadPreparation() +{ + mUploadPreparation.reset(); +} + void LLImageGL::calcAlphaChannelOffsetAndStride() { if(mAlphaOffset == INVALID_OFFSET)//do not need alpha mask @@ -2195,98 +2386,7 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) } LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - - U32 length = w * h; - U32 alphatotal = 0; - - U32 sample[16]; - memset(sample, 0, sizeof(U32)*16); - - // generate histogram of quantized alpha. - // also add-in the histogram of a 2x2 box-sampled version. The idea is - // this will mid-skew the data (and thus increase the chances of not - // being used as a mask) from high-frequency alpha maps which - // suffer the worst from aliasing when used as alpha masks. - if (w >= 2 && h >= 2) - { - llassert(w % 2 == 0); - llassert(h % 2 == 0); - const GLubyte* rowstart = ((const GLubyte*) data_in) + mAlphaOffset; - for (U32 y = 0; y < h; y += 2) - { - const GLubyte* current = rowstart; - for (U32 x = 0; x < w; x += 2) - { - const U32 s1 = current[0]; - alphatotal += s1; - const U32 s2 = current[w * mAlphaStride]; - alphatotal += s2; - current += mAlphaStride; - const U32 s3 = current[0]; - alphatotal += s3; - const U32 s4 = current[w * mAlphaStride]; - alphatotal += s4; - current += mAlphaStride; - - ++sample[s1/16]; - ++sample[s2/16]; - ++sample[s3/16]; - ++sample[s4/16]; - - const U32 asum = (s1+s2+s3+s4); - alphatotal += asum; - sample[asum/(16*4)] += 4; - } - - rowstart += 2 * w * mAlphaStride; - } - length *= 2; // we sampled everything twice, essentially - } - else - { - const GLubyte* current = ((const GLubyte*) data_in) + mAlphaOffset; - for (U32 i = 0; i < length; i++) - { - const U32 s1 = *current; - alphatotal += s1; - ++sample[s1/16]; - current += mAlphaStride; - } - } - - // if more than 1/16th of alpha samples are mid-range, this - // shouldn't be treated as a 1-bit mask - - // also, if all of the alpha samples are clumped on one half - // of the range (but not at an absolute extreme), then consider - // this to be an intentional effect and don't treat as a mask. - - U32 midrangetotal = 0; - for (U32 i = 2; i < 13; i++) - { - midrangetotal += sample[i]; - } - U32 lowerhalftotal = 0; - for (U32 i = 0; i < 8; i++) - { - lowerhalftotal += sample[i]; - } - U32 upperhalftotal = 0; - for (U32 i = 8; i < 16; i++) - { - upperhalftotal += sample[i]; - } - - if (midrangetotal > length/48 || // lots of midrange, or - (lowerhalftotal == length && alphatotal != 0) || // all close to transparent but not all totally transparent, or - (upperhalftotal == length && alphatotal != 255*length)) // all close to opaque but not all totally opaque - { - mIsMask = false; // not suitable for masking - } - else - { - mIsMask = true; - } + mIsMask = analyze_alpha_mask(static_cast(data_in), w, h, mAlphaStride, mAlphaOffset); } //---------------------------------------------------------------------------- @@ -2294,14 +2394,14 @@ U32 LLImageGL::createPickMask(S32 pWidth, S32 pHeight) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; freePickMask(); - U32 pick_width = pWidth/2 + 1; - U32 pick_height = pHeight/2 + 1; + U32 pick_width = (static_cast(pWidth) + 1) / 2; + U32 pick_height = (static_cast(pHeight) + 1) / 2; U32 size = pick_width * pick_height; size = (size + 7) / 8; // pixelcount-to-bits mPickMask = new U8[size]; - mPickMaskWidth = pick_width - 1; - mPickMaskHeight = pick_height - 1; + mPickMaskWidth = static_cast(pick_width); + mPickMaskHeight = static_cast(pick_height); memset(mPickMask, 0, sizeof(U8) * size); @@ -2641,4 +2741,3 @@ void LLImageGLThread::run() gGL.shutdown(); mWindow->destroySharedContext(mContext); } - diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e..adff2c6a10 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -39,7 +39,9 @@ #include "llrender.h" #include "threadpool.h" #include "workqueue.h" +#include #include +#include #define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL @@ -61,6 +63,16 @@ class LLImageGL : public LLRefCount { friend class LLTexUnit; public: + struct TextureUploadPreparation + { + bool mAlphaAnalyzed = false; + bool mIsMask = false; + bool mPickMaskPrepared = false; + U16 mPickMaskWidth = 0; + U16 mPickMaskHeight = 0; + std::vector mPickMask; + }; + // call once per frame static void updateClass(); @@ -207,6 +219,10 @@ public: virtual void cleanup(); // Clean up the LLImageGL so it can be reinitialized. Be careful when using this in derived class destructors void setNeedsAlphaAndPickMask(bool need_mask); + bool getNeedsAlphaAndPickMask() const { return mNeedsAlphaAndPickMask; } + static TextureUploadPreparation prepareForUpload(const LLImageRaw* image); + void applyUploadPreparation(TextureUploadPreparation&& preparation); + void discardUploadPreparation(); #if LL_IMAGEGL_THREAD_CHECK // thread debugging @@ -242,6 +258,7 @@ private: bool mIsMask; bool mNeedsAlphaAndPickMask; + std::unique_ptr mUploadPreparation; S8 mAlphaStride ; S8 mAlphaOffset ; diff --git a/indra/llrender/tests/llimagegl_prepare_test.cpp b/indra/llrender/tests/llimagegl_prepare_test.cpp new file mode 100644 index 0000000000..96df21bcc5 --- /dev/null +++ b/indra/llrender/tests/llimagegl_prepare_test.cpp @@ -0,0 +1,118 @@ +/** + * @file llimagegl_prepare_test.cpp + * @brief Tests for CPU-side texture upload preparation. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../test/lltut.h" + +#include "llimage.h" +#include "llimagegl.h" + +namespace tut +{ + struct llimagegl_prepare_data + { + static LLPointer make_image(U16 width, U16 height, S8 components) + { + LLPointer image = new LLImageRaw(width, height, components); + memset(image->getData(), 0, image->getDataSize()); + return image; + } + }; + + typedef test_group llimagegl_prepare_test; + typedef llimagegl_prepare_test::object llimagegl_prepare_object; + llimagegl_prepare_test llimagegl_prepare_test_factory("LLImageGL upload preparation"); + + template<> template<> + void llimagegl_prepare_object::test<1>() + { + LLPointer image = make_image(3, 5, 4); + U8* data = image->getData(); + for (S32 y = 0; y < image->getHeight(); y += 2) + { + data[(y * image->getWidth()) * image->getComponents() + 3] = 255; + } + + LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); + + ensure("alpha analysis is prepared", result.mAlphaAnalyzed); + ensure("binary alpha is classified as a mask", result.mIsMask); + ensure("RGBA pick mask is prepared", result.mPickMaskPrepared); + ensure_equals("odd width uses ceiling half-width", result.mPickMaskWidth, U16(2)); + ensure_equals("odd height uses ceiling half-height", result.mPickMaskHeight, U16(3)); + ensure_equals("alternating pick bits", result.mPickMask[0], U8(0x15)); + } + + template<> template<> + void llimagegl_prepare_object::test<2>() + { + LLPointer image = make_image(15, 2, 4); + U8* data = image->getData(); + for (S32 x = 0; x < image->getWidth(); x += 4) + { + data[x * image->getComponents() + 3] = 255; + } + + LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); + + ensure("RGBA pick mask is prepared", result.mPickMaskPrepared); + ensure_equals("pick width matches samples written", result.mPickMaskWidth, U16(8)); + ensure_equals("pick height matches samples written", result.mPickMaskHeight, U16(1)); + ensure_equals("exactly eight bits need one byte", result.mPickMask.size(), size_t(1)); + ensure_equals("all eight sample positions map correctly", result.mPickMask[0], U8(0x55)); + } + + template<> template<> + void llimagegl_prepare_object::test<3>() + { + LLPointer image = make_image(3, 3, 2); + U8* data = image->getData(); + for (S32 pixel = 0; pixel < image->getWidth() * image->getHeight(); ++pixel) + { + data[pixel * image->getComponents() + 1] = 255; + } + + LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); + + ensure("luminance-alpha analysis is prepared", result.mAlphaAnalyzed); + ensure("opaque alpha is classified as a mask", result.mIsMask); + ensure("unsupported pick-mask layout falls back to setImage", !result.mPickMaskPrepared); + ensure("no unsupported pick-mask data is produced", result.mPickMask.empty()); + } + + template<> template<> + void llimagegl_prepare_object::test<4>() + { + LLPointer image = make_image(2, 2, 3); + + LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); + + ensure("RGB has no alpha preparation", !result.mAlphaAnalyzed); + ensure("RGB has no pick-mask preparation", !result.mPickMaskPrepared); + ensure("RGB has no prepared pick-mask data", result.mPickMask.empty()); + } +} diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 0f23596c9a..db20658497 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -63,6 +63,8 @@ #include "lltexturecache.h" #include "llviewerwindow.h" #include "llwindow.h" + +#include /////////////////////////////////////////////////////////////////////////////// // statics @@ -1581,6 +1583,7 @@ bool LLViewerFetchedTexture::createTexture(S32 usename/*= 0*/) void LLViewerFetchedTexture::postCreateTexture() { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + mGLTexturep->discardUploadPreparation(); if (!mNeedsCreateTexture) { return; @@ -1679,6 +1682,54 @@ void LLViewerFetchedTexture::scheduleCreateTexture() unref(); }); } + else if (!mGLTexturep->getHasExplicitFormat() && + mGLTexturep->getNeedsAlphaAndPickMask() && + mRawImage->getComponents() != 3) + { + auto main_queue = mMainQueue.lock(); + auto general_queue = LL::WorkQueue::getInstance("General"); + if (main_queue && general_queue) + { + LLPointer raw_image = mRawImage; + ref(); + const bool posted = main_queue->postTo( + general_queue, + [raw_image]() + { + return LLImageGL::prepareForUpload(raw_image); + }, + [this, raw_image](LLImageGL::TextureUploadPreparation preparation) + { + if (mNeedsCreateTexture) + { + if (mRawImage == raw_image) + { + mGLTexturep->applyUploadPreparation(std::move(preparation)); + } + if (!mCreatePending) + { + mCreatePending = true; + gTextureList.mCreateTextureList.push(this); + } + } + unref(); + }); + if (!posted) + { + unref(); + if (!mCreatePending) + { + mCreatePending = true; + gTextureList.mCreateTextureList.push(this); + } + } + } + else if (!mCreatePending) + { + mCreatePending = true; + gTextureList.mCreateTextureList.push(this); + } + } else { if (!mCreatePending) @@ -4108,4 +4159,3 @@ void LLTexturePipelineTester::LLTextureTestSession::reset() //---------------------------------------------------------------------------------------------- //end of LLTexturePipelineTester //---------------------------------------------------------------------------------------------- - -- cgit v1.3 From 556964ab1d88c438a7109c9283b7c095bc6359e4 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:58:36 +0300 Subject: Revert "#6055 Move texture upload preparation off the main thread (#6031)" This reverts commit 1c81813e79c4acc917d75b47117778dd277e5b1f. --- indra/llrender/CMakeLists.txt | 5 - indra/llrender/llimagegl.cpp | 309 ++++++++---------------- indra/llrender/llimagegl.h | 17 -- indra/llrender/tests/llimagegl_prepare_test.cpp | 118 --------- indra/newview/llviewertexture.cpp | 52 +--- 5 files changed, 106 insertions(+), 395 deletions(-) delete mode 100644 indra/llrender/tests/llimagegl_prepare_test.cpp (limited to 'indra/llrender') diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index 149b50e866..fcd287bbb3 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -106,8 +106,3 @@ target_link_libraries(llrender OpenGL::GLU ) -if (LL_TESTS) - include(LLAddBuildTest) - set(test_libs llrender llimage) - LL_ADD_INTEGRATION_TEST(llimagegl_prepare "" "${test_libs}") -endif (LL_TESTS) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 31298b03a3..4a3d32c7ff 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -42,7 +42,6 @@ #include "llwindow.h" #include "llframetimer.h" #include -#include extern LL_COMMON_API bool on_main_thread(); @@ -542,7 +541,6 @@ void LLImageGL::init(bool usemipmaps, bool allow_compression) mIsMask = false; mNeedsAlphaAndPickMask = true ; - mUploadPreparation.reset(); mAlphaStride = 0 ; mAlphaOffset = 0 ; @@ -590,7 +588,6 @@ void LLImageGL::cleanup() destroyGLTexture(); } freePickMask(); - discardUploadPreparation(); mSaveData = NULL; // deletes data } @@ -747,32 +744,6 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - bool alpha_prepared = false; - bool pick_mask_prepared = false; - if (mUploadPreparation) - { - TextureUploadPreparation preparation = std::move(*mUploadPreparation); - mUploadPreparation.reset(); - alpha_prepared = preparation.mAlphaAnalyzed; - pick_mask_prepared = preparation.mPickMaskPrepared; - if (alpha_prepared) - { - mIsMask = preparation.mIsMask; - } - - if (pick_mask_prepared) - { - freePickMask(); - mPickMaskWidth = preparation.mPickMaskWidth; - mPickMaskHeight = preparation.mPickMaskHeight; - if (!preparation.mPickMask.empty()) - { - mPickMask = new U8[preparation.mPickMask.size()]; - memcpy(mPickMask, preparation.mPickMask.data(), preparation.mPickMask.size()); - } - } - } - const bool is_compressed = isCompressed(); if (mUseMipMaps) @@ -832,14 +803,11 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 } LLImageGL::setManualImage(mTarget, gl_level, mFormatInternal, w, h, mFormatPrimary, GL_UNSIGNED_BYTE, (GLvoid*)data_in, mAllowCompression); - if (gl_level == 0 && !alpha_prepared) + if (gl_level == 0) { analyzeAlpha(data_in, w, h); } - if (!pick_mask_prepared) - { - updatePickMask(w, h, data_in); - } + updatePickMask(w, h, data_in); if(mFormatSwapBytes) { @@ -881,16 +849,10 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 w, h, mFormatPrimary, mFormatType, data_in, mAllowCompression); - if (!alpha_prepared) - { - analyzeAlpha(data_in, w, h); - } + analyzeAlpha(data_in, w, h); stop_glerror(); - if (!pick_mask_prepared) - { - updatePickMask(w, h, data_in); - } + updatePickMask(w, h, data_in); if(mFormatSwapBytes) { @@ -988,12 +950,12 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 } LLImageGL::setManualImage(mTarget, m, mFormatInternal, w, h, mFormatPrimary, mFormatType, cur_mip_data, mAllowCompression); - if (m == 0 && !alpha_prepared) + if (m == 0) { analyzeAlpha(data_in, w, h); } stop_glerror(); - if (m == 0 && !pick_mask_prepared) + if (m == 0) { updatePickMask(w, h, cur_mip_data); } @@ -1045,15 +1007,9 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 LLImageGL::setManualImage(mTarget, 0, mFormatInternal, w, h, mFormatPrimary, mFormatType, (GLvoid *)data_in, mAllowCompression); - if (!alpha_prepared) - { - analyzeAlpha(data_in, w, h); - } + analyzeAlpha(data_in, w, h); - if (!pick_mask_prepared) - { - updatePickMask(w, h, data_in); - } + updatePickMask(w, h, data_in); stop_glerror(); @@ -2158,153 +2114,6 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask) } } -namespace -{ -bool analyze_alpha_mask(const U8* data, U32 width, U32 height, S32 alpha_stride, S32 alpha_offset) -{ - U32 length = width * height; - U32 alpha_total = 0; - U32 sample[16] = {}; - - // Generate a histogram of quantized alpha. - // Also add the histogram of a 2x2 box-sampled version. The idea is - // to mid-skew the data (and thus reduce the chance of treating it as - // a mask) for high-frequency alpha maps, which suffer the worst from - // aliasing when used as alpha masks. - if (width >= 2 && height >= 2 && width % 2 == 0 && height % 2 == 0) - { - const U8* row_start = data + alpha_offset; - for (U32 y = 0; y < height; y += 2) - { - const U8* current = row_start; - for (U32 x = 0; x < width; x += 2) - { - const U32 s1 = current[0]; - alpha_total += s1; - const U32 s2 = current[width * alpha_stride]; - alpha_total += s2; - current += alpha_stride; - const U32 s3 = current[0]; - alpha_total += s3; - const U32 s4 = current[width * alpha_stride]; - alpha_total += s4; - current += alpha_stride; - - ++sample[s1 / 16]; - ++sample[s2 / 16]; - ++sample[s3 / 16]; - ++sample[s4 / 16]; - - const U32 average_sum = s1 + s2 + s3 + s4; - alpha_total += average_sum; - sample[average_sum / (16 * 4)] += 4; - } - - row_start += 2 * width * alpha_stride; - } - length *= 2; // We sampled everything twice, essentially. - } - else - { - const U8* current = data + alpha_offset; - for (U32 i = 0; i < length; ++i) - { - const U32 alpha = *current; - alpha_total += alpha; - ++sample[alpha / 16]; - current += alpha_stride; - } - } - - // Too many mid-range alpha samples make the texture unsuitable for a - // 1-bit mask. Likewise, if all samples are clumped in one half of the - // range (but not at an absolute extreme), treat that as an intentional - // effect rather than a mask. - U32 midrange_total = 0; - for (U32 i = 2; i < 13; ++i) - { - midrange_total += sample[i]; - } - U32 lower_half_total = 0; - for (U32 i = 0; i < 8; ++i) - { - lower_half_total += sample[i]; - } - U32 upper_half_total = 0; - for (U32 i = 8; i < 16; ++i) - { - upper_half_total += sample[i]; - } - - return midrange_total <= length / 48 && - (lower_half_total != length || alpha_total == 0) && - (upper_half_total != length || alpha_total == 255 * length); -} -} - -LLImageGL::TextureUploadPreparation LLImageGL::prepareForUpload(const LLImageRaw* image) -{ - LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("prepare texture upload"); - - TextureUploadPreparation result; - if (!image || image->isBufferInvalid()) - { - return result; - } - - const S32 width = image->getWidth(); - const S32 height = image->getHeight(); - const S32 components = image->getComponents(); - const U8* data = image->getData(); - if (!data || (components != 1 && components != 2 && components != 4)) - { - return result; - } - - if (!sSkipAnalyzeAlpha) - { - result.mAlphaAnalyzed = true; - result.mIsMask = analyze_alpha_mask(data, width, height, components, components - 1); - } - - if (components == 4) - { - const U32 pick_width = (static_cast(width) + 1) / 2; - const U32 pick_height = (static_cast(height) + 1) / 2; - result.mPickMaskPrepared = true; - result.mPickMaskWidth = static_cast(pick_width); - result.mPickMaskHeight = static_cast(pick_height); - const U32 bit_count = pick_width * pick_height; - result.mPickMask.resize((bit_count + 7) / 8); - - const S32 alpha_offset = components - 1; - U32 pick_bit = 0; - for (S32 y = 0; y < height; y += 2) - { - for (S32 x = 0; x < width; x += 2) - { - if (data[(y * width + x) * components + alpha_offset] > 32) - { - result.mPickMask[pick_bit / 8] |= 1 << (pick_bit % 8); - } - ++pick_bit; - } - } - } - - return result; -} - -void LLImageGL::applyUploadPreparation(TextureUploadPreparation&& preparation) -{ - mUploadPreparation = std::make_unique(std::move(preparation)); -} - -void LLImageGL::discardUploadPreparation() -{ - mUploadPreparation.reset(); -} - void LLImageGL::calcAlphaChannelOffsetAndStride() { if(mAlphaOffset == INVALID_OFFSET)//do not need alpha mask @@ -2386,7 +2195,98 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) } LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - mIsMask = analyze_alpha_mask(static_cast(data_in), w, h, mAlphaStride, mAlphaOffset); + + U32 length = w * h; + U32 alphatotal = 0; + + U32 sample[16]; + memset(sample, 0, sizeof(U32)*16); + + // generate histogram of quantized alpha. + // also add-in the histogram of a 2x2 box-sampled version. The idea is + // this will mid-skew the data (and thus increase the chances of not + // being used as a mask) from high-frequency alpha maps which + // suffer the worst from aliasing when used as alpha masks. + if (w >= 2 && h >= 2) + { + llassert(w % 2 == 0); + llassert(h % 2 == 0); + const GLubyte* rowstart = ((const GLubyte*) data_in) + mAlphaOffset; + for (U32 y = 0; y < h; y += 2) + { + const GLubyte* current = rowstart; + for (U32 x = 0; x < w; x += 2) + { + const U32 s1 = current[0]; + alphatotal += s1; + const U32 s2 = current[w * mAlphaStride]; + alphatotal += s2; + current += mAlphaStride; + const U32 s3 = current[0]; + alphatotal += s3; + const U32 s4 = current[w * mAlphaStride]; + alphatotal += s4; + current += mAlphaStride; + + ++sample[s1/16]; + ++sample[s2/16]; + ++sample[s3/16]; + ++sample[s4/16]; + + const U32 asum = (s1+s2+s3+s4); + alphatotal += asum; + sample[asum/(16*4)] += 4; + } + + rowstart += 2 * w * mAlphaStride; + } + length *= 2; // we sampled everything twice, essentially + } + else + { + const GLubyte* current = ((const GLubyte*) data_in) + mAlphaOffset; + for (U32 i = 0; i < length; i++) + { + const U32 s1 = *current; + alphatotal += s1; + ++sample[s1/16]; + current += mAlphaStride; + } + } + + // if more than 1/16th of alpha samples are mid-range, this + // shouldn't be treated as a 1-bit mask + + // also, if all of the alpha samples are clumped on one half + // of the range (but not at an absolute extreme), then consider + // this to be an intentional effect and don't treat as a mask. + + U32 midrangetotal = 0; + for (U32 i = 2; i < 13; i++) + { + midrangetotal += sample[i]; + } + U32 lowerhalftotal = 0; + for (U32 i = 0; i < 8; i++) + { + lowerhalftotal += sample[i]; + } + U32 upperhalftotal = 0; + for (U32 i = 8; i < 16; i++) + { + upperhalftotal += sample[i]; + } + + if (midrangetotal > length/48 || // lots of midrange, or + (lowerhalftotal == length && alphatotal != 0) || // all close to transparent but not all totally transparent, or + (upperhalftotal == length && alphatotal != 255*length)) // all close to opaque but not all totally opaque + { + mIsMask = false; // not suitable for masking + } + else + { + mIsMask = true; + } } //---------------------------------------------------------------------------- @@ -2394,14 +2294,14 @@ U32 LLImageGL::createPickMask(S32 pWidth, S32 pHeight) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; freePickMask(); - U32 pick_width = (static_cast(pWidth) + 1) / 2; - U32 pick_height = (static_cast(pHeight) + 1) / 2; + U32 pick_width = pWidth/2 + 1; + U32 pick_height = pHeight/2 + 1; U32 size = pick_width * pick_height; size = (size + 7) / 8; // pixelcount-to-bits mPickMask = new U8[size]; - mPickMaskWidth = static_cast(pick_width); - mPickMaskHeight = static_cast(pick_height); + mPickMaskWidth = pick_width - 1; + mPickMaskHeight = pick_height - 1; memset(mPickMask, 0, sizeof(U8) * size); @@ -2741,3 +2641,4 @@ void LLImageGLThread::run() gGL.shutdown(); mWindow->destroySharedContext(mContext); } + diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index adff2c6a10..6b4492c09e 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -39,9 +39,7 @@ #include "llrender.h" #include "threadpool.h" #include "workqueue.h" -#include #include -#include #define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL @@ -63,16 +61,6 @@ class LLImageGL : public LLRefCount { friend class LLTexUnit; public: - struct TextureUploadPreparation - { - bool mAlphaAnalyzed = false; - bool mIsMask = false; - bool mPickMaskPrepared = false; - U16 mPickMaskWidth = 0; - U16 mPickMaskHeight = 0; - std::vector mPickMask; - }; - // call once per frame static void updateClass(); @@ -219,10 +207,6 @@ public: virtual void cleanup(); // Clean up the LLImageGL so it can be reinitialized. Be careful when using this in derived class destructors void setNeedsAlphaAndPickMask(bool need_mask); - bool getNeedsAlphaAndPickMask() const { return mNeedsAlphaAndPickMask; } - static TextureUploadPreparation prepareForUpload(const LLImageRaw* image); - void applyUploadPreparation(TextureUploadPreparation&& preparation); - void discardUploadPreparation(); #if LL_IMAGEGL_THREAD_CHECK // thread debugging @@ -258,7 +242,6 @@ private: bool mIsMask; bool mNeedsAlphaAndPickMask; - std::unique_ptr mUploadPreparation; S8 mAlphaStride ; S8 mAlphaOffset ; diff --git a/indra/llrender/tests/llimagegl_prepare_test.cpp b/indra/llrender/tests/llimagegl_prepare_test.cpp deleted file mode 100644 index 96df21bcc5..0000000000 --- a/indra/llrender/tests/llimagegl_prepare_test.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @file llimagegl_prepare_test.cpp - * @brief Tests for CPU-side texture upload preparation. - * - * $LicenseInfo:firstyear=2026&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2026, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "linden_common.h" - -#include "../test/lltut.h" - -#include "llimage.h" -#include "llimagegl.h" - -namespace tut -{ - struct llimagegl_prepare_data - { - static LLPointer make_image(U16 width, U16 height, S8 components) - { - LLPointer image = new LLImageRaw(width, height, components); - memset(image->getData(), 0, image->getDataSize()); - return image; - } - }; - - typedef test_group llimagegl_prepare_test; - typedef llimagegl_prepare_test::object llimagegl_prepare_object; - llimagegl_prepare_test llimagegl_prepare_test_factory("LLImageGL upload preparation"); - - template<> template<> - void llimagegl_prepare_object::test<1>() - { - LLPointer image = make_image(3, 5, 4); - U8* data = image->getData(); - for (S32 y = 0; y < image->getHeight(); y += 2) - { - data[(y * image->getWidth()) * image->getComponents() + 3] = 255; - } - - LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); - - ensure("alpha analysis is prepared", result.mAlphaAnalyzed); - ensure("binary alpha is classified as a mask", result.mIsMask); - ensure("RGBA pick mask is prepared", result.mPickMaskPrepared); - ensure_equals("odd width uses ceiling half-width", result.mPickMaskWidth, U16(2)); - ensure_equals("odd height uses ceiling half-height", result.mPickMaskHeight, U16(3)); - ensure_equals("alternating pick bits", result.mPickMask[0], U8(0x15)); - } - - template<> template<> - void llimagegl_prepare_object::test<2>() - { - LLPointer image = make_image(15, 2, 4); - U8* data = image->getData(); - for (S32 x = 0; x < image->getWidth(); x += 4) - { - data[x * image->getComponents() + 3] = 255; - } - - LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); - - ensure("RGBA pick mask is prepared", result.mPickMaskPrepared); - ensure_equals("pick width matches samples written", result.mPickMaskWidth, U16(8)); - ensure_equals("pick height matches samples written", result.mPickMaskHeight, U16(1)); - ensure_equals("exactly eight bits need one byte", result.mPickMask.size(), size_t(1)); - ensure_equals("all eight sample positions map correctly", result.mPickMask[0], U8(0x55)); - } - - template<> template<> - void llimagegl_prepare_object::test<3>() - { - LLPointer image = make_image(3, 3, 2); - U8* data = image->getData(); - for (S32 pixel = 0; pixel < image->getWidth() * image->getHeight(); ++pixel) - { - data[pixel * image->getComponents() + 1] = 255; - } - - LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); - - ensure("luminance-alpha analysis is prepared", result.mAlphaAnalyzed); - ensure("opaque alpha is classified as a mask", result.mIsMask); - ensure("unsupported pick-mask layout falls back to setImage", !result.mPickMaskPrepared); - ensure("no unsupported pick-mask data is produced", result.mPickMask.empty()); - } - - template<> template<> - void llimagegl_prepare_object::test<4>() - { - LLPointer image = make_image(2, 2, 3); - - LLImageGL::TextureUploadPreparation result = LLImageGL::prepareForUpload(image); - - ensure("RGB has no alpha preparation", !result.mAlphaAnalyzed); - ensure("RGB has no pick-mask preparation", !result.mPickMaskPrepared); - ensure("RGB has no prepared pick-mask data", result.mPickMask.empty()); - } -} diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index db20658497..0f23596c9a 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -63,8 +63,6 @@ #include "lltexturecache.h" #include "llviewerwindow.h" #include "llwindow.h" - -#include /////////////////////////////////////////////////////////////////////////////// // statics @@ -1583,7 +1581,6 @@ bool LLViewerFetchedTexture::createTexture(S32 usename/*= 0*/) void LLViewerFetchedTexture::postCreateTexture() { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - mGLTexturep->discardUploadPreparation(); if (!mNeedsCreateTexture) { return; @@ -1682,54 +1679,6 @@ void LLViewerFetchedTexture::scheduleCreateTexture() unref(); }); } - else if (!mGLTexturep->getHasExplicitFormat() && - mGLTexturep->getNeedsAlphaAndPickMask() && - mRawImage->getComponents() != 3) - { - auto main_queue = mMainQueue.lock(); - auto general_queue = LL::WorkQueue::getInstance("General"); - if (main_queue && general_queue) - { - LLPointer raw_image = mRawImage; - ref(); - const bool posted = main_queue->postTo( - general_queue, - [raw_image]() - { - return LLImageGL::prepareForUpload(raw_image); - }, - [this, raw_image](LLImageGL::TextureUploadPreparation preparation) - { - if (mNeedsCreateTexture) - { - if (mRawImage == raw_image) - { - mGLTexturep->applyUploadPreparation(std::move(preparation)); - } - if (!mCreatePending) - { - mCreatePending = true; - gTextureList.mCreateTextureList.push(this); - } - } - unref(); - }); - if (!posted) - { - unref(); - if (!mCreatePending) - { - mCreatePending = true; - gTextureList.mCreateTextureList.push(this); - } - } - } - else if (!mCreatePending) - { - mCreatePending = true; - gTextureList.mCreateTextureList.push(this); - } - } else { if (!mCreatePending) @@ -4159,3 +4108,4 @@ void LLTexturePipelineTester::LLTextureTestSession::reset() //---------------------------------------------------------------------------------------------- //end of LLTexturePipelineTester //---------------------------------------------------------------------------------------------- + -- cgit v1.3 From ac91515c115d0405a2dcb5438d1703bd9f624123 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:26:11 +0300 Subject: #6055 Optimize LLImageGL::analyzeAlpha --- indra/llrender/llimagegl.cpp | 150 +++++++++++++++++++++++++++++++++++-------- indra/llrender/llimagegl.h | 2 + 2 files changed, 127 insertions(+), 25 deletions(-) (limited to 'indra/llrender') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ff..8057149d73 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -541,6 +541,7 @@ void LLImageGL::init(bool usemipmaps, bool allow_compression) mIsMask = false; mNeedsAlphaAndPickMask = true ; + mAlphaAnalysisSerial = 0; mAlphaStride = 0 ; mAlphaOffset = 0 ; @@ -1951,6 +1952,9 @@ void LLImageGL::destroyGLTexture() mTexName = 0; mGLTextureCreated = false ; } + + // Invalidate pending jobs + ++mAlphaAnalysisSerial; } //force to invalidate the gl texture, most likely a sculpty texture @@ -2110,6 +2114,9 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask) { mAlphaOffset = INVALID_OFFSET ; mIsMask = false; + + // Invalidate pending jobs + ++mAlphaAnalysisSerial; } } } @@ -2187,11 +2194,16 @@ void LLImageGL::calcAlphaChannelOffsetAndStride() } } -void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) +bool LLImageGL::analyzeAlphaData( + const void* data_in, + U32 w, + U32 h, + S8 alpha_offset, + S8 alpha_stride) { - if(!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask) + if (!data_in) { - return ; + return false; } LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; @@ -2200,7 +2212,7 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) U32 alphatotal = 0; U32 sample[16]; - memset(sample, 0, sizeof(U32)*16); + memset(sample, 0, sizeof(U32) * 16); // generate histogram of quantized alpha. // also add-in the histogram of a 2x2 box-sampled version. The idea is @@ -2211,46 +2223,46 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) { llassert(w % 2 == 0); llassert(h % 2 == 0); - const GLubyte* rowstart = ((const GLubyte*) data_in) + mAlphaOffset; + const GLubyte* rowstart = ((const GLubyte*)data_in) + alpha_offset; for (U32 y = 0; y < h; y += 2) { - const GLubyte* current = rowstart; + const unsigned char* current = rowstart; for (U32 x = 0; x < w; x += 2) { const U32 s1 = current[0]; alphatotal += s1; - const U32 s2 = current[w * mAlphaStride]; + const U32 s2 = current[w * alpha_stride]; alphatotal += s2; - current += mAlphaStride; + current += alpha_stride; const U32 s3 = current[0]; alphatotal += s3; - const U32 s4 = current[w * mAlphaStride]; + const U32 s4 = current[w * alpha_stride]; alphatotal += s4; - current += mAlphaStride; + current += alpha_stride; - ++sample[s1/16]; - ++sample[s2/16]; - ++sample[s3/16]; - ++sample[s4/16]; + ++sample[s1 / 16]; + ++sample[s2 / 16]; + ++sample[s3 / 16]; + ++sample[s4 / 16]; - const U32 asum = (s1+s2+s3+s4); + const U32 asum = (s1 + s2 + s3 + s4); alphatotal += asum; - sample[asum/(16*4)] += 4; + sample[asum / (16 * 4)] += 4; } - rowstart += 2 * w * mAlphaStride; + rowstart += 2 * w * alpha_stride; } length *= 2; // we sampled everything twice, essentially } else { - const GLubyte* current = ((const GLubyte*) data_in) + mAlphaOffset; + const unsigned char* current = ((const unsigned char*)data_in) + alpha_offset; for (U32 i = 0; i < length; i++) { const U32 s1 = *current; alphatotal += s1; - ++sample[s1/16]; - current += mAlphaStride; + ++sample[s1 / 16]; + current += alpha_stride; } } @@ -2277,15 +2289,103 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) upperhalftotal += sample[i]; } - if (midrangetotal > length/48 || // lots of midrange, or - (lowerhalftotal == length && alphatotal != 0) || // all close to transparent but not all totally transparent, or - (upperhalftotal == length && alphatotal != 255*length)) // all close to opaque but not all totally opaque + if (midrangetotal > length / 48 || + (lowerhalftotal == length && alphatotal != 0) || + (upperhalftotal == length && alphatotal != 255 * length)) { - mIsMask = false; // not suitable for masking + return false; // not suitable for masking } else { - mIsMask = true; + return true; // is a mask + } +} + +void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) +{ + // if mNeedsAlphaAndPickMask is true, then offset and stride are supposed to be valid. + if (!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask) + return; + + // Already on a worker thread or a small image - analyze immediately + if (!on_main_thread() || (w < 64 && h < 64)) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + // Should have been incremented by destroyGLTexture, + // but increment either way, for extra safety. + ++mAlphaAnalysisSerial; + + mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride); + return; + } + + // On main thread - defer to worker thread + + // Capture context + const S8 alpha_offset = mAlphaOffset; + const S8 alpha_stride = mAlphaStride; + const U32 request_serial = ++mAlphaAnalysisSerial; + + // Copy data for worker thread + const size_t data_size = size_t(w) * size_t(h) * size_t(alpha_stride); + U8* data_copy = new (std::nothrow) U8[data_size]; + + if (!data_copy) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + mIsMask = analyzeAlphaData(data_in, w, h, alpha_offset, alpha_stride); + return; + } + memcpy(data_copy, static_cast(data_in), data_size); + + // Viewer can rapidly switch between lods, which would invalidate + // previous analysis results. + // Use a serial number to filter out obsolete analysis results. + + ref(); // Keep texture alive + + auto mainq = mMainQueue.lock(); + + // Get the GL thread queue + auto workerq = LLImageGLThread::sEnabledTextures ? + LL::WorkQueue::getInstance("LLImageGL") : // Use the image processing queue if available + LL::WorkQueue::getInstance("General"); // Fallback to general + + bool posted_job = false; + if (mainq && workerq) + { + posted_job = mainq->postTo( + workerq, + // Worker thread: analyze alpha + [data_copy, w, h, alpha_offset, alpha_stride]() -> bool + { + LL_PROFILE_ZONE_NAMED("Deffered alpha mask analysis"); + bool is_mask = LLImageGL::analyzeAlphaData(data_copy, w, h, alpha_offset, alpha_stride); + delete[] data_copy; + return is_mask; + }, + // Main thread: apply result + [this, request_serial](bool is_mask) + { + // Only apply if no newer analysis has been requested + if (mAlphaAnalysisSerial == request_serial) + { + mIsMask = is_mask; + } + unref(); + } + ); + + // Conservative default until analysis completes + mIsMask = false; + } + if (!posted_job) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + // Queues not available - fall back to synchronous analysis + delete[] data_copy; + mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride); + unref(); } } diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e..a98094de29 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -111,6 +111,7 @@ public: protected: virtual ~LLImageGL(); + static bool analyzeAlphaData(const void* data_in, U32 w, U32 h, S8 alpha_offset, S8 alpha_stride); void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); @@ -242,6 +243,7 @@ private: bool mIsMask; bool mNeedsAlphaAndPickMask; + LLAtomicU32 mAlphaAnalysisSerial; // for request tracking. S8 mAlphaStride ; S8 mAlphaOffset ; -- cgit v1.3