From 1c81813e79c4acc917d75b47117778dd277e5b1f Mon Sep 17 00:00:00 2001 From: Lisa Scheers Date: Mon, 27 Jul 2026 21:35:52 +0200 Subject: #6055 Move texture upload preparation off the main thread (#6031) --- indra/llrender/llimagegl.cpp | 309 ++++++++++++++++++++++++++++--------------- 1 file changed, 204 insertions(+), 105 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') 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); } - -- 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/llimagegl.cpp') 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/llimagegl.cpp') 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