From b4de193e4ac48fee4f6254780221a1a0604d87f3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:27 -0400 Subject: [PATCH 01/38] A loading avatar's placeholder sphere hands back the G-buffer's colour mask With particles hidden, an avatar not yet fully loaded draws a sphere in pass 0 of the deferred avatar pool, and set the colour mask to (true, false) after it: the post-deferred convention, not the G-buffer's. Pass 0 runs for every avatar pool before pass 1 does, so every later avatar and animesh G-buffer draw, and the sky's, stopped writing alpha, which carries the normal buffer's flags. The sphere's mask is scoped now. Co-Authored-By: Claude Opus 5.5 --- indra/newview/lldrawpoolavatar.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indra/newview/lldrawpoolavatar.cpp b/indra/newview/lldrawpoolavatar.cpp index 8477faff59..7bd5309bad 100644 --- a/indra/newview/lldrawpoolavatar.cpp +++ b/indra/newview/lldrawpoolavatar.cpp @@ -752,7 +752,9 @@ void LLDrawPoolAvatar::renderAvatars(LLVOAvatar* single_avatar, S32 pass) { // debug code to draw a sphere in place of avatar gGL.getTextureSlot(0)->bindSampled(LLViewerFetchedTexture::sWhiteImagep, ALSamplers::AnisoWrap); - gGL.setColorMask(true, true); + // Scoped: pass 0 runs for every avatar pool before pass 1 does, and the G-buffer + // passes behind this one write their flags to alpha. + LLGLSColorMask mask(true, true); LLVector3 pos = avatarp->getPositionAgent(); gGL.color4f(1.0f, 1.0f, 1.0f, 0.7f); @@ -765,7 +767,6 @@ void LLDrawPoolAvatar::renderAvatars(LLVOAvatar* single_avatar, S32 pass) gSphere.renderGGL(); gGL.popMatrix(); - gGL.setColorMask(true, false); } // don't render please return; From 577e2474f3f3a7f4e7fb25ffb533b9425c0a539c Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:29 -0400 Subject: [PATCH 02/38] Avatar profiling renders with depth testing and depth writes on profileAvatar ran under whatever depth state its caller left. The Performance floater calls it from inside the UI draw, where LLGLSUIDefault turns depth testing and depth writes off, and the autotune caller runs from the main loop's work queue, outside display(), where neither is on. The G-buffer clear left depth alone and the opaque pools drew untested, so the GPU time it measured was not the avatar's. It sets the depth state display() gives a world render now, and a full colour mask for the clear, both scoped. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 77e2a970fe..cab8f1d76e 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -13473,6 +13473,14 @@ void LLPipeline::profileAvatar(LLVOAvatar* avatar, bool profile_attachments) // on a partially clipped draw, which is the one thing it must not be. LLGLDisable no_scissor(GL_SCISSOR_TEST); + // The depth and colour state display() gives a world render. No caller has it: the UI's + // LLGLSUIDefault turns depth testing and depth writes off, and the autotune caller runs + // from the main loop's work queue, outside display(), where neither is on. Without it + // the clear below leaves depth alone and the opaque pools draw untested, so the time + // measured is not the avatar's. + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + LLGLSColorMask color_mask(true, true); + // The globals generateImpostor overwrites are saved here rather than there, because the // display() path repairs them itself immediately afterwards and this path does not: it // returns into the middle of a UI draw. sCull in particular is left pointing at From 37c394e30ae32f235147d1e8273df1e2840f1ac3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:31 -0400 Subject: [PATCH 03/38] The scene monitor's sample count covers the draw it measures calcDiffAggregate ended its GL_SAMPLES_PASSED query right after gl_draw_scaled_target, whose quad was still batched in gGL -- LLRender::end() does not flush a small triangle batch -- and reached the GPU at the shader unbind, after the query had ended. Every count was 0, so SceneLoadingMonitorEnabled never saw the scene change. gGL is flushed before the query ends. The raw glColorMask pair around it, which went around gGL's colour-mask cache and turned every channel on afterwards, is a scoped LLGLSColorMask. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llscenemonitor.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index 82ef7fb967..893e097ee5 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -42,6 +42,8 @@ #include "llviewerparcelmgr.h" #include "llviewerpartsim.h" +#include + LLSceneMonitorView* gSceneMonitorView = NULL; // @@ -409,9 +411,10 @@ void LLSceneMonitor::calcDiffAggregate() } LLGLDepthTest depth(true, false, GL_ALWAYS); + std::optional no_color; if(!mDebugViewerVisible) { - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + no_color.emplace(false, false); } LLGLSLShader* cur_shader = NULL; @@ -429,6 +432,9 @@ void LLSceneMonitor::calcDiffAggregate() if(mDiffState == EXECUTE_DIFF) { + // The quad is still batched in gGL; the query only counts what reaches the GPU before + // it ends. + gGL.flush(); glEndQuery(GL_SAMPLES_PASSED); mDiffState = WAIT_ON_RESULT; } @@ -439,11 +445,6 @@ void LLSceneMonitor::calcDiffAggregate() { cur_shader->bind(); } - - if(!mDebugViewerVisible) - { - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - } #endif } From f821b04e9a62294c02c07d1dd1b54759c82da363 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:34 -0400 Subject: [PATCH 04/38] Accelerated media frames are bound through the texture slot On Linux, every accelerated media frame bound its EGL image's texture with a raw glBindTexture and then bound 0, on whatever unit was active. The slot went on believing its old texture, or the white placeholder, was bound there: its next bind of that texture was skipped and sampled texture 0, and unbind() on the emptied slot no longer put the white texture back. The import binds and unbinds through slot 0, and so does the macOS IOSurface import, which did the same on the rectangle target. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llcefaccelinterop.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp index 25a674ec0c..0cc13b38ad 100644 --- a/indra/newview/llcefaccelinterop.cpp +++ b/indra/newview/llcefaccelinterop.cpp @@ -256,6 +256,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) #elif LL_DARWIN // macOS: bind the shared IOSurface to a GL texture, blit to media #include "llgl.h" +#include "llrender.h" #include #include #include @@ -341,11 +342,13 @@ bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, i const int new_height = (int)IOSurfaceGetHeight(surf); CGLContextObj cgl = CGLGetCurrentContext(); - glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m->tex); + // Through the slot, so its record of what is bound stays true (see the Linux import). + ALTextureSlot* slot = gGL.getTextureSlot(0); + slot->bindManual(ALTextureSlot::TT_RECT_TEXTURE, m->tex); CGLError err = CGLTexImageIOSurface2D(cgl, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, new_width, new_height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surf, 0); - glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + slot->unbind(); if (err != kCGLNoError) { CFRelease(surf); @@ -403,6 +406,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) #elif LL_LINUX // import the plugin's dma-buf via EGL, blit to the media texture #include "llgl.h" +#include "llrender.h" #include #include #include @@ -592,9 +596,12 @@ bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, i l->image = new_image; l->width = width; l->height = height; - glBindTexture(GL_TEXTURE_2D, l->tex); + // Through the slot, so its record of what is bound stays true: a raw bind left it naming a + // texture this replaced, and its next bind of that texture was skipped. + ALTextureSlot* slot = gGL.getTextureSlot(0); + slot->bindManual(ALTextureSlot::TT_TEXTURE, l->tex); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, l->image); - glBindTexture(GL_TEXTURE_2D, 0); + slot->unbind(); return true; } From 82a2fc0e9f9384e4795e175f4052f91a64a24568 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:36 -0400 Subject: [PATCH 05/38] Recorded UI images and text lapse when the samplers are rebuilt A UI image's display list and the font text cache record slot 0's sampler as a GL name. Changing the anisotropy, or flipping the depth convention, deletes every sampler object and makes new ones, and the recordings went on binding the old names: dead ones, a GL_INVALID_OPERATION, or names GL had handed out again to a different sampler. LLRender's sampler generation, which counts those rebuilds and which nothing read, is part of the LLUIImage key and of the font cache's environment check now. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llfonttextcache.cpp | 6 +++++- indra/llrender/llfonttextcache.h | 1 + indra/llrender/lluiimage.cpp | 6 ++++-- indra/llrender/lluiimage.h | 12 ++++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/indra/llrender/llfonttextcache.cpp b/indra/llrender/llfonttextcache.cpp index b6636749fd..48786dfb23 100644 --- a/indra/llrender/llfonttextcache.cpp +++ b/indra/llrender/llfonttextcache.cpp @@ -76,13 +76,16 @@ namespace bool ALFontCacheKey::environmentMoved(const LLFontGL* fontp) { const U64 font_cache_gen = fontp ? fontp->getCacheGeneration() : 0; + // The capture keeps the sampler's GL name, which a sampler rebuild deletes. + const U32 sampler_gen = gGL.getSamplerGeneration(); if (mFont == fontp && mScaleX == LLFontGL::sScaleX && mScaleY == LLFontGL::sScaleY && mVertDPI == LLFontGL::sVertDPI && mHorizDPI == LLFontGL::sHorizDPI && mResGeneration == LLFontGL::sResolutionGeneration - && mFontCacheGen == font_cache_gen) + && mFontCacheGen == font_cache_gen + && mSamplerGen == sampler_gen) { return false; } @@ -94,6 +97,7 @@ bool ALFontCacheKey::environmentMoved(const LLFontGL* fontp) mHorizDPI = LLFontGL::sHorizDPI; mResGeneration = LLFontGL::sResolutionGeneration; mFontCacheGen = font_cache_gen; + mSamplerGen = sampler_gen; return true; } diff --git a/indra/llrender/llfonttextcache.h b/indra/llrender/llfonttextcache.h index 3556b9f781..ee62a5cc5b 100644 --- a/indra/llrender/llfonttextcache.h +++ b/indra/llrender/llfonttextcache.h @@ -146,6 +146,7 @@ class ALFontCacheKey F32 mHorizDPI = 0.f; S32 mResGeneration = 0; U64 mFontCacheGen = 0; + U32 mSamplerGen = 0; // 0 is never a generation, so the first ask records }; // Everything one piece of text costs to draw, kept so it can be reused: the diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index 892d7f4ee4..3d515f5e18 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -100,9 +100,11 @@ void LLUIImage::drawCached(S32 x, S32 y, S32 width, S32 height, const LLColor4& } // The GL name stands for the texture's current state: a discard change or a - // recreation renames it, and recordings made against the old name lapse. + // recreation renames it, and recordings made against the old name lapse. The sampler + // generation does the same for the sampler name the recording keeps. const LLVector3 ui_scale = gGL.getUIScale(); - const PackedKey key = PackedKey::create(width, height, color, solid_color, ui_scale, gl_image->getTexName()); + const PackedKey key = PackedKey::create(width, height, color, solid_color, ui_scale, gl_image->getTexName(), + gGL.getSamplerGeneration()); // Where the image sits on screen, in the units the recording's vertices are // in. It goes through the modelview rather than into the vertices, which is diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index e45aa7cc6d..e48ff18dba 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -135,7 +135,7 @@ class LLUIImage : public LLRefCount 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 scale; // UI scale (32 bits each for X and Y) - uint64_t tex_name; // OpenGL texture name (32 bits) + padding + uint64_t tex_name; // OpenGL texture name (low 32 bits) + sampler generation (high 32 bits) constexpr bool operator==(const PackedKey& other) const { @@ -164,7 +164,8 @@ class LLUIImage : public LLRefCount static constexpr PackedKey create(S32 width, S32 height, const LLColor4& color, bool solid_color, const LLVector3& scale, - LLGLuint texture_name) + LLGLuint texture_name, + U32 sampler_generation) { auto float_to_u8 = [](F32 f) -> uint8_t { return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); @@ -191,8 +192,11 @@ class LLUIImage : public LLRefCount uint64_t scl = (static_cast(float_to_bits(scale.mV[VX])) << 32) | static_cast(float_to_bits(scale.mV[VY])); - // Store full 32-bit texture name in lower 32 bits (upper 32 bits unused/zero) - uint64_t tex = static_cast(texture_name); + // A recording keeps the sampler's GL name, and rebuilding the samplers (an + // anisotropy change, a depth convention flip) deletes every name, so a recording + // made under an older generation would bind a dead or reused one. + uint64_t tex = (static_cast(sampler_generation) << 32) | + static_cast(texture_name); return PackedKey{ col, dim, scl, tex }; } From c0d5cc0b1c99e8be82b687f40d6aac8ce88b3b44 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:38 -0400 Subject: [PATCH 06/38] The octree debug view pops the matrices it pushed for a rigged mesh it skips When uploadMatrixPalette failed, as it does while a rigged mesh is still loading, renderOctree continued past both of the drawable's popMatrix calls. The modelview stack grew every frame until it overflowed at 32, and before that the caller's own pops restored matrices it had not pushed. Both are popped before the continue. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llspatialpartition.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp index 93690b2d5a..bf6077fd5f 100644 --- a/indra/newview/llspatialpartition.cpp +++ b/indra/newview/llspatialpartition.cpp @@ -1749,6 +1749,12 @@ void renderOctree(LLSpatialGroup* group) gGL.loadMatrix(LLViewerCamera::getCurrent().getModelview()); if (!LLRenderPass::uploadMatrixPalette(face->mAvatar, face->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) { + // Both pushes above are popped at the end of the iteration this skips. + gGL.popMatrix(); + if (!group->getSpatialPartition()->isBridge()) + { + gGL.popMatrix(); + } continue; } } From e55be249b1ca880e7738d6185c5d63147d34d96d Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:40 -0400 Subject: [PATCH 07/38] Physics shape outlines draw in front of their own fill The physics shapes view lays the shapes' depth down unoffset in pass 0 and draws their outlines in pass 2 with a polygon offset of +3, +3: away from the camera, so the depth test rejected the outline everywhere the fill covered and only the silhouette showed. The offset has applied since 6cdd9cff70 gave the LLGLEnable a name; before that it was a temporary destroyed on its own line. It is -3, -3 now, toward the camera (setPolygonOffset flips the sign under reverse-Z itself), and the colour mask the passes set is handed back to the caller instead of left at (true, false). Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index cab8f1d76e..19bcd95447 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -4991,8 +4991,14 @@ void LLPipeline::renderPhysicsDisplay() gGL.flush(); gDebugProgram.bind(); + // The passes below set their own masks; this hands the caller's back. + LLGLSColorMask color_mask(true, false); + + // Toward the camera: pass 0 lays the shapes' own depth down unoffset, and an outline + // pushed behind it fails the depth test everywhere the fill covers. setPolygonOffset + // flips the sign under reverse-Z itself. LLGLEnable polygon_offset_line(GL_POLYGON_OFFSET_LINE); - gGL.setPolygonOffset(3.f, 3.f); + gGL.setPolygonOffset(-3.f, -3.f); gGL.setLineWidth(3.f); LLGLEnable blend(GL_BLEND); gGL.setSceneBlendType(LLRender::BT_ALPHA); From 1c1adfe2540dfb44faf4db77ba3e3bc7b935e52e Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:42 -0400 Subject: [PATCH 08/38] Wireframe mode draws only the post-deferred pools as lines renderGeomPostDeferred switched to line mode for the whole pass. That took in the full-screen triangles drawn between the pools, the atmospheric and underwater haze, which then shaded only their edges, and the water exclusion prims, which marked only their outlines in the stencil, so water showed through them. The highlights, the debug displays and the far-plane overlay came after, some of them forcing fill back on partway. Line mode covers the pools' own draws alone now. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 19bcd95447..f91a48f939 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -4756,11 +4756,6 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL; LL_PROFILE_GPU_ZONE("renderGeomPostDeferred"); - if (gUseWireframe) - { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - } - U32 cur_type = 0; LLGLEnable cull(GL_CULL_FACE); @@ -4832,6 +4827,15 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) { LL_PROFILE_ZONE_NAMED_CATEGORY_DRAWPOOL("deferred poolrender"); + // Wireframe is for the pools' geometry alone. The passes run between pools above + // are full-screen triangles, which in line mode shade only their edges, and the + // water exclusion would stencil only the outlines of its prims. + if (gUseWireframe) + { + gGL.flush(); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + gGLLastMatrix = NULL; gGL.loadMatrix(LLViewerCamera::getCurrent().getModelview()); @@ -4857,6 +4861,12 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) LLGLState::checkStates(GL_FALSE); } } + + if (gUseWireframe) + { + gGL.flush(); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } } else { @@ -4894,11 +4904,6 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) ALFarPlaneOverlay::render(); } } - - if (gUseWireframe) - { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } } void LLPipeline::renderGeomShadow(LLCamera& camera) From b28cc59811f14e6bae831b4ea431a5e6e5d2a31d Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:45 -0400 Subject: [PATCH 09/38] The UI is drawn straight to the window, without the cached UI buffer RenderUIBuffer, off by default, redrew the UI into a screen-sized target only where views were dirty and composited the target over the world. It had decayed past use. The scissor test was on but the dirty rectangle was never applied to it, so the clear and the redraw were cut to whatever box was set last. The composite's texture coordinates were in UI units on a normalized texture, tiling it thousands of times. And no program was bound to draw the composite with, so its flush would hit LLRender's bound-shader assert on the first dirty frame. The path, its target and the setting are gone, and the dirty rectangle is always the window. Co-Authored-By: Claude Opus 5.5 --- indra/newview/app_settings/settings.xml | 11 ---- indra/newview/llviewercontrol.cpp | 7 -- indra/newview/llviewerdisplay.cpp | 64 +------------------ indra/newview/llviewerwindow.cpp | 5 +- indra/newview/pipeline.cpp | 13 ---- indra/newview/pipeline.h | 4 -- .../default/xui/pt/settings_comments.xml | 2 - 7 files changed, 2 insertions(+), 104 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 1888bc877c..4c90b8fe22 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -10234,17 +10234,6 @@ Value 0 - RenderUIBuffer - - Comment - Cache ui render in a screen aligned buffer. - Persist - 1 - Type - Boolean - Value - 0 - RenderUnloadedAvatar Comment diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 71f2838628..37e223d676 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -331,12 +331,6 @@ static bool handleShadowsResized(const LLSD& newvalue) return true; } -static bool handleWindowResized(const LLSD& newvalue) -{ - gPipeline.requestResizeScreenTexture(); - return true; -} - static bool handleReleaseGLBufferChanged(const LLSD& newvalue) { if (gPipeline.isInit()) @@ -1064,7 +1058,6 @@ void settings_setup_listeners() setting_setup_signal_listener(gSavedSettings, "OctreeAlphaDistanceFactor", handleRepartition); setting_setup_signal_listener(gSavedSettings, "OctreeAttachmentSizeFactor", handleRepartition); setting_setup_signal_listener(gSavedSettings, "RenderMaxTextureIndex", handleSetShaderChanged); - setting_setup_signal_listener(gSavedSettings, "RenderUIBuffer", handleWindowResized); setting_setup_signal_listener(gSavedSettings, "RenderDepthOfField", handleReleaseGLBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderFSAAType", handleReleaseGLBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderSMAAUseStencil", handleReleaseGLBufferChanged); diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 0ddfd97b78..ac15008c0b 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -1821,69 +1821,7 @@ void render_ui_2d() stop_glerror(); } - - if (LLPipeline::RenderUIBuffer) - { - if (LLView::sIsRectDirty) - { - LLView::sIsRectDirty = false; - LLRect t_rect; - - gPipeline.mUIScreen.bindTarget(); - gGL.setColorMask(true, true); - { - constexpr S32 pad = 8; - - LLView::sDirtyRect.mLeft -= pad; - LLView::sDirtyRect.mRight += pad; - LLView::sDirtyRect.mBottom -= pad; - LLView::sDirtyRect.mTop += pad; - - LLGLEnable scissor(GL_SCISSOR_TEST); - static LLRect last_rect = LLView::sDirtyRect; - - //union with last rect to avoid mouse poop - last_rect.unionWith(LLView::sDirtyRect); - - t_rect = LLView::sDirtyRect; - LLView::sDirtyRect = last_rect; - last_rect = t_rect; - - last_rect.mLeft = LLRect::tCoordType(last_rect.mLeft / LLUI::getScaleFactor().mV[0]); - last_rect.mRight = LLRect::tCoordType(last_rect.mRight / LLUI::getScaleFactor().mV[0]); - last_rect.mTop = LLRect::tCoordType(last_rect.mTop / LLUI::getScaleFactor().mV[1]); - last_rect.mBottom = LLRect::tCoordType(last_rect.mBottom / LLUI::getScaleFactor().mV[1]); - - LLRect clip_rect(last_rect); - - glClear(GL_COLOR_BUFFER_BIT); - - gViewerWindow->draw(); - } - - gPipeline.mUIScreen.flush(); - gGL.setColorMask(true, false); - - LLView::sDirtyRect = t_rect; - } - - LLGLDisable cull(GL_CULL_FACE); - LLGLDisable blend(GL_BLEND); - S32 width = gViewerWindow->getWindowWidthScaled(); - S32 height = gViewerWindow->getWindowHeightScaled(); - gGL.getTextureSlot(0)->bind(&gPipeline.mUIScreen); - gGL.begin(LLRender::TRIANGLE_STRIP); - gGL.color4f(1.f,1.f,1.f,1.f); - gGL.texCoord2f(0.f, 0.f); gGL.vertex2i(0, 0); - gGL.texCoord2f((F32)width, 0.f); gGL.vertex2i(width, 0); - gGL.texCoord2f(0.f, (F32)height); gGL.vertex2i(0, height); - gGL.texCoord2f((F32)width, (F32)height); gGL.vertex2i(width, height); - gGL.end(); - } - else - { - gViewerWindow->draw(); - } + gViewerWindow->draw(); // reset current origin for font rendering, in case of tiling render LLFontGL::sCurOrigin.set(0, 0); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 30ee15e6cf..f5e1717c8b 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -3030,10 +3030,7 @@ void LLViewerWindow::draw() //S32 screen_x, screen_y; - if (!LLPipeline::RenderUIBuffer) - { - LLView::sDirtyRect = getWindowRectScaled(); - } + LLView::sDirtyRect = getWindowRectScaled(); // HACK for timecode debugging if (gSavedSettings.getBOOL("DisplayTimecode")) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index f91a48f939..dddd116ea2 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -177,7 +177,6 @@ U32 LLPipeline::RenderResolutionDivisor; // [SL:KB] - Patch: Settings-RenderResolutionMultiplier | Checked: Catznip-5.4 F32 LLPipeline::RenderResolutionMultiplier; // [/SL:KB] -bool LLPipeline::RenderUIBuffer; S32 LLPipeline::RenderShadowDetail; S32 LLPipeline::RenderShadowSplits; bool LLPipeline::RenderDeferredSSAO; @@ -590,7 +589,6 @@ void LLPipeline::init() // [SL:KB] - Patch: Settings-RenderResolutionMultiplier | Checked: Catznip-5.4 connectRefreshCachedSettingsSafe("RenderResolutionMultiplier"); // [/SL:KB] - connectRefreshCachedSettingsSafe("RenderUIBuffer"); connectRefreshCachedSettingsSafe("RenderShadowDetail"); connectRefreshCachedSettingsSafe("RenderShadowSplits"); connectRefreshCachedSettingsSafe("RenderDeferredSSAO"); @@ -1038,14 +1036,6 @@ bool LLPipeline::allocateScreenBufferInternal(U32 resX, U32 resY) if (!gCubeSnapshot) // hack to not re-allocate various targets for cube snapshots { - if (RenderUIBuffer) - { - if (!mUIScreen.allocate(resX, resY, GL_RGBA8)) - { - return false; - } - } - if (RenderFSAAType > 0) { // SMAA benefits from a stencil buffer shared across its passes so the @@ -1234,7 +1224,6 @@ void LLPipeline::refreshCachedSettings() // [SL:KB] - Patch: Settings-RenderResolutionMultiplier | Checked: Catznip-5.4 RenderResolutionMultiplier = gSavedSettings.getF32("RenderResolutionMultiplier"); // [/SL:KB] - RenderUIBuffer = gSavedSettings.getBOOL("RenderUIBuffer"); RenderShadowDetail = gSavedSettings.getS32("RenderShadowDetail"); RenderShadowSplits = gSavedSettings.getS32("RenderShadowSplits"); RenderDeferredSSAO = gSavedSettings.getBOOL("RenderDeferredSSAO"); @@ -1378,8 +1367,6 @@ void LLPipeline::releaseGLBuffers() mFXAAMap.release(); mSMAABlendBuffer.release(); - mUIScreen.release(); - mDownResMap.release(); mPreviewMap.release(); diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h index 8e53f9bf15..0731c96719 100644 --- a/indra/newview/pipeline.h +++ b/indra/newview/pipeline.h @@ -907,9 +907,6 @@ class LLPipeline LLRenderTarget mFXAAMap; LLRenderTarget mSMAABlendBuffer; - // render ui to buffer target - LLRenderTarget mUIScreen; - // downres scratch space for GPU downscaling of textures LLRenderTarget mDownResMap; @@ -1325,7 +1322,6 @@ class LLPipeline // [SL:KB] - Patch: Settings-RenderResolutionMultiplier | Checked: Catznip-5.4 static F32 RenderResolutionMultiplier; // [/SL:KB] - static bool RenderUIBuffer; static S32 RenderShadowDetail; static S32 RenderShadowSplits; static bool RenderDeferredSSAO; diff --git a/indra/newview/skins/default/xui/pt/settings_comments.xml b/indra/newview/skins/default/xui/pt/settings_comments.xml index e2dec64506..f0a2486be5 100644 --- a/indra/newview/skins/default/xui/pt/settings_comments.xml +++ b/indra/newview/skins/default/xui/pt/settings_comments.xml @@ -2197,8 +2197,6 @@ Renderizar a água como transparente. Definir como falso renderiza a água como opaca com uma textura simples aplicada. RenderTreeLODFactor Controla o nível de detalhe da vegetação (multiplicador para a área de tela atual ao calcular o nível de detalhe). - RenderUIBuffer - Armazenar em cache a renderização da interface em um buffer alinhado à tela. RenderUIInSnapshot Exibir a interface do usuário na captura. RenderUnloadedAvatar From 5fd84081f0fa29498c5484778b3b70493dc6f323 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:47 -0400 Subject: [PATCH 10/38] Bump map generation hands back the colour mask Generating a brightness or darkness bump map set the colour mask to (true, true) and left it there. It also runs from the post-deferred bump pass, when a fullbright face's bump image is missing or its source changes resolution, and there the batches after it went on writing alpha -- BT_MULT_X2 doubled the glow alpha -- until a later pool set a mask of its own. The mask is scoped to the generation. Co-Authored-By: Claude Opus 5.5 --- indra/newview/lldrawpoolbump.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/newview/lldrawpoolbump.cpp b/indra/newview/lldrawpoolbump.cpp index d2ab4cd9d7..17c99f8aac 100644 --- a/indra/newview/lldrawpoolbump.cpp +++ b/indra/newview/lldrawpoolbump.cpp @@ -941,7 +941,9 @@ void LLBumpImageList::onSourceUpdated(LLViewerTexture* src, EBumpEffect bump_cod LLGLDepthTest depth(GL_FALSE); LLGLDisable cull(GL_CULL_FACE); LLGLDisable blend(GL_BLEND); - gGL.setColorMask(true, true); + // Scoped: this is reached from the post-deferred bump pass too, where the passes + // after it keep alpha writes off. + LLGLSColorMask mask(true, true); LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; gNormalMapGenProgram.bind(); From a60e2e59f70e0ac97a9099f7c16fbeb2b7d0a35d Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:49 -0400 Subject: [PATCH 11/38] A downscale larger than the scratch target takes the PBO path The FBO downscale draws into the target its caller bound, the texture list's 1024x1024 scratch, and copies the result back out of it. A texture wider or taller than 2048, at discard 0 under TextureLoadFullRes or a boost, dropped a level to a size the scratch could not hold, and glCopyTexSubImage2D filled about three quarters of the new texture with undefined pixels from outside the framebuffer. Such a downscale reads the level out of the texture's own mip chain through the PBO now. The PBO path reads a level that a texture without mips does not have, and uploaded whatever the scratch buffer last held. That was already so under RenderDownScaleMethod 1 and for deprecated source formats, which always take the PBO path. A downscale that would take it without mips is refused. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llimagegl.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 39a713e163..84b0ee3bd9 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -42,6 +42,7 @@ #include "llgl.h" #include "llglslshader.h" #include "llrender.h" +#include "llrendertarget.h" #include "llwindow.h" #include "llframetimer.h" #include @@ -3221,6 +3222,24 @@ bool LLImageGL::scaleDown(S32 desired_discard) S32 desired_width = getWidth(desired_discard); S32 desired_height = getHeight(desired_discard); + // A deprecated source format is held under a swizzle, and the FBO path cannot carry + // one: it samples the old texture THROUGH the swizzle and copies the result back by + // position, so luminance-alpha's alpha comes back as luminance and alpha-only loses + // its alpha entirely. The PBO path copies the stored channels as they are. + // + // The FBO path also draws into the target the caller bound -- the texture list's + // 1024x1024 scratch -- and copies back out of it, so a result larger than that target + // copies undefined pixels from outside the framebuffer. Such a texture takes the PBO + // path, which reads the level out of the texture's own mip chain; without one there + // is nothing it can read, however it came to that path, so the downscale is refused. + const bool fits_target = desired_width <= (S32)LLRenderTarget::sCurResX + && desired_height <= (S32)LLRenderTarget::sCurResY; + const bool use_fbo = gGLManager.mDownScaleMethod == 0 && mDeprecatedSourceFormat == 0 && fits_target; + if (!use_fbo && !mHasMipMaps) + { + return false; + } + // Downscale into a NEW texture object rather than reallocating this one in place. // glTexImage2D on the live name is illegal once the texture has immutable storage // (GL_INVALID_OPERATION, silently leaving it at the old size), and it has no D3D11 @@ -3235,12 +3254,6 @@ bool LLImageGL::scaleDown(S32 desired_discard) return false; } - // A deprecated source format is held under a swizzle, and the FBO path cannot carry - // one: it samples the old texture THROUGH the swizzle and copies the result back by - // position, so luminance-alpha's alpha comes back as luminance and alpha-only loses - // its alpha entirely. The PBO path copies the stored channels as they are. - const bool use_fbo = gGLManager.mDownScaleMethod == 0 && mDeprecatedSourceFormat == 0; - if (use_fbo) { // use an FBO to downscale the texture glViewport(0, 0, desired_width, desired_height); From 39a9dc2fe69abef44175c573ac798b80888d5248 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:51 -0400 Subject: [PATCH 12/38] The material swatch writes its alpha LLGLTFPreviewTexture::render drew under whatever colour mask the last frame ended with, which is (true, false) after a HUD pass, the translate manipulator or a UI redraw. Then nothing in the swatch's chain wrote alpha, from its clears to the final blit, and the swatch showed whatever alpha the preview target last held over its checkerboard. The render scopes a full mask now. Its second clear also asked for depth inside the render's own depth-writes-off scope, where it could not write it; it clears colour only. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llgltfmaterialpreviewmgr.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index 3df66b433f..8c51529a2d 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -428,6 +428,11 @@ bool LLGLTFPreviewTexture::render() if (!mShouldRender) { return false; } + // The swatch's alpha is its coverage, drawn over a checkerboard. Without this the + // preview takes whatever mask the last frame ended on, and after a HUD pass that leaves + // alpha unwritten from the clears through the final blit. + LLGLSColorMask color_mask(true, true); + gGL.setClearColor(LLColor4::transparent); gGL.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -507,7 +512,9 @@ bool LLGLTFPreviewTexture::render() // Alpha blend rendering screen.bindTarget(); - gGL.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // Colour only: the sphere draws with the depth test off, and depth writes are off + // with it, so a depth clear here would do nothing. + gGL.clear(GL_COLOR_BUFFER_BIT); LLGLSLShader& shader = *gDeferredPBRAlphaProgram.selectVariant(); From 513af65e7b917e33bd240ac89294734446ac2909 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:53 -0400 Subject: [PATCH 13/38] The material swatch is tonemapped under HDR Under RenderHDREnabled the swatch's colorCorrect writes its result to postPingMap, but the final blit sampled the screen target, so the swatch showed the untonemapped linear scene. Without HDR the glow combine writes the result back to the screen, which is why that path looked right. The blit reads whichever target holds the end of the chain now. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llgltfmaterialpreviewmgr.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index 8c51529a2d..3245614ccf 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -565,7 +565,10 @@ bool LLGLTFPreviewTexture::render() gDeferredPostNoDoFProgram.bind(); // From LLPipeline::renderFinalize: "Whatever is last in the above post processing chain should _always_ be rendered directly here. If not, expect problems." - gDeferredPostNoDoFProgram.bindTexture(LLShaderMgr::DEFERRED_DIFFUSE, &screen); + // Under HDR that is colorCorrect's output in postPingMap; otherwise the glow combine has + // written the result back to the screen. + LLRenderTarget* last_in_chain = hdr ? &gPipeline.mRT->postPingMap : &screen; + gDeferredPostNoDoFProgram.bindTexture(LLShaderMgr::DEFERRED_DIFFUSE, last_in_chain); gDeferredPostNoDoFProgram.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &gPipeline.mAuxillaryRT.deferredScreen); { From fdafb75da7d5046e36a0e085515d7630316c8bcf Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:56 -0400 Subject: [PATCH 14/38] The cross-screen filter's passes are separated by a texture barrier Passes 1 and 2 of each arm sample the quadrant of mWaterDis the pass before wrote, while mWaterDis is still attached to the bound framebuffer. GL 4.5 (9.3.1) defines that read only across glTextureBarrier, which was loaded and never called; rebinding the framebuffer between passes happens to flush on real drivers. The barrier is issued before those passes where it is available. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index dddd116ea2..fa66f14fbf 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -9228,6 +9228,16 @@ void LLPipeline::generateBloomHDR(LLRenderTarget* src) 1.f / (F32)streak_h); gCrossFilterProgram.uniform1f(LLShaderMgr::CROSS_PASS_SCALE, scales[pass]); + // Passes 1 and 2 sample a quadrant the previous draw wrote while + // mWaterDis is still attached to the bound framebuffer, a read GL + // defines only across a texture barrier. The same barriers put each + // arm's reads of a scratch quadrant before the next arm's write to it. + // The entry point is loaded only on GL 4.5 and up. + if (src == &mWaterDis && glTextureBarrier) + { + glTextureBarrier(); + } + mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); }; From d930a035f845f8886e93767b50a5243912898352 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:15:58 -0400 Subject: [PATCH 15/38] Sun shadow rendering hands back the colour mask on every exit generateSunShadow turned colour writes off at the top and back on at the bottom, and its early return for an empty point cloud, nothing in view to fit the shadow to, skipped the second half. Both of its callers set (true, true) just before calling it, so a scoped mask restores the same thing, on every exit. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index fa66f14fbf..8eb3ffd172 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -12629,7 +12629,9 @@ void LLPipeline::generateSunShadow(LLCamera& camera) LLPipeline::RENDER_TYPE_PASS_GLTF_PBR_ALPHA_MASK_RIGGED, END_RENDER_TYPES); - gGL.setColorMask(false, false); + // The caller's mask comes back on every exit, the early one for an empty point cloud + // included. + LLGLSColorMask color_mask(false, false); LLEnvironment& environment = LLEnvironment::instance(); @@ -13415,7 +13417,6 @@ void LLPipeline::generateSunShadow(LLCamera& camera) gGL.loadMatrix(proj[1]); gGL.matrixMode(LLRender::MM_MODELVIEW); } - gGL.setColorMask(true, true); gGLViewport[0] = saved_gl_viewport[0]; gGLViewport[1] = saved_gl_viewport[1]; From 8b18be586fed8b00f3cfee36447bf88f866e1a63 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:00 -0400 Subject: [PATCH 16/38] The haze passes hand back the colour mask doAtmospherics and doWaterHaze set the colour mask to (true, true) for the haze and left it there, inside renderGeomPostDeferred, which had set (true, false). The pools after them set their own, but the debug displays at the end of the pass could inherit it. Both are scoped. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 8eb3ffd172..70cb8bbdd9 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -11574,7 +11574,8 @@ void LLPipeline::doAtmospherics() LLGLEnable blend(GL_BLEND); gGL.blendFunc(LLRender::BF_ONE, LLRender::BF_SOURCE_ALPHA, LLRender::BF_ZERO, LLRender::BF_SOURCE_ALPHA); - gGL.setColorMask(true, true); + // The caller's mask comes back for the pools drawn after the haze. + LLGLSColorMask color_mask(true, true); // apply haze LLGLSLShader& haze_shader = *gHazeProgram.selectVariant(); @@ -11639,7 +11640,8 @@ void LLPipeline::doWaterHaze() LLGLEnable blend(GL_BLEND); gGL.blendFunc(LLRender::BF_ONE, LLRender::BF_SOURCE_ALPHA, LLRender::BF_ZERO, LLRender::BF_SOURCE_ALPHA); - gGL.setColorMask(true, true); + // The caller's mask comes back for the pools drawn after the haze. + LLGLSColorMask color_mask(true, true); // apply haze LLGLSLShader& haze_shader = gHazeWaterProgram; From f405e6ebe96b280bd2c4b3ef16ce96e1b344d525 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:02 -0400 Subject: [PATCH 17/38] Occlusion queries hand back the colour mask instead of forcing it on doOcclusion turned colour writes off for its two query passes and set (true, true) by hand after each. Its one live caller, renderGeomDeferred, runs at (true, true), so the value was right by coincidence. Both passes scope the mask. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 70cb8bbdd9..f53255143e 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3250,7 +3250,7 @@ void LLPipeline::doOcclusion(LLCamera& camera) if (sReflectionProbesEnabled && sUseOcclusion > 1 && !LLPipeline::sShadowRender && !gCubeSnapshot) { - gGL.setColorMask(false, false); + LLGLSColorMask color_mask(false, false); LLGLDepthTest depth(GL_TRUE, GL_FALSE); LLGLDisable cull(GL_CULL_FACE); @@ -3265,8 +3265,6 @@ void LLPipeline::doOcclusion(LLCamera& camera) mReflectionMapManager.doOcclusion(); mHeroProbeManager.doOcclusion(); gOcclusionCubeProgram.unbind(); - - gGL.setColorMask(true, true); } if (LLPipeline::sUseOcclusion > 1 && @@ -3274,7 +3272,7 @@ void LLPipeline::doOcclusion(LLCamera& camera) { LLVertexBuffer::unbind(); - gGL.setColorMask(false, false); + LLGLSColorMask color_mask(false, false); LLGLDisable blend(GL_BLEND); gGL.getTextureSlot(0)->unbind(); @@ -3310,8 +3308,6 @@ void LLPipeline::doOcclusion(LLCamera& camera) vo_part->processOccluders(&camera); } } - - gGL.setColorMask(true, true); } } From 71b0552d8525bd908262133787fd688a84a5be51 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:04 -0400 Subject: [PATCH 18/38] SMAA's stencil clear writes every bit, whatever the last frame left The edge-detect pass cleared its stencil before setting glStencilMask, so the clear relied on the frame before having left the mask at 0xFF. Every writer does today; the mask is set before the clear regardless. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index f53255143e..04e25c3d55 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -9512,6 +9512,11 @@ void LLPipeline::generateSMAABuffers(LLRenderTarget* src) LLGLSLShader& edge_shader = gSMAAEdgeDetectProgram[fsaa_quality]; dest.bindTarget(); + if (use_stencil) + { + // The stencil clear writes only the bits the stencil write mask allows. + glStencilMask(0xFF); + } dest.clear(use_stencil ? (GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) : GL_COLOR_BUFFER_BIT); edge_shader.bind(); From d4c7a9a59acdbe625c655ff49f9cc097cf780d3b Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:06 -0400 Subject: [PATCH 19/38] The HUD pass skips the water exclusion mask renderGeomPostDeferred skipped atmospherics and water haze on the HUD pass but built the water exclusion mask there every frame, under the HUD camera, from whatever the exclusion pool still held of the world. Nothing read it: the water pool and water haze do not run on the HUD pass, and every pass that runs them builds the mask first. It is skipped like the other two. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 04e25c3d55..0498040137 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -4745,7 +4745,9 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) bool done_atmospherics = LLPipeline::sRenderingHUDs; //skip atmospherics on huds bool done_water_haze = done_atmospherics; - bool done_water_exclusion = false; + // and the water exclusion mask: the water pool and water haze, which read it, do not run + // on huds, and every pass that runs them builds the mask first + bool done_water_exclusion = LLPipeline::sRenderingHUDs; // do water exclusion just before water pass. U32 water_exclusion_pass = LLDrawPool::POOL_WATEREXCLUSION; From 46565dfbde48f9eb9e782da94dee3cdf32368c09 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:08 -0400 Subject: [PATCH 20/38] LLRender::refreshState puts the cached colour mask, blend and line width back refreshState is for GL state changed behind gGL's back, but it re-asserted the colour mask through setColorMask with the values already cached, so the setter's comparison skipped the GL call. It issues the colour mask, the blend factors (once any have been set) and the line width directly now, as it already did the polygon offset, the clear colour and the scissor. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llrender.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 8b65316277..bd0202d2ce 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -526,7 +526,7 @@ void LLRender::refreshState(void) mDirty = true; // Called when GL state may have been changed behind our back, so re-assert the shared - // blocks' bindings along with the texture units and colour mask. + // blocks' bindings along with the texture units and the state the setters cache. mLightsUBOBound = false; mMatricesUBOBound = false; @@ -539,10 +539,23 @@ void LLRender::refreshState(void) mTextureSlots[active_unit].activate(); - setColorMask(mCurrColorMask[0], mCurrColorMask[1], mCurrColorMask[2], mCurrColorMask[3]); + // The cached state below is issued straight to GL: each setter would see its cache + // agreeing with the value asked for and skip the call. + glColorMask(mCurrColorMask[0] ? GL_TRUE : GL_FALSE, + mCurrColorMask[1] ? GL_TRUE : GL_FALSE, + mCurrColorMask[2] ? GL_TRUE : GL_FALSE, + mCurrColorMask[3] ? GL_TRUE : GL_FALSE); + + // Blend factors still unknown since init have nothing to put back. + if (mCurrBlendColorSFactor != BF_UNDEF && mCurrBlendColorDFactor != BF_UNDEF && + mCurrBlendAlphaSFactor != BF_UNDEF && mCurrBlendAlphaDFactor != BF_UNDEF) + { + glBlendFuncSeparate(sGLBlendFactor[mCurrBlendColorSFactor], sGLBlendFactor[mCurrBlendColorDFactor], + sGLBlendFactor[mCurrBlendAlphaSFactor], sGLBlendFactor[mCurrBlendAlphaDFactor]); + } + + glLineWidth(mLineWidth); - // Unconditional re-issue: setPolygonOffset would see the cache already agreeing with the - // requested value and skip the GL call, leaving the fresh context at its 0,0 default. rebasePolygonOffset(); // The same for the clear colour and a scissor box that was ever set. From 7db2fcd78e39fe762f519f700452418476f7612e Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:11 -0400 Subject: [PATCH 21/38] The colour mask and line width setters flush only when the value changes setColorMask and setLineWidth flushed gGL before comparing with the cache, so every redundant call split the batch: LLViewBorder alone sets a line width of 1 after every border. They flush in the changed branch now, as setPolygonOffset and setScissor do. The callers were checked for any that relied on the flush. A bounding box's pulsing outline could take the next drawable's colour, a uniform, before it was drawn, and the octree raycast view's triangles relied on a depth-state change to flush them; both flush for themselves. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llrender.cpp | 7 +++---- indra/newview/llspatialpartition.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index bd0202d2ce..610d29518d 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -1017,13 +1017,13 @@ void LLRender::setColorMask(bool writeColor, bool writeAlpha) void LLRender::setColorMask(bool writeColorR, bool writeColorG, bool writeColorB, bool writeAlpha) { - flush(); - if (mCurrColorMask[0] != writeColorR || mCurrColorMask[1] != writeColorG || mCurrColorMask[2] != writeColorB || mCurrColorMask[3] != writeAlpha) { + flush(); + mCurrColorMask[0] = writeColorR; mCurrColorMask[1] = writeColorG; mCurrColorMask[2] = writeColorB; @@ -1683,11 +1683,10 @@ void LLRender::diffuseColor4ub(U8 r, U8 g, U8 b, U8 a) void LLRender::setLineWidth(F32 width) { - gGL.flush(); - width = llclamp(width, gGLManager.mAliasedLineRange[0], gGLManager.mAliasedLineRange[1]); if(mLineWidth != width) { + flush(); mLineWidth = width; glLineWidth(width); } diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp index bf6077fd5f..ee6232a94f 100644 --- a/indra/newview/llspatialpartition.cpp +++ b/indra/newview/llspatialpartition.cpp @@ -1928,6 +1928,10 @@ void renderUpdateType(LLDrawable* drawablep) void renderBoundingBox(LLDrawable* drawable, bool set_color = true) { + // The colour is a uniform, so the last drawable's outlines go out under their own colour + // before it changes. + gGL.flush(); + if (set_color) { if (drawable->isSpatialBridge()) @@ -2955,6 +2959,8 @@ class LLRenderOctreeRaycast : public LLOctreeTriangleRayIntersect gGL.vertex3fv(tri->mV[2]->getF32ptr()); } gGL.end(); + // Drawn under this pass's depth func and colour, which the next pass changes. + gGL.flush(); if (i == 1) { From 499d4251d4986134823a3bfd7ebdff92126dda4c Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:13 -0400 Subject: [PATCH 22/38] Binding a cube map that is already bound still sets the pass's sampler ALTextureSlot::bind(LLCubeMap*) applied the caller's sampler only when the texture name changed, unlike every other bind, which sets it outside the redundancy check. Rebinding the same cube after a pass that left another sampler on the slot sampled through that one. bindSampler skips a redundant bind itself. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/altextureslot.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/indra/llrender/altextureslot.cpp b/indra/llrender/altextureslot.cpp index 78d7cfa814..fda1447229 100644 --- a/indra/llrender/altextureslot.cpp +++ b/indra/llrender/altextureslot.cpp @@ -390,12 +390,6 @@ bool ALTextureSlot::bind(LLCubeMap* cubeMap, ALSampler key) mCurrTexture = cubeMap->mImages[0]->getTexName(); glBindTexture(GL_TEXTURE_CUBE_MAP, mCurrTexture); cubeMap->mImages[0]->updateBindStats(); - // Named by the caller, not read off face 0. A cube map is as much a shared - // resource as any other texture -- the environment map is sampled by every - // shiny surface in the scene -- so which sampler a pass wants is the pass's - // business. - bindSampler(gGL.getSampler(key)); - return true; } else { @@ -403,6 +397,13 @@ bool ALTextureSlot::bind(LLCubeMap* cubeMap, ALSampler key) return false; } } + + // Named by the caller, not read off face 0. A cube map is as much a shared + // resource as any other texture -- the environment map is sampled by every + // shiny surface in the scene -- so which sampler a pass wants is the pass's + // business. Outside the redundancy check, as in bindImpl: the same cube can be + // bound again after a pass that left a different sampler on this slot. + bindSampler(gGL.getSampler(key)); return true; } From bd328e4e76d6759af6d50740b643e75b91afcdfc Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:15 -0400 Subject: [PATCH 23/38] A bound shader program is unbound before it is deleted unloadInternal deleted the GL program without looking at whether it was bound, leaving sCurBoundShader and sCurBoundShaderPtr naming it; for an owned variant freed straight after, the pointer dangled, and the next bind() read a profile query through it. A bound program is unbound first now, and the destructor clears the statics if they name it. unload() resets the program's features after unloadInternal rather than before, since the unbind ends the profile queries by the features they were begun with. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llglslshader.cpp | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index f1bf366e05..bf600e4863 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -484,6 +484,14 @@ LLGLSLShader::~LLGLSLShader() // than a leak. Recursion covers the subtree: each child erases itself on the way out. sInstances.erase(this); + // Nor may the bound-program statics name this object. They are cleared without a GL call, + // for the reason below. + if (sCurBoundShaderPtr == this) + { + sCurBoundShaderPtr = nullptr; + sCurBoundShader = 0; + } + // Free the owned subtree's OBJECTS, so a program destroyed without unload() does not leak // them. Deliberately not unload(): a global program's destructor runs at static destruction // with no GL context, where deleting program objects is undefined -- and the driver reclaims @@ -644,20 +652,29 @@ bool LLGLSLShader::createVariant(EVariant axis) void LLGLSLShader::unload() { + freeOwnedVariants(); + + unloadInternal(); + + // After unloadInternal: unbinding a bound program ends its profile queries by the + // features they were begun with. mShaderFiles.clear(); mDefines.clear(); mPermutationsAdded = false; mFeatures = LLShaderFeatures(); - - freeOwnedVariants(); - - unloadInternal(); } void LLGLSLShader::unloadInternal() { sInstances.erase(this); + // A bound program is unbound before it is deleted, so the bound-program statics never + // name a dead program, or an owned variant freed right after this. + if (sCurBoundShaderPtr == this || (mProgramObject != 0 && sCurBoundShader == mProgramObject)) + { + unbind(); + } + stop_glerror(); mAttribute.clear(); mTexture.clear(); From 8d6b5bcf3c8486e97ee6cb5c12c8b577de3ddd57 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:17 -0400 Subject: [PATCH 24/38] Unloading a shader deletes its primitives query placeProfileQuery generates the timer, samples and primitives queries together, and unloadInternal deleted only the first two, so every reload of a profiled program leaked a query object. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llglslshader.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index bf600e4863..2d31da1961 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -731,6 +731,12 @@ void LLGLSLShader::unloadInternal() mSamplesQuery = 0; } + if (mPrimitivesQuery) + { + glDeleteQueries(1, &mPrimitivesQuery); + mPrimitivesQuery = 0; + } + stop_glerror(); } From 74eadb4c33a5a567425e5c98c3c7935b2fa15024 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:19 -0400 Subject: [PATCH 25/38] Render target binds, clears, blits and draw-buffer changes flush gGL first LLRender::end() leaves small line, triangle and point batches pending, and bindTarget, clear, copyContentsToFramebuffer, setDrawBuffers and setDrawOutput changed the framebuffer or its attachments without flushing them. A batch could land in the newly bound target, after the clear, or on attachments selected after it was drawn. Each flushes first, as copyContents and flush already did. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llrendertarget.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 0b175e2265..dac14f36d7 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -536,6 +536,8 @@ void LLRenderTarget::release() void LLRenderTarget::bindTarget() { LL_PROFILE_GPU_ZONE("bindTarget"); + // What gGL has batched belongs to the framebuffer bound before this one. + gGL.flush(); llassert(mFBO); llassert(!isBoundInStack()); @@ -571,6 +573,8 @@ void LLRenderTarget::bindTarget() void LLRenderTarget::clear(U32 mask_in) { LL_PROFILE_GPU_ZONE("clear"); + // What gGL has batched is drawn before the clear, not after it. + gGL.flush(); llassert(mFBO); U32 mask = GL_COLOR_BUFFER_BIT; if (mUseDepth) @@ -750,6 +754,9 @@ void LLRenderTarget::copyContents(LLRenderTarget& source, S32 srcX0, S32 srcY0, void LLRenderTarget::copyContentsToFramebuffer(LLRenderTarget& source, S32 srcX0, S32 srcY0, S32 srcX1, S32 srcY1, S32 dstX0, S32 dstY0, S32 dstX1, S32 dstY1, U32 mask, U32 filter) { + // What gGL has batched is drawn before the blit reads or overwrites it. + gGL.flush(); + if (!source.mFBO) { LL_WARNS() << "Cannot copy framebuffer contents for non FBO render targets." << LL_ENDL; @@ -808,6 +815,8 @@ void LLRenderTarget::setDrawBuffers(U32 count) const U32 wanted = (count == 0) ? attachments : llmin(count, attachments); + // What gGL has batched goes to the attachments selected when it was drawn. + gGL.flush(); glDrawBuffers(static_cast(wanted), drawbuffers); } @@ -822,6 +831,7 @@ void LLRenderTarget::setDrawOutput(U32 output) output = llmin(output, (U32)LL_ARRAY_SIZE(drawbuffers) - 1); drawbuffers[output] = GL_COLOR_ATTACHMENT0; + gGL.flush(); glDrawBuffers(static_cast(output + 1), drawbuffers); } From 5f397a749c28495024b78d87b4bbb0b675ca2b83 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:21 -0400 Subject: [PATCH 26/38] LLGLState knows the caps LLRender::init enables raw LLGLState takes a cap it has not seen for disabled, but LLRender::init enables GL_TEXTURE_CUBE_MAP_SEAMLESS with a raw glEnable, and debug output can be on raw too. An LLGLDisable of one of them would have taken it for off already and done nothing. Those caps are recorded as they are set, through LLGLState::seedState, on the main thread only: worker threads run LLRender::init on contexts of their own, which the map does not describe. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llgl.h | 4 ++++ indra/llrender/llrender.cpp | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index f403991b99..703d62ff25 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -319,6 +319,10 @@ class LLGLState static void initClass(); static void restoreGL(); + // Record a cap set with a raw glEnable/glDisable, so a scoped state does not take it for + // GL's default. The map describes the main context; call on the main thread only. + static void seedState(LLGLenum state, LLGLboolean enabled) { sStateMap[state] = enabled; } + static void resetTextureStates(); static void dumpStates(); diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 610d29518d..6f065c6209 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -39,6 +39,7 @@ #include "hbxxh.h" #include "alprojection.h" #include "llcamera.h" +#include "llthread.h" #include @@ -352,6 +353,10 @@ LLRender::~LLRender() bool LLRender::init(bool needs_vertex_buffer) { + // LLGLState's map describes the main context. A worker thread's shared context keeps its + // own caps, and the map is not the worker's to write. + const bool main_context = on_main_thread(); + #if GL_ARB_debug_output && !LL_DARWIN if (gGLManager.mHasDebugOutput && gDebugGL) { //setup debug output callback @@ -359,6 +364,13 @@ bool LLRender::init(bool needs_vertex_buffer) glDebugMessageCallback((GLDEBUGPROC) gl_debug_callback, NULL); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); } + if (main_context && gGLManager.mHasDebugOutput) + { + // Both caps are set raw: above, by a debug context's default, or by whoever made the + // context before this ran. They are read back rather than assumed. + LLGLState::seedState(GL_DEBUG_OUTPUT, glIsEnabled(GL_DEBUG_OUTPUT)); + LLGLState::seedState(GL_DEBUG_OUTPUT_SYNCHRONOUS, glIsEnabled(GL_DEBUG_OUTPUT_SYNCHRONOUS)); + } #endif glPixelStorei(GL_PACK_ALIGNMENT, 1); @@ -422,6 +434,10 @@ bool LLRender::init(bool needs_vertex_buffer) // necessary for reflection maps glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + if (main_context) + { + LLGLState::seedState(GL_TEXTURE_CUBE_MAP_SEAMLESS, GL_TRUE); + } #if LL_WINDOWS if (glGenVertexArrays == nullptr) From 33f639c2339749e3b571aea90ee237f0edd7252a Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:23 -0400 Subject: [PATCH 27/38] LLGLState and LLGLDepthTest start each GL context from its defaults LLGLState's map and LLGLDepthTest's statics are process-wide and outlived the context they described. initClass, which runs for each new context, clears the map now and resets the depth state to GL's defaults, issuing the depth func through the latched convention so GL agrees with it. The GL tests, which make a context per test, no longer need rebase() to undo the last one. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llgl.cpp | 14 ++++++++++++++ indra/llrender/llglstates.h | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 14279aa2f0..27cc503d37 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -3292,6 +3292,11 @@ GLboolean LLGLDepthTest::sWriteEnabled = GL_TRUE; // OpenGL default //static void LLGLState::initClass() { + // Runs per context, from initGL: the map and the depth state describe this context, and + // GL starts every context at its defaults. + sStateMap.clear(); + LLGLDepthTest::initClass(); + sStateMap[GL_DITHER] = GL_TRUE; // sStateMap[GL_TEXTURE_2D] = GL_TRUE; @@ -3677,6 +3682,15 @@ GLenum LLGLDepthTest::remap(GLenum func) } } +//static +void LLGLDepthTest::initClass() +{ + sDepthEnabled = GL_FALSE; + sDepthFunc = GL_LESS; + sWriteEnabled = GL_TRUE; + glDepthFunc(remap(sDepthFunc)); +} + void LLGLDepthTest::rebase() { // Force the physical depth func to agree with the current translation of the tracked diff --git a/indra/llrender/llglstates.h b/indra/llrender/llglstates.h index c3b28548c0..71f9979f66 100644 --- a/indra/llrender/llglstates.h +++ b/indra/llrender/llglstates.h @@ -42,6 +42,11 @@ class LLGLDepthTest void checkState(); + // Take the tracked state back to a fresh context's: test off, writes on, GL_LESS, with the + // func issued through remap so GL agrees under whichever convention is latched. Run per + // context by LLGLState::initClass. + static void initClass(); + // Reverse-Z depth-func translation. The tracked state (sDepthFunc) and every call // site stay in the forward/semantic convention; the physical glDepthFunc is fed the // translated func so a call site asking for "nearer wins" keeps that meaning under From 9eab3094d2a9f37a1f22e2ff07da0a69c865bfdb Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:25 -0400 Subject: [PATCH 28/38] LLGLState and LLGLDepthTest cannot be copied A copy would restore its state a second time when destroyed. Both delete copying, as LLGLSColorMask, LLGLSScissor and LLGLStencil already do; nothing copied either. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llgl.h | 3 +++ indra/llrender/llglstates.h | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index 703d62ff25..e2588dbff0 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -338,6 +338,9 @@ class LLGLState enum { CURRENT_STATE = -2, DISABLED_STATE = 0, ENABLED_STATE = 1 }; LLGLState(LLGLenum state, S32 enabled = CURRENT_STATE); ~LLGLState(); + // A copy would restore the cap a second time. + LLGLState(const LLGLState&) = delete; + LLGLState& operator=(const LLGLState&) = delete; void setEnabled(S32 enabled); void enable() { setEnabled(ENABLED_STATE); } void disable() { setEnabled(DISABLED_STATE); } diff --git a/indra/llrender/llglstates.h b/indra/llrender/llglstates.h index 71f9979f66..3bf4411d26 100644 --- a/indra/llrender/llglstates.h +++ b/indra/llrender/llglstates.h @@ -40,6 +40,10 @@ class LLGLDepthTest ~LLGLDepthTest(); + // A copy would restore the depth state a second time. + LLGLDepthTest(const LLGLDepthTest&) = delete; + LLGLDepthTest& operator=(const LLGLDepthTest&) = delete; + void checkState(); // Take the tracked state back to a fresh context's: test off, writes on, GL_LESS, with the From 7b9aefa40228d1cd5dec93a05c1da5a7afd23817 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:27 -0400 Subject: [PATCH 29/38] Tree and spot shadow polygon offsets are gone, and their settings with them The tree pool's shadow pass set a polygon offset from RenderDeferredTreeShadowOffset and RenderDeferredTreeShadowBias, and its end set one from RenderDeferredSpotShadowOffset and RenderDeferredSpotShadowBias, but nothing on the shadow path enables GL_POLYGON_OFFSET_FILL, so neither did anything; every pass that does enable an offset sets its own value first. Turning them on now would change the bias of tree shadows alone, so the calls and the four settings are removed. Co-Authored-By: Claude Opus 5.5 --- indra/newview/app_settings/settings.xml | 44 ------------------- indra/newview/lldrawpooltree.cpp | 7 --- .../default/xui/pt/settings_comments.xml | 8 ---- 3 files changed, 59 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 4c90b8fe22..71a1156048 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8595,28 +8595,6 @@ Value 4.0 - RenderDeferredSpotShadowBias - - Comment - Bias value for spot shadows (prevent shadow acne). - Persist - 1 - Type - F32 - Value - -64.0 - - RenderDeferredSpotShadowOffset - - Comment - Offset value for spot shadows (prevent shadow acne). - Persist - 1 - Type - F32 - Value - 0.8 - RenderShadowBias @@ -8757,28 +8735,6 @@ Value 1.0 - RenderDeferredTreeShadowBias - - Comment - Bias value for tree shadows (prevent shadow acne). - Persist - 1 - Type - F32 - Value - 1.0 - - RenderDeferredTreeShadowOffset - - Comment - Offset value for tree shadows (prevent shadow acne). - Persist - 1 - Type - F32 - Value - 1.0 - RenderHoverGlowEnable diff --git a/indra/newview/lldrawpooltree.cpp b/indra/newview/lldrawpooltree.cpp index 9b02cb90ac..11451acda9 100644 --- a/indra/newview/lldrawpooltree.cpp +++ b/indra/newview/lldrawpooltree.cpp @@ -36,7 +36,6 @@ #include "llviewercamera.h" #include "llviewershadermgr.h" #include "llrender.h" -#include "llviewercontrol.h" #include "llviewerregion.h" #include "llenvironment.h" @@ -117,10 +116,6 @@ void LLDrawPoolTree::beginShadowPass(S32 pass) { LL_PROFILE_ZONE_SCOPED; - static LLCachedControl shadow_offset(gSavedSettings, "RenderDeferredTreeShadowOffset"); - static LLCachedControl shadow_bias(gSavedSettings, "RenderDeferredTreeShadowBias"); - gGL.setPolygonOffset(shadow_offset(), shadow_bias()); - LLEnvironment& environment = LLEnvironment::instance(); gDeferredTreeShadowProgram.bind(); @@ -137,8 +132,6 @@ void LLDrawPoolTree::endShadowPass(S32 pass) { LL_PROFILE_ZONE_SCOPED; - gGL.setPolygonOffset(gSavedSettings.getF32("RenderDeferredSpotShadowOffset"), - gSavedSettings.getF32("RenderDeferredSpotShadowBias")); gDeferredTreeShadowProgram.unbind(); } diff --git a/indra/newview/skins/default/xui/pt/settings_comments.xml b/indra/newview/skins/default/xui/pt/settings_comments.xml index f0a2486be5..1f6c1ca6f9 100644 --- a/indra/newview/skins/default/xui/pt/settings_comments.xml +++ b/indra/newview/skins/default/xui/pt/settings_comments.xml @@ -1839,18 +1839,10 @@ Escalar de ruído para esconder banding na renderização diferida. RenderDeferredSSAO Executar o shader de oclusão de ambiente em espaço de tela no renderizador diferido. - RenderDeferredSpotShadowBias - Valor de viés para sombras de spot (previne shadow acne). - RenderDeferredSpotShadowOffset - Valor de deslocamento para sombras de spot (previne shadow acne). RenderDeferredSun Executar o shader de luz solar no renderizador diferido. RenderDeferredSunWash Quanto as luzes locais são apagadas pelo sol. - RenderDeferredTreeShadowBias - Valor de viés para sombras de árvores (previne shadow acne). - RenderDeferredTreeShadowOffset - Valor de deslocamento para sombras de árvores (previne shadow acne). RenderDelayCreation Limitar a criação de drawables. RenderDelayVBUpdate From 4668ae3e6b9c9e6f3bb1b2786bbc203b3de08609 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:30 -0400 Subject: [PATCH 30/38] Two GL calls that could not take effect are gone from display display_startup cleared depth after swapping, inside its LLGLSUIDefault scope, whose depth test turns depth writes off, so the clear wrote nothing. display() set a 512x512 viewport before updating impostors, and every impostor bake binds its own target, which sets its own viewport, with setup3DViewport after. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerdisplay.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index ac15008c0b..0274cb38c6 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -200,8 +200,6 @@ void display_startup() if (gViewerWindow && gViewerWindow->getWindow()) gViewerWindow->getWindow()->swapBuffers(); - - glClear(GL_DEPTH_BUFFER_BIT); } void display_update_camera() @@ -841,7 +839,6 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) LLGLState::checkStates(); const LLCamera saved_camera = LLViewerCamera::getCurrent(); - glViewport(0,0,512,512); LLVOAvatar::updateImpostors(); From cdf91481e072f43f4e3e61752076223d7f8bb91f Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:32 -0400 Subject: [PATCH 31/38] The joint manipulator's marker spheres no longer drain glGetError Each marker sphere drained glGetError, ungated, every frame the manipulator was up: a round trip to the driver per joint on threaded drivers, which also swallowed the errors gDebugGL's checks report. Co-Authored-By: Claude Opus 5.5 --- indra/newview/fsmaniprotatejoint.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp index 8694bb7386..deb10b1443 100644 --- a/indra/newview/fsmaniprotatejoint.cpp +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -115,13 +115,6 @@ static void renderPulsingSphere(const LLVector3& joint_world_position, const LLC gGL.popMatrix(); } gGL.popMatrix(); - - // Check for OpenGL errors - GLenum err; - while ((err = glGetError()) != GL_NO_ERROR) - { - LL_INFOS() << "OpenGL Error: " << err << LL_ENDL; - } } static void renderStaticSphere(const LLVector3& joint_world_position, const LLColor4& color = LLColor4(1.f, 1.f, 0.f, .6f), float radius=0.01f) @@ -160,13 +153,6 @@ static void renderStaticSphere(const LLVector3& joint_world_position, const LLCo gGL.popMatrix(); } gGL.popMatrix(); - - // Check for OpenGL errors - GLenum err; - while ((err = glGetError()) != GL_NO_ERROR) - { - LL_INFOS() << "OpenGL Error: " << err << LL_ENDL; - } } bool FSManipRotateJoint::isMouseOverJoint(S32 mouseX, S32 mouseY, const LLVector3& jointWorldPos, F32 jointRadius, F32& outDistanceFromCamera, F32& outRayDistanceFromCenter) const From 266cac52c0e2dc1f1a2977825913a47110e6b39a Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:34 -0400 Subject: [PATCH 32/38] Accelerated media blits restore the framebuffer LLRenderTarget tracks blitTo queried the read and draw framebuffer bindings with glGetIntegerv on every media update, a driver round trip each on threaded drivers. It runs from the idle loop, where every framebuffer bind goes through LLRenderTarget and restores both bindings together, so LLRenderTarget::sCurFBO is the binding to put back. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llcefaccelinterop.cpp | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp index 0cc13b38ad..4554f00a7a 100644 --- a/indra/newview/llcefaccelinterop.cpp +++ b/indra/newview/llcefaccelinterop.cpp @@ -30,6 +30,7 @@ #include "llgl.h" // viewer GL entry points + the global wglDX* pointers #include "llrender.h" +#include "llrendertarget.h" #include "lldxhardware.h" // shared D3D11 device + GL interop device (gDXHardware) #include @@ -231,10 +232,6 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) if (!w->read_fbo) glGenFramebuffers(1, &w->read_fbo); if (!w->draw_fbo) glGenFramebuffers(1, &w->draw_fbo); - GLint prev_read = 0, prev_draw = 0; - glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); - glBindFramebuffer(GL_READ_FRAMEBUFFER, w->read_fbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, w->local_gl, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, w->draw_fbo); @@ -246,8 +243,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + glBindFramebuffer(GL_FRAMEBUFFER, LLRenderTarget::sCurFBO); wglDXUnlockObjectsNV(gl_dx, 1, &w->local_obj); return true; @@ -257,6 +253,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) #include "llgl.h" #include "llrender.h" +#include "llrendertarget.h" #include #include #include @@ -383,10 +380,6 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) if (!m->read_fbo) glGenFramebuffers(1, &m->read_fbo); if (!m->draw_fbo) glGenFramebuffers(1, &m->draw_fbo); - GLint prev_read = 0, prev_draw = 0; - glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); - glBindFramebuffer(GL_READ_FRAMEBUFFER, m->read_fbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, m->tex, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->draw_fbo); @@ -398,8 +391,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, 0, 0); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + glBindFramebuffer(GL_FRAMEBUFFER, LLRenderTarget::sCurFBO); return true; } @@ -407,6 +399,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) #include "llgl.h" #include "llrender.h" +#include "llrendertarget.h" #include #include #include @@ -626,10 +619,6 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) if (!l->read_fbo) glGenFramebuffers(1, &l->read_fbo); if (!l->draw_fbo) glGenFramebuffers(1, &l->draw_fbo); - GLint prev_read = 0, prev_draw = 0; - glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); - glBindFramebuffer(GL_READ_FRAMEBUFFER, l->read_fbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, l->tex, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, l->draw_fbo); @@ -641,8 +630,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + glBindFramebuffer(GL_FRAMEBUFFER, LLRenderTarget::sCurFBO); return true; } From 802c133aadd5cdcd3c48d6278767a9ba0cd65768 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 23:16:36 -0400 Subject: [PATCH 33/38] Batched debug lines are drawn before raw viewport and polygon mode changes LLManip::setupSnapGuideRenderPass set each snap-guide pass's viewport through setup3DViewport, a raw glViewport, with the pass before's lines still batched in gGL. renderOctree switched to line polygon mode trusting the depth-state change before it to have flushed the batch. Both flush first. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llmanip.cpp | 3 +++ indra/newview/llspatialpartition.cpp | 1 + 2 files changed, 4 insertions(+) diff --git a/indra/newview/llmanip.cpp b/indra/newview/llmanip.cpp index 09b8c0bdf0..4733488057 100644 --- a/indra/newview/llmanip.cpp +++ b/indra/newview/llmanip.cpp @@ -594,6 +594,9 @@ LLColor4 LLManip::setupSnapGuideRenderPass(S32 pass) LLColor4 line_color; F32 line_alpha = gSavedSettings.getF32("GridOpacity"); + // The last pass's lines go out under its viewport; setup3DViewport sets the next one raw. + gGL.flush(); + switch(pass) { case 0: diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp index ee6232a94f..cd936aac24 100644 --- a/indra/newview/llspatialpartition.cpp +++ b/indra/newview/llspatialpartition.cpp @@ -1706,6 +1706,7 @@ void renderOctree(LLSpatialGroup* group) { LLGLDepthTest gl_depth(false, false); + gGL.flush(); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); gGL.diffuseColor4f(1,0,0,group->mBuilt); From 6d8d262479e27aef0701d22cafe256a5b6872633 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 03:41:01 -0400 Subject: [PATCH 34/38] The texture barrier is loaded from its ARB and NV extensions below GL 4.5 glTextureBarrier was resolved only by the GL_VERSION_4_5 ladder, which returns before it on a lower context. A driver exposing GL_ARB_texture_barrier or GL_NV_texture_barrier there ran the cross-screen filter's quadrant reads with no barrier after the draws that wrote them. It is resolved beside the other entry points that early-out would leave null: by its own name under GL 4.5 or the ARB extension, and as glTextureBarrierNV under the NV one, which ARB's took over unchanged. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llgl.cpp | 12 ++++++++++++ indra/newview/pipeline.cpp | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 27cc503d37..e47d40f538 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2420,6 +2420,18 @@ void LLGLManager::initExtensions() glGetnUniformuiv = (PFNGLGETNUNIFORMUIVPROC)LL_GET_PROC_ADDRESS("glGetnUniformuiv"); } + // Texture barrier. Core in 4.5, and GL_ARB_texture_barrier names it without a suffix on a + // lower context, so it is resolved here for the GL_VERSION_4_5 ladder's early-out as above. + // GL_NV_texture_barrier, which ARB's took over unchanged, names it glTextureBarrierNV. + if (mGLVersion >= 4.49f || mGLExtensions.contains("GL_ARB_texture_barrier")) + { + glTextureBarrier = (PFNGLTEXTUREBARRIERPROC)LL_GET_PROC_ADDRESS("glTextureBarrier"); + } + if (!glTextureBarrier && mGLExtensions.contains("GL_NV_texture_barrier")) + { + glTextureBarrier = (PFNGLTEXTUREBARRIERPROC)LL_GET_PROC_ADDRESS("glTextureBarrierNV"); + } + // GL_EXT_memory_object if (mHasEXTMemoryObject) { diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 0498040137..8cedca2f63 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -9230,7 +9230,8 @@ void LLPipeline::generateBloomHDR(LLRenderTarget* src) // mWaterDis is still attached to the bound framebuffer, a read GL // defines only across a texture barrier. The same barriers put each // arm's reads of a scratch quadrant before the next arm's write to it. - // The entry point is loaded only on GL 4.5 and up. + // Null where the driver has neither GL 4.5 nor a texture barrier + // extension. if (src == &mWaterDis && glTextureBarrier) { glTextureBarrier(); From b158a827de5b3728c52bb55e77d6581684d0cb59 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 03:41:04 -0400 Subject: [PATCH 35/38] LLRender::refreshState puts the cached patch size back refreshState re-issues what gGL's setters cache, because a setter whose cache agrees issues nothing, but it left out the patch size. After the state changed behind gGL's back, the terrain's setPatchVertices(4) saw its cache agree, and its four-corner patches were assembled three vertices at a time. It is issued wherever glPatchParameteri loaded, which is every context with tessellation. alrenderstate_gl covers it. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llrender.cpp | 6 +++ .../llrender/tests/alrenderstate_gl_test.cpp | 39 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 6f065c6209..5ae5ac6436 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -572,6 +572,12 @@ void LLRender::refreshState(void) glLineWidth(mLineWidth); + // Loaded only on a context with tessellation, the one that has a patch size. + if (glPatchParameteri) + { + glPatchParameteri(GL_PATCH_VERTICES, mPatchVertices); + } + rebasePolygonOffset(); // The same for the clear colour and a scissor box that was ever set. diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index a11edb4584..d17807e860 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -1,7 +1,8 @@ /** * @file alrenderstate_gl_test.cpp - * @brief LLRender's cached clear colour and scissor box on the hidden window: what a clear fills - * with and cuts to, what GL holds after refreshState, and what LLGLSScissor hands back. + * @brief LLRender's cached clear colour, scissor box and patch size on the hidden window: what a + * clear fills with and cuts to, what GL holds after refreshState, and what LLGLSScissor + * hands back. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -134,6 +135,40 @@ namespace tut ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); } + // And the patch size, on a context with tessellation to have one. + template<> template<> + void alrenderstate_object_t::test<4>() + { + if (!glPatchParameteri) + { + skip("this context has no tessellation"); + } + + // Handed back however the test ends: the shared context outlives it, and its scope does not keep this. + struct PatchSizeRestore + { + GLint mFound = 0; + PatchSizeRestore() { glGetIntegerv(GL_PATCH_VERTICES, &mFound); } + ~PatchSizeRestore() + { + gGL.setPatchVertices((U32)mFound); + glPatchParameteri(GL_PATCH_VERTICES, mFound); + } + } restore; + + gGL.setPatchVertices(4); + glPatchParameteri(GL_PATCH_VERTICES, 3); + gGL.setPatchVertices(4); + GLint patch = 0; + glGetIntegerv(GL_PATCH_VERTICES, &patch); + ensure_equals("an agreeing cache issues nothing", (S32)patch, 3); + + gGL.refreshState(); + glGetIntegerv(GL_PATCH_VERTICES, &patch); + ensure_equals("refreshState restores the patch size", (S32)patch, 4); + ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + } + // LLGLSScissor cuts a clear to its box with the test on, and hands back the box and the test it found, nested // scopes included. template<> template<> From 5064434c4af1c55945373d7b160d5ffd556d44e5 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 03:41:06 -0400 Subject: [PATCH 36/38] The water exclusion mask is built without its pool when there is none doWaterExclusionMask rendered mWaterExclusionPool unchecked. The pool is made in LLPipeline::init and kept until cleanup, so this guards rather than fixes a crash anyone has seen, as doWaterHaze already checks mWaterPool. Without the pool the mask stays the white it was cleared to, which excludes nothing. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 8cedca2f63..b25eb080ee 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -11703,7 +11703,11 @@ void LLPipeline::doWaterExclusionMask() mWaterExclusionMask.bindTarget(); gGL.setClearColor(LLColor4::white); mWaterExclusionMask.clear(); - mWaterExclusionPool->render(); + // Made in init and kept to cleanup. Without it the mask stays cleared, excluding nothing. + if (mWaterExclusionPool) + { + mWaterExclusionPool->render(); + } mWaterExclusionMask.flush(); gGL.setClearColor(LLColor4::transparent); From 09fb83a3111f546c6949287196ed966d6ea494a5 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 04:05:32 -0400 Subject: [PATCH 37/38] The PBO downscale's size check is stated from MAX_IMAGE_SIZE The check said the PBO path should not be used for large textures. It now takes every result the 1024x1024 scratch target cannot hold, by design. Its bound is the same 2048x2048 at four bytes a texel, written as a level below MAX_IMAGE_SIZE: a fetched texture larger than that is set missing on load, so no downscale result exceeds it. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llimagegl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 84b0ee3bd9..6b0cc42ec7 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -3296,7 +3296,10 @@ bool LLImageGL::scaleDown(S32 desired_discard) else { // use a PBO to downscale the texture U64 size = getBytes(desired_discard); - llassert(size <= 2048 * 2048 * 4); // we shouldn't be using this method to downscale huge textures, but it'll work + // At most a level below the largest texture the viewer keeps (a larger one is set + // missing on load), at four bytes a texel. The scratch buffer grows to whatever it is + // asked for; this bounds how far. + llassert(size <= (U64)(MAX_IMAGE_SIZE / 2) * (MAX_IMAGE_SIZE / 2) * 4); gGL.getTextureSlot(0)->bind(this, false, true); if (sScratchPBO == 0) From d8e150293bc35eb860c1a342e3dc9493b48db570 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 05:02:19 -0400 Subject: [PATCH 38/38] The cross-screen filter runs only with a texture barrier Each of the filter's passes after the first reads a quadrant of mWaterDis that the pass before it wrote while the texture is still attached to the bound framebuffer, which GL defines only across a texture barrier. Where glTextureBarrier did not resolve (no GL 4.5, no ARB or NV texture barrier extension) the passes ran anyway and the streaks were undefined. The filter is off there now, and the Lightbox hides its two sections as it hides the side of the HDR fork that is not running, so neither its settings nor Find offer an effect that cannot draw. Co-Authored-By: Claude Opus 5.5 --- doc/ARCHITECTURE.md | 2 +- doc/LIGHTBOX.md | 5 +++- indra/newview/alfloaterlightbox.cpp | 24 ++++++++++--------- indra/newview/pipeline.cpp | 11 +++++---- .../default/xui/en/panel_lightbox_lens.xml | 2 ++ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 6045dfbffd..ad7f80d6cd 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -121,7 +121,7 @@ Depth of field runs **before** bloom and before the tonemapper, on linear HDR. G - **Auto-exposure:** Progressive histogram (`gLuminanceProgram`, `gExposureProgram`) with history fade - **Lens flare sun state:** a 2x1 temporal pass (`gLensFlareStateProgram`, `lensFlareStateF.glsl`) run just before colour correction measures how much of the sun disc is unoccluded and its colour, and filters both under a rate limit; `computeLensFlare` in `postEffectUtilsF.glsl` reads one texel of it. The shader is the source of truth for the filter; `scripts/content_tools/check_lens_flare_state.py` mirrors it and is its regression test - **Bloom:** Bright-area extraction → downsample/upsample pyramid over `bloomMip[]` with warmth correction and optional halation carried in alpha. The additive composite is **not** a pass of its own: it is folded into `colorCorrect`'s `BLOOM_COMPOSITE` permutation. `compositeBloomHDR` is a standalone equivalent that nothing currently calls -- **Cross-screen (star) filter:** `gCrossFilterProgram` streaks every thresholded highlight, seeded from `bloomMip[0]` and accumulated through three quadrants of `mWaterDis`, which the two shaders address through `uCrossRegion`/`uCrossClamp` so a tap at the quadrant's edge clamps exactly as it did at a dedicated target's edge. One strictly one-sided three-pass chain per arm, at strides that tile the reachable offsets exactly once — the tiling is load-bearing and the shader explains why. Composited in `colorCorrect` alongside the pyramid, so it inherits bloom strength +- **Cross-screen (star) filter:** `gCrossFilterProgram` streaks every thresholded highlight, seeded from `bloomMip[0]` and accumulated through three quadrants of `mWaterDis`, which the two shaders address through `uCrossRegion`/`uCrossClamp` so a tap at the quadrant's edge clamps exactly as it did at a dedicated target's edge. One strictly one-sided three-pass chain per arm, at strides that tile the reachable offsets exactly once — the tiling is load-bearing and the shader explains why. Composited in `colorCorrect` alongside the pyramid, so it inherits bloom strength. Each pass reads a quadrant the pass before it wrote while `mWaterDis` is still attached, which GL defines only across `glTextureBarrier`, so the filter runs only where that resolved (GL 4.5, or `GL_ARB_texture_barrier`/`GL_NV_texture_barrier`) and the Lightbox hides its sections elsewhere - **Lens dirt:** `gLensDirtGenProgram` draws the grime plate into `mLensDirtMap` — dust motes, wipe smudges, stray fibres, fine grit and optional scratches, built from hashes and distance fields rather than loaded from an image. Not a per-frame pass: it runs when a generation parameter moves or the window resizes, guarded by the cached parameter set in `mLensDirtParams`, which is also what makes a failed allocation stop retrying. Generated at the frame's own resolution, so nothing has to fit a square plate to a wide window. A rebuild is held off while `mLensDirtSliderHeld` is raised — the Lightbox raises it on a generation slider's mouse-down and lowers it on mouse-up, so a drag costs one plate on release rather than one per frame. Everything that is not a drag (typed values, resets, applying a Look, undo, window resizes) rebuilds immediately, which is what a settle timer would have delayed for no reason. `colorCorrect` multiplies the accumulated bloom and flare terms by it - **Depth of Field:** three passes — circle of confusion (`gDeferredCoFProgram`, writing sharp linear colour plus signed CoF in alpha), a gather blur at `CameraDoFResScale`, and a combine (`gDeferredDoFCombineProgram`) back over `mRT->screen` under `setColorMask(true, false)` so the legacy prim-glow alpha tag survives. It owns no targets: the sharp copy borrows `mWaterDis`, which the water passes refill each frame and have finished with, and the blur borrows `bloomMip[0]` between the luminance read and the extract pass (`postPongMap` without HDR). The blur viewport is clamped to what mip 0 can hold when `RenderBloomResolutionScale` shrinks it, and the combine is told the fraction it actually covers via `res_scale` and where it sits via `dof_uv_scale`. The gather has four compile-time variants over two axes, `FRONT_BLUR` × `DOF_SHAPED`: `gDeferredPostProgram`, `gDeferredPostProgramNoNear`, `gDeferredPostProgramShaped`, `gDeferredPostProgramNoNearShaped`. Optics: `CameraFNumber`, `CameraFocalLength`, `CameraFieldOfView`, `CameraMaxCoF`, `CameraDoFResScale` - **Bokeh shaping** (inside the `DOF_SHAPED` gather): polygonal aperture (`RenderBokehApertureBlades`, `Rotation`, `Curvature`), anamorphic squeeze, cat's-eye optical vignetting, defocus fringing, and the lens aberrations — spherical (`RenderBokehSphericalAberration`), field stretch for swirl and coma (`RenderBokehFieldStretch`, `FieldFalloff`) and comatic asymmetry. The CPU picks a shaped variant only when one of them is actually doing something diff --git a/doc/LIGHTBOX.md b/doc/LIGHTBOX.md index 0b18e6a94c..be7880fe6a 100644 --- a/doc/LIGHTBOX.md +++ b/doc/LIGHTBOX.md @@ -1223,7 +1223,10 @@ only; apply per row, not on the parent panel. Reference patterns: Advanced section apply while `RenderHDREnabled` is on (on GL above 4.05, the pipeline's own test), and so do Cross Filter and its Advanced section, which streak what the bloom pyramid holds; Glow (Legacy) and its Advanced section - apply while it is off. An effect that only runs on one side of the fork + apply while it is off. Cross Filter also needs a texture barrier, so on a + driver with neither GL 4.5 nor a texture barrier extension its sections stay + hidden under HDR too, as the renderer leaves it off there. An effect that only + runs on one side of the fork, or only with some capability, belongs in the table in `refreshBloomSections()`, not just behind an `enabled_control`. `refreshBloomSections()` hides the other side's accordion tabs and re-arranges the accordion, live on the setting's signal. A tab's diff --git a/indra/newview/alfloaterlightbox.cpp b/indra/newview/alfloaterlightbox.cpp index fe1402975b..192a642b3e 100644 --- a/indra/newview/alfloaterlightbox.cpp +++ b/indra/newview/alfloaterlightbox.cpp @@ -2377,26 +2377,28 @@ void ALFloaterLightBox::refreshBloomSections() // is on the HDR side with the bloom it is seeded from: it streaks what // the bloom pyramid's top holds, and without the pyramid it draws nothing. // So is the auto exposure, which renderFinalize meters only under HDR. + // The cross filter also needs a texture barrier (generateBloomHDR), which a + // driver with neither GL 4.5 nor a texture barrier extension does not have. const bool hdr = gGLManager.mGLVersion > 4.05f && gSavedSettings.getBOOL("RenderHDREnabled"); - static const std::pair bloom_sections[] = { - { "sec_autoexp", true }, - { "sec_autoexp_adv", true }, - { "sec_bloom", true }, - { "sec_bloom_adv", true }, - { "sec_crossfilter", true }, - { "sec_crossfilter_adv", true }, - { "sec_glow", false }, - { "sec_glow_adv", false }, + const bool cross_filter = hdr && glTextureBarrier; + const std::pair bloom_sections[] = { + { "sec_autoexp", hdr }, + { "sec_autoexp_adv", hdr }, + { "sec_bloom", hdr }, + { "sec_bloom_adv", hdr }, + { "sec_crossfilter", cross_filter }, + { "sec_crossfilter_adv", cross_filter }, + { "sec_glow", !hdr }, + { "sec_glow_adv", !hdr }, }; std::set changed; - for (const auto& [name, for_hdr] : bloom_sections) + for (const auto& [name, shown] : bloom_sections) { const ALLightboxDirectory::Section* section = mDirectory.section(name); if (!section || !section->mTab) { continue; } - const bool shown = (for_hdr == hdr); if (section->mTab->getVisible() == shown) { continue; diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index b25eb080ee..4e5f1eed63 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -9088,7 +9088,11 @@ void LLPipeline::generateBloomHDR(LLRenderTarget* src) const F32 streak_strength = (no_post || bloom_strength_gate() <= 0.f) ? 0.f : llclamp(streak_strength_setting(), 0.f, CROSS_FILTER_MAX_STRENGTH); - const bool streaks_on = (streak_strength > 0.f) && gCrossFilterProgram.isComplete(); + // And only with a texture barrier: the chain reads quadrants of mWaterDis that the draw + // before it wrote while the texture is still attached, which GL defines only across one. + // glTextureBarrier is null where the driver has neither GL 4.5 nor a texture barrier + // extension, and there the streaks are off rather than undefined. + const bool streaks_on = (streak_strength > 0.f) && gCrossFilterProgram.isComplete() && glTextureBarrier; bool streaks_ready = false; if (streaks_on) @@ -9230,9 +9234,8 @@ void LLPipeline::generateBloomHDR(LLRenderTarget* src) // mWaterDis is still attached to the bound framebuffer, a read GL // defines only across a texture barrier. The same barriers put each // arm's reads of a scratch quadrant before the next arm's write to it. - // Null where the driver has neither GL 4.5 nor a texture barrier - // extension. - if (src == &mWaterDis && glTextureBarrier) + // Never null here: streaks_on requires it. + if (src == &mWaterDis) { glTextureBarrier(); } diff --git a/indra/newview/skins/default/xui/en/panel_lightbox_lens.xml b/indra/newview/skins/default/xui/en/panel_lightbox_lens.xml index 4e0eb1fb71..cce1f4cf49 100644 --- a/indra/newview/skins/default/xui/en/panel_lightbox_lens.xml +++ b/indra/newview/skins/default/xui/en/panel_lightbox_lens.xml @@ -7,6 +7,8 @@ renderer is running: the two Bloom (HDR) sections, and the two Cross Filter sections that are seeded from the same bloom, while RenderHDREnabled is on, the two Glow (Legacy) sections while it is off (ALFloaterLightBox::refreshBloomSections, live as the setting changes). The +Cross Filter sections also need a texture barrier, and stay hidden on a driver +without one, where the renderer leaves the filter off. The rows also grey out on the same setting (enabled_control on Bloom and Cross Filter, disabled_control on Glow), which is all a build of this panel without the floater gets. (RenderBloomHDR is currently unwired renderer-side, so the fork