summaryrefslogtreecommitdiff
path: root/indra/llrender
diff options
context:
space:
mode:
authorMcGroarty <git@mcgroarty.me>2026-07-27 12:59:10 -0700
committerGitHub <noreply@github.com>2026-07-27 12:59:10 -0700
commit21939024cdee694bf8e98b79c3ba929ffc2fef5e (patch)
tree450fbbd27c07fdcc7236d1e782af0da7bb88a022 /indra/llrender
parentd1796fba2500a30bd3c71b0588efd18c1a8625bf (diff)
parent1c81813e79c4acc917d75b47117778dd277e5b1f (diff)
Merge pull request #1 from secondlife/develop
Fix UI issues, improve warnings, and enhance process handling
Diffstat (limited to 'indra/llrender')
-rw-r--r--indra/llrender/CMakeLists.txt5
-rw-r--r--indra/llrender/llgl.cpp9
-rw-r--r--indra/llrender/llimagegl.cpp309
-rw-r--r--indra/llrender/llimagegl.h17
-rw-r--r--indra/llrender/tests/llimagegl_prepare_test.cpp118
5 files changed, 346 insertions, 112 deletions
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/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/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 <unordered_set>
+#include <utility>
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<U32>(width) + 1) / 2;
+ const U32 pick_height = (static_cast<U32>(height) + 1) / 2;
+ result.mPickMaskPrepared = true;
+ result.mPickMaskWidth = static_cast<U16>(pick_width);
+ result.mPickMaskHeight = static_cast<U16>(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<TextureUploadPreparation>(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<const U8*>(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<U32>(pWidth) + 1) / 2;
+ U32 pick_height = (static_cast<U32>(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<U16>(pick_width);
+ mPickMaskHeight = static_cast<U16>(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 <memory>
#include <unordered_set>
+#include <vector>
#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<U8> 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<TextureUploadPreparation> 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<LLImageRaw> make_image(U16 width, U16 height, S8 components)
+ {
+ LLPointer<LLImageRaw> image = new LLImageRaw(width, height, components);
+ memset(image->getData(), 0, image->getDataSize());
+ return image;
+ }
+ };
+
+ typedef test_group<llimagegl_prepare_data> 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<LLImageRaw> 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<LLImageRaw> 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<LLImageRaw> 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<LLImageRaw> 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());
+ }
+}