From fa6a1adced1c1f9894a7f0b7e88d5d2ac9e04413 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 05:58:15 -0400 Subject: [PATCH 1/8] LLRender keeps the raster state, stencil and viewport it sets, and owns the GL newview still made raw newview set the polygon mode, cull face, point size, stencil, blend equation and viewport with raw GL, so nothing knew what was in force, nothing put it back after a context reset, and a state change landed under geometry gGL still had batched. LLRender now keeps each as it keeps the line width and scissor box: - setPolygonMode, setCullFace, setPointSize, setStencilFunc/Op/Mask, setBlendEquation and setViewport drop a set the cache agrees with, draw what is batched before a change, and have getters. init() issues GL's initial values, refreshState() re-issues them, and the viewport is unknown until set, as the scissor box is. - Every writer of them goes through it: LLRenderTarget's bind and flush, LLImageGL's downscale, ALGLStencil, and the transient pool's debug poison, which read the stencil write mask back with glGet. And what newview reached raw GL for besides: - readPixels: a synchronous read of the bound framebuffer, for a snapshot or a click's probe. - applyDepthConvention: the clip control and depth clear value sReverseZ calls for. - LLRenderTarget::copyFromWindow: the window's framebuffer into attachment 0. - ALTextureBlit: a copy from one texture into another through framebuffers it keeps, upside down if asked, for textures no render target holds. - gGLManager.mGLVendorString/mGLRendererString: GL_VENDOR and GL_RENDERER in the driver's case, as mGLVersionString is GL_VERSION, so no caller asks the driver. The GL test fixture hands back the new state as it found it, and the on-demand target test sets its viewport through gGL, as the code it mirrors now does. New cases in alrenderstate_gl_test and altextureblit_gl_test; 13 mutants of the new code each fail them. Co-Authored-By: Claude Opus 5.5 --- indra/alrender/altransienttargetpool.cpp | 7 +- .../tests/alondemandtarget_gl_test.cpp | 4 +- indra/llrender/CMakeLists.txt | 7 +- indra/llrender/altextureblit.cpp | 83 +++++ indra/llrender/altextureblit.h | 59 ++++ indra/llrender/llgl.cpp | 6 +- indra/llrender/llgl.h | 4 + indra/llrender/llglstates.h | 14 +- indra/llrender/llimagegl.cpp | 2 +- indra/llrender/llrender.cpp | 148 +++++++- indra/llrender/llrender.h | 87 +++++ indra/llrender/llrendertarget.cpp | 23 +- indra/llrender/llrendertarget.h | 5 + .../llrender/tests/alrenderstate_gl_test.cpp | 334 +++++++++++++++++- .../llrender/tests/altextureblit_gl_test.cpp | 189 ++++++++++ indra/llrender/tests/llheadlessgl_fixture.h | 32 ++ 16 files changed, 978 insertions(+), 26 deletions(-) create mode 100644 indra/llrender/altextureblit.cpp create mode 100644 indra/llrender/altextureblit.h create mode 100644 indra/llrender/tests/altextureblit_gl_test.cpp diff --git a/indra/alrender/altransienttargetpool.cpp b/indra/alrender/altransienttargetpool.cpp index ee0d1be6e8..2c5f9391f3 100644 --- a/indra/alrender/altransienttargetpool.cpp +++ b/indra/alrender/altransienttargetpool.cpp @@ -95,15 +95,14 @@ namespace LLGLDisable no_scissor(GL_SCISSOR_TEST); LLGLSColorMask colour_mask(true, true); LLGLDepthTest depth_mask(GL_TRUE, GL_TRUE, GL_ALWAYS); - GLint stencil_mask = 0xff; GLint clear_stencil = 0; GLfloat clear_depth = 1.f; - glGetIntegerv(GL_STENCIL_WRITEMASK, &stencil_mask); glGetIntegerv(GL_STENCIL_CLEAR_VALUE, &clear_stencil); glGetFloatv(GL_DEPTH_CLEAR_VALUE, &clear_depth); const LLColor4 clear_colour = gGL.getClearColor(); + const U32 stencil_mask = gGL.getStencilMask(); - glStencilMask(0xff); + gGL.setStencilMask(0xff); glClearDepth(Pool::POISON_DEPTH); glClearStencil(Pool::POISON_STENCIL); gGL.setClearColor(POISON_COLOUR); @@ -114,7 +113,7 @@ namespace gGL.setClearColor(clear_colour); glClearStencil(clear_stencil); glClearDepth(clear_depth); - glStencilMask((GLuint)stencil_mask); + gGL.setStencilMask(stencil_mask); } void free(LLRenderTarget& target) override { target.release(); } diff --git a/indra/alrender/tests/alondemandtarget_gl_test.cpp b/indra/alrender/tests/alondemandtarget_gl_test.cpp index 330fb9b490..9e88b2257b 100644 --- a/indra/alrender/tests/alondemandtarget_gl_test.cpp +++ b/indra/alrender/tests/alondemandtarget_gl_test.cpp @@ -130,8 +130,8 @@ namespace { ++item.mDrawn; CHECK(LLRenderTarget::getCurrentBoundTarget() == &target); - glViewport(0, 0, item.mWidth, item.mHeight); - glClear(GL_DEPTH_BUFFER_BIT); + gGL.setViewport(0, 0, item.mWidth, item.mHeight); + gGL.clear(GL_DEPTH_BUFFER_BIT); draw(target); REQUIRE(item.mImage->setSubImageFromFrameBuffer(0, 0, 0, 0, item.mWidth, item.mHeight)); } diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index 85d0b7530e..a034a07bb6 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -12,6 +12,7 @@ set( alquerypool.cpp alsamplerstate.cpp altexture3d.cpp + altextureblit.cpp altextureslot.cpp alfontcolrv1.cpp alfontface.cpp @@ -54,6 +55,7 @@ set( alrendertargetdesc.h alsamplerstate.h altexture3d.h + altextureblit.h altextureslot.h alfontcolrv1.h alfontface.h @@ -210,7 +212,9 @@ if(AL_BUILD_TESTS) # - which fragment output reaches a render target's colour attachment, # integer attachments cleared and drawn, targets kept, built again and # re-pointed at a shared depth by their descriptions, and their labels; - # - LLRender's cached clear colour and scissor box, and LLGLSScissor; + # - LLRender's cached clear colour, scissor box, viewport and raster state, + # LLGLSScissor and ALGLStencil, and a copy from one texture into another + # (ALTextureBlit); # - LLGLSLShader's cache key, its uniform value cache, the compute path # (programs built through the shader manager, their images, storage # blocks and dispatches), integer textures through reserved samplers, @@ -251,6 +255,7 @@ if(AL_BUILD_TESTS) tests/alquerypool_gl_test.cpp tests/alrenderstate_gl_test.cpp tests/altessellation_gl_test.cpp + tests/altextureblit_gl_test.cpp tests/aluniformbuffer_gl_test.cpp tests/alwindowsdlheadless_test.cpp tests/llfontgl_test.cpp diff --git a/indra/llrender/altextureblit.cpp b/indra/llrender/altextureblit.cpp new file mode 100644 index 0000000000..fb361640c5 --- /dev/null +++ b/indra/llrender/altextureblit.cpp @@ -0,0 +1,83 @@ +/** + * @file altextureblit.cpp + * @brief A copy from one texture into another through framebuffers it keeps, for textures no render target holds. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "altextureblit.h" + +#include "llgl.h" +#include "llrender.h" +#include "llrendertarget.h" + +ALTextureBlit::~ALTextureBlit() +{ + release(); +} + +void ALTextureBlit::blit(U32 src_target, U32 src, U32 dst, S32 width, S32 height, bool flip_y) +{ + llassert(src_target == GL_TEXTURE_2D || src_target == GL_TEXTURE_RECTANGLE); + gGL.flush(); + + if (!mReadFBO) + { + glGenFramebuffers(1, &mReadFBO); + } + if (!mDrawFBO) + { + glGenFramebuffers(1, &mDrawFBO); + } + + glBindFramebuffer(GL_READ_FRAMEBUFFER, mReadFBO); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, src_target, src, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mDrawFBO); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0); + + glBlitFramebuffer(0, 0, width, height, + 0, flip_y ? height : 0, width, flip_y ? 0 : height, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // Detached, so neither framebuffer keeps a texture its owner may delete or hand to another context. + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, src_target, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_FRAMEBUFFER, LLRenderTarget::sCurFBO); +} + +void ALTextureBlit::release() +{ + // The names die with the context; deleting them after it would fault in the driver. + if (gGLManager.mInited) + { + if (mReadFBO) + { + glDeleteFramebuffers(1, &mReadFBO); + } + if (mDrawFBO) + { + glDeleteFramebuffers(1, &mDrawFBO); + } + } + mReadFBO = 0; + mDrawFBO = 0; +} diff --git a/indra/llrender/altextureblit.h b/indra/llrender/altextureblit.h new file mode 100644 index 0000000000..08da12ee6e --- /dev/null +++ b/indra/llrender/altextureblit.h @@ -0,0 +1,59 @@ +/** + * @file altextureblit.h + * @brief A copy from one texture into another through framebuffers it keeps, for textures no render target holds. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "stdtypes.h" + +// Copies level 0 of one texture into another by glBlitFramebuffer, through a pair of framebuffers it makes on its first +// copy and keeps: the copy from a texture no LLRenderTarget holds, such as one whose storage a platform's interop +// supplies (a D3D11 texture, an IOSurface, a dma-buf), into another that need not be a target's either, upside down if +// asked. +// +// All operations require a current GL context, the one the framebuffers were made in. Non-copyable. +class ALTextureBlit +{ +public: + ALTextureBlit() = default; + ~ALTextureBlit(); + + ALTextureBlit(const ALTextureBlit&) = delete; + ALTextureBlit& operator=(const ALTextureBlit&) = delete; + + // Copy the `width` x `height` rectangle at the origin of `src`, a texture of `src_target` (GL_TEXTURE_2D or + // GL_TEXTURE_RECTANGLE), into the same rectangle of `dst`, a GL_TEXTURE_2D, texel for texel, its rows in the + // opposite order where `flip_y`. What gGL has batched is drawn first. Neither texture stays attached, and the + // framebuffer bound before (LLRenderTarget::sCurFBO) is bound again. + void blit(U32 src_target, U32 src, U32 dst, S32 width, S32 height, bool flip_y); + + // Delete the framebuffers, if any. Call from a teardown that still has the context; the destructor calls it as a + // backstop, and issues no GL once the context is gone. + void release(); + + bool hasFramebuffers() const { return mReadFBO != 0 || mDrawFBO != 0; } + +private: + U32 mReadFBO = 0; + U32 mDrawFBO = 0; +}; diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 1e5d8a59f3..e8c5e569c7 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2029,10 +2029,12 @@ bool LLGLManager::initGL() // Extract video card strings and convert to upper case to // work around driver-to-driver variation in capitalization. - mGLVendor = ll_safe_string((const char *)glGetString(GL_VENDOR)); + mGLVendorString = ll_safe_string((const char *)glGetString(GL_VENDOR)); + mGLVendor = mGLVendorString; LLStringUtil::toUpper(mGLVendor); - mGLRenderer = ll_safe_string((const char *)glGetString(GL_RENDERER)); + mGLRendererString = ll_safe_string((const char *)glGetString(GL_RENDERER)); + mGLRenderer = mGLRendererString; LLStringUtil::toUpper(mGLRenderer); parse_gl_version( &mDriverVersionMajor, diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index c5926675ec..402c0512fe 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -238,6 +238,10 @@ class LLGLManager // In ALL CAPS std::string mGLRenderer; + // GL_VENDOR and GL_RENDERER in the driver's own case, as mGLVersionString is GL_VERSION. + std::string mGLVendorString; + std::string mGLRendererString; + // GL_VENDOR, GL_RENDERER and GL_VERSION as reported, less the kernel's DRM version and // release radeonsi and r600 put in the renderer on Linux. Keys what a driver update must // invalidate and a kernel update must not: the benchmark result and program binaries. diff --git a/indra/llrender/llglstates.h b/indra/llrender/llglstates.h index 48d12d03ac..2217ea2000 100644 --- a/indra/llrender/llglstates.h +++ b/indra/llrender/llglstates.h @@ -221,18 +221,16 @@ class ALGLStencil { llassert(sOpen == 0); ++sOpen; - gGL.flush(); - glStencilFunc(func, ref, read_mask); - glStencilOp(fail, depth_fail, pass); - glStencilMask(write_mask); + gGL.setStencilFunc(func, ref, read_mask); + gGL.setStencilOp(fail, depth_fail, pass); + gGL.setStencilMask(write_mask); } ~ALGLStencil() { - gGL.flush(); - glStencilFunc(GL_ALWAYS, 0, ~0u); - glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); - glStencilMask(~0u); + gGL.setStencilFunc(GL_ALWAYS, 0, ~0u); + gGL.setStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + gGL.setStencilMask(~0u); --sOpen; } diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 5dc3bfe69a..1171ccc9bf 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -3543,7 +3543,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) if (use_fbo) { // use an FBO to downscale the texture - glViewport(0, 0, desired_width, desired_height); + gGL.setViewport(0, 0, desired_width, desired_height); // draw a full screen triangle, sampling the old (larger) texture if (gGL.getTextureSlot(0)->bind(this, true, true)) diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 5b15465c88..d61258f4f4 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -443,6 +443,25 @@ bool LLRender::init(bool needs_vertex_buffer) mScissor[1] = 0; mScissor[2] = -1; mScissor[3] = -1; + mViewport[0] = 0; + mViewport[1] = 0; + mViewport[2] = -1; + mViewport[3] = -1; + + // The rest goes to GL's initial values and is issued, for the same reason as the clear colour. + mPolygonMode = GL_FILL; + mCullFace = GL_BACK; + mPointSize = 1.f; + mStencilFunc = GL_ALWAYS; + mStencilRef = 0; + mStencilReadMask = ~0u; + mStencilFail = GL_KEEP; + mStencilDepthFail = GL_KEEP; + mStencilPass = GL_KEEP; + mStencilWriteMask = ~0u; + mBlendEquationRGB = GL_FUNC_ADD; + mBlendEquationAlpha = GL_FUNC_ADD; + issueRasterState(); // Build this context's sampler objects before anything can ask for one. mSamplerCache.warmup(); @@ -451,8 +470,6 @@ bool LLRender::init(bool needs_vertex_buffer) gGL.setSceneBlendType(LLRender::BT_ALPHA); gGL.setAmbientLightColor(LLColor4::black); - glCullFace(GL_BACK); - // necessary for reflection maps glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); if (main_context) @@ -624,12 +641,18 @@ void LLRender::refreshState(void) rebasePolygonOffset(); - // The same for the clear colour and a scissor box that was ever set. + // The same for the clear colour and a scissor box and viewport that were ever set. glClearColor(mClearColor.mV[VRED], mClearColor.mV[VGREEN], mClearColor.mV[VBLUE], mClearColor.mV[VALPHA]); if (mScissor[2] >= 0) { glScissor(mScissor[0], mScissor[1], mScissor[2], mScissor[3]); } + if (mViewport[2] >= 0) + { + glViewport(mViewport[0], mViewport[1], mViewport[2], mViewport[3]); + } + + issueRasterState(); flush(); @@ -1905,6 +1928,125 @@ void LLRender::clear(U32 mask) glClear(mask); } +void LLRender::setPolygonMode(U32 mode) +{ + llassert(mode == GL_FILL || mode == GL_LINE || mode == GL_POINT); + if (mPolygonMode != mode) + { + flush(); + mPolygonMode = mode; + glPolygonMode(GL_FRONT_AND_BACK, mode); + } +} + +void LLRender::setCullFace(U32 face) +{ + llassert(face == GL_BACK || face == GL_FRONT || face == GL_FRONT_AND_BACK); + if (mCullFace != face) + { + flush(); + mCullFace = face; + glCullFace(face); + } +} + +void LLRender::setPointSize(F32 size) +{ + llassert(size > 0.f); + if (mPointSize != size) + { + flush(); + mPointSize = size; + glPointSize(size); + } +} + +void LLRender::setStencilFunc(U32 func, S32 ref, U32 mask) +{ + if (mStencilFunc != func || mStencilRef != ref || mStencilReadMask != mask) + { + flush(); + mStencilFunc = func; + mStencilRef = ref; + mStencilReadMask = mask; + glStencilFunc(func, ref, mask); + } +} + +void LLRender::setStencilOp(U32 stencil_fail, U32 depth_fail, U32 pass) +{ + if (mStencilFail != stencil_fail || mStencilDepthFail != depth_fail || mStencilPass != pass) + { + flush(); + mStencilFail = stencil_fail; + mStencilDepthFail = depth_fail; + mStencilPass = pass; + glStencilOp(stencil_fail, depth_fail, pass); + } +} + +void LLRender::setStencilMask(U32 mask) +{ + if (mStencilWriteMask != mask) + { + flush(); + mStencilWriteMask = mask; + glStencilMask(mask); + } +} + +void LLRender::setBlendEquation(U32 rgb, U32 alpha) +{ + if (mBlendEquationRGB != rgb || mBlendEquationAlpha != alpha) + { + flush(); + mBlendEquationRGB = rgb; + mBlendEquationAlpha = alpha; + glBlendEquationSeparate(rgb, alpha); + } +} + +void LLRender::setViewport(S32 x, S32 y, S32 width, S32 height) +{ + llassert(width >= 0 && height >= 0); + if (mViewport[0] != x || mViewport[1] != y || mViewport[2] != width || mViewport[3] != height) + { + flush(); + mViewport[0] = x; + mViewport[1] = y; + mViewport[2] = width; + mViewport[3] = height; + glViewport(x, y, width, height); + } +} + +void LLRender::issueRasterState() const +{ + glPolygonMode(GL_FRONT_AND_BACK, mPolygonMode); + glCullFace(mCullFace); + glPointSize(mPointSize); + glStencilFunc(mStencilFunc, mStencilRef, mStencilReadMask); + glStencilOp(mStencilFail, mStencilDepthFail, mStencilPass); + glStencilMask(mStencilWriteMask); + glBlendEquationSeparate(mBlendEquationRGB, mBlendEquationAlpha); +} + +// static +void LLRender::applyDepthConvention() +{ + if (gGLManager.mHasClipControl) + { + glClipControl(GL_LOWER_LEFT, sReverseZ ? GL_ZERO_TO_ONE : GL_NEGATIVE_ONE_TO_ONE); + } + glClearDepth(sReverseZ ? 0.0 : 1.0); +} + +void LLRender::readPixels(S32 x, S32 y, S32 width, S32 height, U32 format, U32 type, void* data) +{ + flush(); + glReadPixels(x, y, width, height, format, type, data); +} + void LLRender::drawArraysInstanced(eGeomModes mode, U32 first, U32 count, U32 instances) { // An input left in the program would read whatever buffer the last vertex buffer left on its array. diff --git a/indra/llrender/llrender.h b/indra/llrender/llrender.h index a2090e2d2f..4ced5cd6b9 100644 --- a/indra/llrender/llrender.h +++ b/indra/llrender/llrender.h @@ -410,6 +410,75 @@ class LLRender // Redundant sets are dropped without a flush. void setPatchVertices(U32 count); + // How polygons rasterize, front and back alike, the only face a core context takes: GL_FILL, GL_LINE or + // GL_POINT. A change flushes what is batched under the old mode; redundant sets are dropped without one. + void setPolygonMode(U32 mode); + U32 getPolygonMode() const { return mPolygonMode; } + + // The faces GL_CULL_FACE (an LLGLState) culls: GL_BACK, GL_FRONT or GL_FRONT_AND_BACK. A change flushes what is + // batched; redundant sets are dropped without one. + void setCullFace(U32 face); + U32 getCullFace() const { return mCullFace; } + + // The diameter in pixels points rasterize at while the program writes no gl_PointSize. A change flushes what is + // batched; redundant sets are dropped without one. + void setPointSize(F32 size); + F32 getPointSize() const { return mPointSize; } + + // The stencil test GL_STENCIL_TEST (an LLGLState) runs, front and back alike: its function, reference and the + // bits it compares, what each outcome does to the stencil, and the bits a draw or a clear may write. Each setter + // flushes what is batched on a change and drops a redundant set without one. ALGLStencil scopes all three. + void setStencilFunc(U32 func, S32 ref, U32 mask); + void setStencilOp(U32 stencil_fail, U32 depth_fail, U32 pass); + void setStencilMask(U32 mask); + void getStencilFunc(U32& func, S32& ref, U32& mask) const + { + func = mStencilFunc; + ref = mStencilRef; + mask = mStencilReadMask; + } + void getStencilOp(U32& stencil_fail, U32& depth_fail, U32& pass) const + { + stencil_fail = mStencilFail; + depth_fail = mStencilDepthFail; + pass = mStencilPass; + } + U32 getStencilMask() const { return mStencilWriteMask; } + + // How blending combines source and destination, the colour by `rgb` and alpha by `alpha` (GL_FUNC_ADD, GL_MAX + // and the rest), for every draw buffer. A change flushes what is batched; redundant sets are dropped without one. + void setBlendEquation(U32 rgb, U32 alpha); + void getBlendEquation(U32& rgb, U32& alpha) const + { + rgb = mBlendEquationRGB; + alpha = mBlendEquationAlpha; + } + + // The viewport, in pixels of the bound framebuffer. Every writer goes through here, LLRenderTarget's binds + // included; a change flushes what is batched under the old viewport, and redundant sets are dropped without one. + void setViewport(S32 x, S32 y, S32 width, S32 height); + // The viewport in force: x, y, width, height. Width is negative until one is set in this context, which GL starts + // at the size of the window it was made for. + void getViewport(S32 (&box)[4]) const + { + box[0] = mViewport[0]; + box[1] = mViewport[1]; + box[2] = mViewport[2]; + box[3] = mViewport[3]; + } + + // Issue the clip control and the depth clear value sReverseZ calls for: zero-to-one clip depth cleared to 0 + // under reverse-Z, minus-one-to-one cleared to 1 otherwise, clip control only where the context has it + // (gGLManager.mHasClipControl). Both are issued whether or not they changed. + static void applyDepthConvention(); + + // Reads the `width` x `height` rectangle at (x, y) of the framebuffer bound for reading into `data`, as + // glReadPixels reads it in `format` and `type` under the pack alignment of 1 init() set, after what is batched + // has been drawn. Synchronous: it waits for the GPU, and for a threaded driver's own thread, to finish everything + // issued before it. For a read made once on purpose, a snapshot or a click's probe; anything read every frame + // reads through ALGPUReadback. + void readPixels(S32 x, S32 y, S32 width, S32 height, U32 format, U32 type, void* data); + // The colour glClear fills colour buffers with. Every writer in the tree goes through // here, so the cache is the state. Redundant sets are dropped; nothing batched reads it, // so a change needs no flush. @@ -654,6 +723,24 @@ class LLRender U32 mPatchVertices = 3; // GL's initial GL_PATCH_VERTICES LLColor4 mClearColor = LLColor4(0.f, 0.f, 0.f, 0.f); // GL's initial GL_COLOR_CLEAR_VALUE S32 mScissor[4] = { 0, 0, -1, -1 }; // unknown until set (see getScissor) + S32 mViewport[4] = { 0, 0, -1, -1 }; // unknown until set (see getViewport) + // The rest at GL's initial values, which init() issues. + U32 mPolygonMode = GL_FILL; + U32 mCullFace = GL_BACK; + F32 mPointSize = 1.f; + U32 mStencilFunc = GL_ALWAYS; + S32 mStencilRef = 0; + U32 mStencilReadMask = ~0u; + U32 mStencilFail = GL_KEEP; + U32 mStencilDepthFail = GL_KEEP; + U32 mStencilPass = GL_KEEP; + U32 mStencilWriteMask = ~0u; + U32 mBlendEquationRGB = GL_FUNC_ADD; + U32 mBlendEquationAlpha = GL_FUNC_ADD; + + // Issue the polygon mode, cull face, point size, stencil state and blend equation this holds, as init() and + // refreshState() do. + void issueRasterState() const; LLPointer mBuffer; LLStrider mVerticesp; diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index db31db4b68..7446b7c281 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -924,7 +924,7 @@ void LLRenderTarget::bindTarget() } check_framebuffer_status(); - glViewport(0, 0, mResX, mResY); + gGL.setViewport(0, 0, mResX, mResY); sCurResX = mResX; sCurResY = mResY; @@ -1111,7 +1111,7 @@ void LLRenderTarget::flush() gGL.queries().checkPassBoundary(); glBindFramebuffer(GL_FRAMEBUFFER, 0); sCurFBO = 0; - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); sCurResX = gGLViewport[2]; sCurResY = gGLViewport[3]; glReadBuffer(GL_BACK); @@ -1229,6 +1229,25 @@ void LLRenderTarget::copyFromBound(S32 srcX0, S32 srcY0, S32 srcX1, S32 srcY1, S glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } +void LLRenderTarget::copyFromWindow(S32 x, S32 y, S32 width, S32 height) +{ + // What gGL has batched is drawn before the copy reads it. + gGL.flush(); + + if (mTex.empty()) + { + LL_WARNS() << "Cannot copy into a render target without a colour attachment." << LL_ENDL; + return; + } + + LL_PROFILE_GPU_ZONE("LLRenderTarget::copyFromWindow"); + gGL.getTextureSlot(0)->bind(this); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glCopyTexSubImage2D(ALTextureSlot::getInternalType(mUsage), 0, 0, 0, x, y, width, height); + stop_glerror(); + glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); +} + bool LLRenderTarget::isComplete() const { // A sharer with no colour of its own renders into its owner's depth. diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index 294abaffbd..6bc8703401 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -301,6 +301,11 @@ class LLRenderTarget // The scissor does not clip it, and what is bound stays bound. void copyFromBound(S32 srcX0, S32 srcY0, S32 srcX1, S32 srcY1, S32 dstX0, S32 dstY0, S32 dstX1, S32 dstY1, U32 filter); + // Copies the `width` x `height` rect at (x, y) of the window's framebuffer, the one drawn into with no target + // bound, into colour attachment 0 at its origin, one to one, whatever is bound now. What is bound stays bound, and + // texture slot 0 is left holding the attachment. + void copyFromWindow(S32 x, S32 y, S32 width, S32 height); + //Returns TRUE if target is ready to be rendered into. //That is, if the target has been allocated with at least //one renderable attachment (i.e. color buffer, depth buffer). diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index 9eefa7bdf5..fee7964056 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -1,8 +1,10 @@ /** * @file alrenderstate_gl_test.cpp - * @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. + * @brief LLRender's cached clear colour, scissor box, patch size, polygon mode, cull face, point size, + * stencil state, blend equation and viewport on the hidden window: what a clear fills with and + * cuts to, what GL holds after refreshState, what a change flushes, and what LLGLSScissor and + * ALGLStencil hand back; the depth convention, a synchronous read, a copy from the window, and + * the driver's own strings. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -30,12 +32,16 @@ #include "../llglheaders.h" #include "../llglstates.h" #include "../llrender.h" +#include "../llrender2dutils.h" #include "../llrendertarget.h" +#include "../llvertexbuffer.h" #include "llheadlessgl_fixture.h" #include +#include + namespace { constexpr U32 SIZE = 4; @@ -67,6 +73,90 @@ namespace } } + // The first value GL holds for `pname`. Room for four: GL_POLYGON_MODE answers with two on some drivers. + GLint glInteger(GLenum pname) + { + GLint value[4] = { -1, -1, -1, -1 }; + glGetIntegerv(pname, value); + return value[0]; + } + + F32 glFloat(GLenum pname) + { + GLfloat value[4] = { -1.f, -1.f, -1.f, -1.f }; + glGetFloatv(pname, value); + return value[0]; + } + + bool glViewportIs(S32 x, S32 y, S32 width, S32 height) + { + GLint value[4] = { -1, -1, -1, -1 }; + glGetIntegerv(GL_VIEWPORT, value); + return value[0] == x && value[1] == y && value[2] == width && value[3] == height; + } + + bool viewportIs(S32 x, S32 y, S32 width, S32 height) + { + S32 box[4]; + gGL.getViewport(box); + return box[0] == x && box[1] == y && box[2] == width && box[3] == height; + } + + // Whether `change` draws what gGL has batched: one triangle is batched into a list, which holds it once it + // is drawn. + template bool drawsBatched(Change&& change) + { + gUIProgram.bind(); + std::list drawn; + gGL.beginList(&drawn); + gGL.begin(LLRender::TRIANGLES); + gGL.vertex3f(0.f, 0.f, 0.f); + gGL.vertex3f(1.f, 0.f, 0.f); + gGL.vertex3f(0.f, 1.f, 0.f); + gGL.end(); + change(); + const bool drew = !drawn.empty(); + gGL.endList(); + return drew; + } + + // GL's stencil state, both faces, against what is given. The masks are held to the 8 bits a stencil buffer has: + // past them, what glGetIntegerv answers for an unsigned mask is the driver's choice. + void checkStencil(GLenum func, GLint ref, GLuint read_mask, GLenum fail, GLenum depth_fail, GLenum pass, + GLuint write_mask) + { + CHECK((U32)glInteger(GL_STENCIL_FUNC) == func); + CHECK(glInteger(GL_STENCIL_REF) == ref); + CHECK(((U32)glInteger(GL_STENCIL_VALUE_MASK) & 0xFF) == (read_mask & 0xFF)); + CHECK((U32)glInteger(GL_STENCIL_FAIL) == fail); + CHECK((U32)glInteger(GL_STENCIL_PASS_DEPTH_FAIL) == depth_fail); + CHECK((U32)glInteger(GL_STENCIL_PASS_DEPTH_PASS) == pass); + CHECK(((U32)glInteger(GL_STENCIL_WRITEMASK) & 0xFF) == (write_mask & 0xFF)); + CHECK((U32)glInteger(GL_STENCIL_BACK_FUNC) == func); + CHECK(glInteger(GL_STENCIL_BACK_REF) == ref); + CHECK(((U32)glInteger(GL_STENCIL_BACK_VALUE_MASK) & 0xFF) == (read_mask & 0xFF)); + CHECK((U32)glInteger(GL_STENCIL_BACK_FAIL) == fail); + CHECK((U32)glInteger(GL_STENCIL_BACK_PASS_DEPTH_FAIL) == depth_fail); + CHECK((U32)glInteger(GL_STENCIL_BACK_PASS_DEPTH_PASS) == pass); + CHECK(((U32)glInteger(GL_STENCIL_BACK_WRITEMASK) & 0xFF) == (write_mask & 0xFF)); + } + + // gGL's stencil state against what is given. + void checkCachedStencil(U32 func, S32 ref, U32 read_mask, U32 fail, U32 depth_fail, U32 pass, U32 write_mask) + { + U32 cached_func = 0, cached_read_mask = 0, cached_fail = 0, cached_depth_fail = 0, cached_pass = 0; + S32 cached_ref = -1; + gGL.getStencilFunc(cached_func, cached_ref, cached_read_mask); + gGL.getStencilOp(cached_fail, cached_depth_fail, cached_pass); + CHECK(cached_func == func); + CHECK(cached_ref == ref); + CHECK(cached_read_mask == read_mask); + CHECK(cached_fail == fail); + CHECK(cached_depth_fail == depth_fail); + CHECK(cached_pass == pass); + CHECK(gGL.getStencilMask() == write_mask); + } + // The clear colour and scissor box the tests set go back with mGL. struct alrenderstate_data { @@ -219,4 +309,242 @@ namespace CHECK((S32)patch == 4); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } + + // The polygon mode, cull face, point size, stencil state and blend equation: what gGL is given GL holds, a set + // the cache agrees with issues nothing, and refreshState puts back what gGL holds. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: the raster state gGL is given is issued, kept and put back by refreshState", "[alrenderstate_gl]") + { + const auto set = [] + { + gGL.setPolygonMode(GL_LINE); + gGL.setCullFace(GL_FRONT); + gGL.setPointSize(4.f); + gGL.setStencilFunc(GL_EQUAL, 3, 0x0F); + gGL.setStencilOp(GL_ZERO, GL_INCR, GL_REPLACE); + gGL.setStencilMask(0xF0); + gGL.setBlendEquation(GL_FUNC_ADD, GL_MAX); + }; + const auto check_set = [] + { + CHECK((U32)glInteger(GL_POLYGON_MODE) == (U32)GL_LINE); + CHECK((U32)glInteger(GL_CULL_FACE_MODE) == (U32)GL_FRONT); + CHECK(glFloat(GL_POINT_SIZE) == 4.f); + checkStencil(GL_EQUAL, 3, 0x0F, GL_ZERO, GL_INCR, GL_REPLACE, 0xF0); + CHECK((U32)glInteger(GL_BLEND_EQUATION_RGB) == (U32)GL_FUNC_ADD); + CHECK((U32)glInteger(GL_BLEND_EQUATION_ALPHA) == (U32)GL_MAX); + }; + + set(); + { + INFO("what gGL is given, GL holds"); + check_set(); + } + CHECK(gGL.getPolygonMode() == (U32)GL_LINE); + CHECK(gGL.getCullFace() == (U32)GL_FRONT); + CHECK(gGL.getPointSize() == 4.f); + checkCachedStencil(GL_EQUAL, 3, 0x0F, GL_ZERO, GL_INCR, GL_REPLACE, 0xF0); + U32 rgb = 0, alpha = 0; + gGL.getBlendEquation(rgb, alpha); + CHECK(rgb == (U32)GL_FUNC_ADD); + CHECK(alpha == (U32)GL_MAX); + + // Behind gGL's back, then the same asked for again. + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glCullFace(GL_BACK); + glPointSize(1.f); + glStencilFunc(GL_ALWAYS, 0, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilMask(0xFF); + glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + set(); + { + INFO("an agreeing cache issues nothing"); + CHECK((U32)glInteger(GL_POLYGON_MODE) == (U32)GL_FILL); + CHECK((U32)glInteger(GL_CULL_FACE_MODE) == (U32)GL_BACK); + CHECK(glFloat(GL_POINT_SIZE) == 1.f); + checkStencil(GL_ALWAYS, 0, 0xFF, GL_KEEP, GL_KEEP, GL_KEEP, 0xFF); + CHECK((U32)glInteger(GL_BLEND_EQUATION_ALPHA) == (U32)GL_FUNC_ADD); + } + + gGL.refreshState(); + { + INFO("refreshState puts back what gGL holds"); + check_set(); + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // A change of any of it draws what gGL has batched under the state before; a set the cache agrees with leaves the + // batch where it is. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: a change of raster state draws what is batched first, and a redundant set does not", "[alrenderstate_gl]") + { + gGL.setViewport(0, 0, 8, 8); + CHECK_FALSE(drawsBatched([] { gGL.setPolygonMode(gGL.getPolygonMode()); })); + CHECK(drawsBatched([] { gGL.setPolygonMode(GL_LINE); })); + CHECK_FALSE(drawsBatched([] { gGL.setCullFace(gGL.getCullFace()); })); + CHECK(drawsBatched([] { gGL.setCullFace(GL_FRONT); })); + CHECK_FALSE(drawsBatched([] { gGL.setPointSize(gGL.getPointSize()); })); + CHECK(drawsBatched([] { gGL.setPointSize(3.f); })); + CHECK_FALSE(drawsBatched([] { gGL.setStencilFunc(GL_ALWAYS, 0, ~0u); })); + CHECK(drawsBatched([] { gGL.setStencilFunc(GL_EQUAL, 1, 0xFF); })); + CHECK_FALSE(drawsBatched([] { gGL.setStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); })); + CHECK(drawsBatched([] { gGL.setStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); })); + CHECK_FALSE(drawsBatched([] { gGL.setStencilMask(~0u); })); + CHECK(drawsBatched([] { gGL.setStencilMask(0x0F); })); + CHECK_FALSE(drawsBatched([] { gGL.setBlendEquation(GL_FUNC_ADD, GL_FUNC_ADD); })); + CHECK(drawsBatched([] { gGL.setBlendEquation(GL_FUNC_ADD, GL_MAX); })); + CHECK_FALSE(drawsBatched([] { gGL.setViewport(0, 0, 8, 8); })); + CHECK(drawsBatched([] { gGL.setViewport(0, 0, 4, 4); })); + U8 rgba[4] = { 0 }; + CHECK(drawsBatched([&] { gGL.readPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba); })); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // Every viewport goes through gGL, a target's included: binding one sets the target's size, and flushing the + // last puts back the window's, gGLViewport. refreshState puts back the one gGL holds. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: a target's bind and flush set the viewport through gGL", "[alrenderstate_gl]") + { + setUp(); + gGL.setViewport(1, 2, 3, 4); + UNSCOPED_INFO("what gGL is given, GL holds"); + CHECK(glViewportIs(1, 2, 3, 4)); + + mTarget.bindTarget(); + UNSCOPED_INFO("a bound target's size"); + CHECK(viewportIs(0, 0, SIZE, SIZE)); + CHECK(glViewportIs(0, 0, SIZE, SIZE)); + mTarget.flush(); + UNSCOPED_INFO("the window's, once no target is bound"); + CHECK(viewportIs(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3])); + CHECK(glViewportIs(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3])); + + glViewport(0, 0, 1, 1); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + UNSCOPED_INFO("an agreeing cache issues nothing"); + CHECK(glViewportIs(0, 0, 1, 1)); + gGL.refreshState(); + UNSCOPED_INFO("refreshState puts back what gGL holds"); + CHECK(glViewportIs(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3])); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // ALGLStencil turns the test on with what it is given, through gGL, and on exit turns it off with the rest at GL's + // defaults. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: ALGLStencil sets its state through gGL and leaves GL's defaults", "[alrenderstate_gl]") + { + { + ALGLStencil stencil(GL_EQUAL, 3, 0x0F, GL_KEEP, GL_INCR, GL_REPLACE, 0xF0); + UNSCOPED_INFO("the test is on in the scope"); + CHECK(glIsEnabled(GL_STENCIL_TEST) == GL_TRUE); + checkCachedStencil(GL_EQUAL, 3, 0x0F, GL_KEEP, GL_INCR, GL_REPLACE, 0xF0); + checkStencil(GL_EQUAL, 3, 0x0F, GL_KEEP, GL_INCR, GL_REPLACE, 0xF0); + } + UNSCOPED_INFO("the test is off again"); + CHECK(glIsEnabled(GL_STENCIL_TEST) == GL_FALSE); + checkCachedStencil(GL_ALWAYS, 0, ~0u, GL_KEEP, GL_KEEP, GL_KEEP, ~0u); + checkStencil(GL_ALWAYS, 0, ~0u, GL_KEEP, GL_KEEP, GL_KEEP, ~0u); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // A clear writes the stencil only through the write mask gGL holds. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: a stencil clear writes only the bits of gGL's write mask", "[alrenderstate_gl]") + { + LLRenderTarget stenciled; + REQUIRE(stenciled.allocate(SIZE, SIZE, GL_RGBA8, true, true)); + stenciled.bindTarget(); + glClearStencil(0xFF); + gGL.setStencilMask(0x0F); + stenciled.clear(GL_STENCIL_BUFFER_BIT); + U8 value = 0; + gGL.readPixels(1, 1, 1, 1, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, &value); + CHECK((U32)value == 0x0Fu); + + gGL.setStencilMask(~0u); + glClearStencil(0); + stenciled.flush(); + stenciled.release(); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // The clip control and depth clear value follow sReverseZ, issued by applyDepthConvention. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: applyDepthConvention issues the clip depth and depth clear of sReverseZ", "[alrenderstate_gl]") + { + for (bool reverse : { true, false, true }) + { + CAPTURE(reverse); + LLRender::sReverseZ = reverse; + LLRender::applyDepthConvention(); + GLdouble clear_depth = -1.0; + glGetDoublev(GL_DEPTH_CLEAR_VALUE, &clear_depth); + CHECK(clear_depth == (reverse ? 0.0 : 1.0)); + if (gGLManager.mHasClipControl) + { + CHECK((U32)glInteger(GL_CLIP_DEPTH_MODE) == (U32)(reverse ? GL_ZERO_TO_ONE : GL_NEGATIVE_ONE_TO_ONE)); + CHECK((U32)glInteger(GL_CLIP_ORIGIN) == (U32)GL_LOWER_LEFT); + } + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // readPixels reads the framebuffer bound for reading. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: readPixels reads the bound framebuffer", "[alrenderstate_gl]") + { + setUp(); + mTarget.bindTarget(); + gGL.setClearColor(LLColor4::red); + mTarget.clear(); + U8 rgba[4] = { 0 }; + gGL.readPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + CHECK(LLColor4U(rgba[0], rgba[1], rgba[2], rgba[3]) == RED); + mTarget.flush(); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // copyFromWindow copies from the window's framebuffer whatever is bound, and leaves bound what was. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: copyFromWindow copies the window's framebuffer into attachment 0", "[alrenderstate_gl]") + { + setUp(); + LLRenderTarget bound; + REQUIRE(bound.allocate(SIZE, SIZE, GL_RGBA8)); + // Every target red, the window green. + mTarget.bindTarget(); + gGL.setClearColor(LLColor4::red); + mTarget.clear(); + mTarget.flush(); + gGL.setClearColor(LLColor4::green); + gGL.clear(GL_COLOR_BUFFER_BIT); + bound.bindTarget(); + gGL.setClearColor(LLColor4::red); + bound.clear(); + + mTarget.copyFromWindow(0, 0, SIZE, SIZE); + GLint draw = -1, read = -1; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw); + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read); + UNSCOPED_INFO("what was bound stays bound"); + CHECK((U32)draw == LLRenderTarget::sCurFBO); + CHECK((U32)read == LLRenderTarget::sCurFBO); + bound.flush(); + bound.release(); + + mTarget.bindTarget(); + UNSCOPED_INFO("the window's colour, not the bound target's"); + CHECK(pixel(1, 1) == GREEN); + CHECK(pixel(SIZE - 1, SIZE - 1) == GREEN); + mTarget.flush(); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // The driver's own vendor and renderer strings, which the uppercased ones are made from. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: gGLManager keeps GL_VENDOR and GL_RENDERER in the driver's case", "[alrenderstate_gl]") + { + const std::string vendor = ll_safe_string((const char*)glGetString(GL_VENDOR)); + const std::string renderer = ll_safe_string((const char*)glGetString(GL_RENDERER)); + CHECK(gGLManager.mGLVendorString == vendor); + CHECK(gGLManager.mGLRendererString == renderer); + std::string upper = renderer; + LLStringUtil::toUpper(upper); + CHECK(gGLManager.mGLRenderer == upper); + CHECK_FALSE(gGLManager.mGLVersionString.empty()); + } } diff --git a/indra/llrender/tests/altextureblit_gl_test.cpp b/indra/llrender/tests/altextureblit_gl_test.cpp new file mode 100644 index 0000000000..87b96d06c5 --- /dev/null +++ b/indra/llrender/tests/altextureblit_gl_test.cpp @@ -0,0 +1,189 @@ +/** + * @file altextureblit_gl_test.cpp + * @brief ALTextureBlit on the hidden window: a copy from a 2D or rectangle texture into a 2D one, upside down or + * not, that leaves bound the framebuffer it found and keeps its framebuffers until released. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../altextureblit.h" +#include "../llgl.h" +#include "../llglheaders.h" +#include "../llrender.h" +#include "../llrendertarget.h" + +#include "llheadlessgl_fixture.h" + +#include + +#include + +namespace +{ + constexpr S32 W = 4; + constexpr S32 H = 4; + + // Row y of the source is the colour (y + 1) * 40 in red and 255 - y in green, so every row differs from every + // other and from nothing. + U32 rowColour(S32 y) + { + const U32 r = (U32)(y + 1) * 40; + const U32 g = 255 - (U32)y; + return r | (g << 8) | (0xFFu << 24); + } + + struct altextureblit_data + { + ~altextureblit_data() + { + mBlit.release(); + if (mTarget.isBoundInStack()) + { + mTarget.flush(); + } + mTarget.release(); + for (U32 texture : { mSrc, mDst }) + { + if (texture) + { + gGL.forgetTextures(1, &texture); + glDeleteTextures(1, &texture); + } + } + } + + // A source of `target` with its rows as rowColour gives them, and a destination of 0. + void setUp(GLenum target) + { + mSrcTarget = target; + std::vector rows((size_t)W * H); + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + rows[(size_t)y * W + x] = rowColour(y); + } + } + glGenTextures(1, &mSrc); + glBindTexture(target, mSrc); + glTexStorage2D(target, 1, GL_RGBA8, W, H); + glTexSubImage2D(target, 0, 0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, rows.data()); + glBindTexture(target, 0); + + const std::vector zero((size_t)W * H, 0); + glGenTextures(1, &mDst); + glBindTexture(GL_TEXTURE_2D, mDst); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, zero.data()); + glBindTexture(GL_TEXTURE_2D, 0); + gGL.getTextureSlot(0)->unbind(); + } + + // Row y of the destination, the colour of its first texel, all of its texels alike or 0. + U32 dstRow(S32 y) + { + std::vector texels((size_t)W * H, 0); + glBindTexture(GL_TEXTURE_2D, mDst); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + glBindTexture(GL_TEXTURE_2D, 0); + const U32 first = texels[(size_t)y * W]; + for (S32 x = 1; x < W; ++x) + { + if (texels[(size_t)y * W + x] != first) + { + return 0; + } + } + return first; + } + + ll_test::SharedGLScope mGL; + ALTextureBlit mBlit; + LLRenderTarget mTarget; + GLenum mSrcTarget = GL_TEXTURE_2D; + U32 mSrc = 0; + U32 mDst = 0; + }; + + TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: a 2D texture is copied row for row", "[altextureblit_gl]") + { + setUp(GL_TEXTURE_2D); + mBlit.blit(GL_TEXTURE_2D, mSrc, mDst, W, H, false); + for (S32 y = 0; y < H; ++y) + { + CAPTURE(y); + CHECK(dstRow(y) == rowColour(y)); + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: a flipped copy puts the rows in the opposite order", "[altextureblit_gl]") + { + setUp(GL_TEXTURE_2D); + mBlit.blit(GL_TEXTURE_2D, mSrc, mDst, W, H, true); + for (S32 y = 0; y < H; ++y) + { + CAPTURE(y); + CHECK(dstRow(y) == rowColour(H - 1 - y)); + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: a rectangle texture is copied as a 2D one is", "[altextureblit_gl]") + { + setUp(GL_TEXTURE_RECTANGLE); + mBlit.blit(GL_TEXTURE_RECTANGLE, mSrc, mDst, W, H, true); + for (S32 y = 0; y < H; ++y) + { + CAPTURE(y); + CHECK(dstRow(y) == rowColour(H - 1 - y)); + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // The framebuffer bound before the copy is bound after it, for drawing and reading, and the copy's framebuffers + // are kept for the next copy until released. + TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: the framebuffer bound before is bound after, and release drops the copy's", "[altextureblit_gl]") + { + setUp(GL_TEXTURE_2D); + REQUIRE(mTarget.allocate(W, H, GL_RGBA8)); + mTarget.bindTarget(); + CHECK_FALSE(mBlit.hasFramebuffers()); + mBlit.blit(GL_TEXTURE_2D, mSrc, mDst, W, H, false); + GLint draw = -1, read = -1; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw); + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read); + CHECK((U32)draw == LLRenderTarget::sCurFBO); + CHECK((U32)read == LLRenderTarget::sCurFBO); + CHECK(LLRenderTarget::sCurFBO != 0); + CHECK(mBlit.hasFramebuffers()); + mTarget.flush(); + + mBlit.release(); + CHECK_FALSE(mBlit.hasFramebuffers()); + UNSCOPED_INFO("a released blit makes its framebuffers again"); + mBlit.blit(GL_TEXTURE_2D, mSrc, mDst, W, H, true); + CHECK(dstRow(0) == rowColour(H - 1)); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } +} diff --git a/indra/llrender/tests/llheadlessgl_fixture.h b/indra/llrender/tests/llheadlessgl_fixture.h index 57b11afd63..baf9307072 100644 --- a/indra/llrender/tests/llheadlessgl_fixture.h +++ b/indra/llrender/tests/llheadlessgl_fixture.h @@ -631,6 +631,13 @@ namespace ll_test glGetIntegerv(GL_CLIP_DEPTH_MODE, &mClipDepthMode); } mClearColor = gGL.getClearColor(); + mPolygonMode = gGL.getPolygonMode(); + mCullFace = gGL.getCullFace(); + mPointSize = gGL.getPointSize(); + gGL.getStencilFunc(mStencilFunc, mStencilRef, mStencilReadMask); + gGL.getStencilOp(mStencilFail, mStencilDepthFail, mStencilPass); + mStencilWriteMask = gGL.getStencilMask(); + gGL.getBlendEquation(mBlendEquationRGB, mBlendEquationAlpha); mReverseZ = LLRender::sReverseZ; mCompute = LLRender::sCompute; mMatrixMode = gGL.getMatrixMode(); @@ -660,6 +667,12 @@ namespace ll_test glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mDrawFramebuffer); glBindFramebuffer(GL_READ_FRAMEBUFFER, mReadFramebuffer); + S32 viewport[4]; + gGL.getViewport(viewport); + if (viewport[2] >= 0) + { + gGL.setViewport(mViewport[0], mViewport[1], mViewport[2], mViewport[3]); + } glViewport(mViewport[0], mViewport[1], mViewport[2], mViewport[3]); S32 scissor[4]; @@ -684,6 +697,13 @@ namespace ll_test LLRender::sCompute = mCompute; gGL.setClearColor(mClearColor); + gGL.setPolygonMode(mPolygonMode); + gGL.setCullFace(mCullFace); + gGL.setPointSize(mPointSize); + gGL.setStencilFunc(mStencilFunc, mStencilRef, mStencilReadMask); + gGL.setStencilOp(mStencilFail, mStencilDepthFail, mStencilPass); + gGL.setStencilMask(mStencilWriteMask); + gGL.setBlendEquation(mBlendEquationRGB, mBlendEquationAlpha); gGL.matrixMode(LLRender::MM_TEXTURE0); gGL.loadIdentity(); gGL.matrixMode(LLRender::MM_PROJECTION); @@ -730,6 +750,18 @@ namespace ll_test GLint mClipOrigin = GL_LOWER_LEFT; GLint mClipDepthMode = GL_NEGATIVE_ONE_TO_ONE; LLColor4 mClearColor; + U32 mPolygonMode = GL_FILL; + U32 mCullFace = GL_BACK; + F32 mPointSize = 1.f; + U32 mStencilFunc = GL_ALWAYS; + S32 mStencilRef = 0; + U32 mStencilReadMask = ~0u; + U32 mStencilFail = GL_KEEP; + U32 mStencilDepthFail = GL_KEEP; + U32 mStencilPass = GL_KEEP; + U32 mStencilWriteMask = ~0u; + U32 mBlendEquationRGB = GL_FUNC_ADD; + U32 mBlendEquationAlpha = GL_FUNC_ADD; bool mReverseZ = false; bool mCompute = false; LLRender::eMatrixMode mMatrixMode = LLRender::MM_MODELVIEW; From 45cd9440a113752c2764a81790a7f13e9e79cd9a Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 05:59:37 -0400 Subject: [PATCH 2/8] newview reaches GL through llrender Of the 133 raw gl* calls newview made in 19 files (comments, tests and #if 0 blocks aside), 131 go to LLRender, LLRenderTarget, LLImageGL, ALTextureBlit, ALGPUReadback and gGLManager: - Polygon mode, cull face, point size, the SMAA pass's stencil write mask, the water's blend equation and every viewport go through gGL's cached setters, and every glClear through gGL.clear. A change now draws what gGL has batched first, as the line width and scissor box already did, so the explicit flushes before some of them go. - The snapshot, simple snapshot and scene pixel probe reads are gGL.readPixels. DebugShowColor read the pixel under the mouse with glReadPixels every frame, which waits for the GPU and for a threaded driver's own thread: it reads through an ALGPUReadback of three slots now, and shows each pixel a frame or two late, at the coordinates it was read at. - updateReverseZ's clip control and depth clear are LLRender::applyDepthConvention. - The scene monitor's capture is LLRenderTarget::copyFromWindow. - The CEF interop's three copies of the flip blit are ALTextureBlit, and its textures' names come and go through LLImageGL, so the slots forget them when they are deleted. - The About floater's and the feature table's GPU strings are gGLManager's own, not glGetString again. - clearSceneMap set its clear colour with raw glClearColor, leaving gGL's cache stale: the next setClearColor of the old colour was skipped and its clear came out black. It goes through gGL now. - validate_framebuffer_object had no caller. Two stay: forceErrorDriverCrash's glDeleteTextures(1, NULL), whose point is to crash inside the driver, and the Linux dma-buf import's glEGLImageTargetTexture2DOES, which completes the egl* calls around it. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llappviewer.cpp | 6 +- indra/newview/llcefaccelinterop.cpp | 91 +++++------------- indra/newview/lldrawpoolwater.cpp | 6 +- indra/newview/lldynamictexture.cpp | 6 +- indra/newview/llfeaturemanager.cpp | 2 +- indra/newview/llfloaterimagepreview.cpp | 2 +- indra/newview/llgltfmaterialpreviewmgr.cpp | 2 +- indra/newview/llheroprobemanager.cpp | 2 +- indra/newview/llmodelpreview.cpp | 18 ++-- indra/newview/llreflectionmapmanager.cpp | 6 +- indra/newview/llscenemonitor.cpp | 12 +-- indra/newview/llspatialpartition.cpp | 16 ++-- indra/newview/llterrainpaintmap.cpp | 2 +- indra/newview/llviewercamera.cpp | 2 +- indra/newview/llviewerdisplay.cpp | 15 ++- indra/newview/llviewerjoint.cpp | 4 +- indra/newview/llviewerwindow.cpp | 60 +++++++++--- indra/newview/pipeline.cpp | 103 +++++++-------------- indra/newview/rlveffects.cpp | 2 +- 19 files changed, 147 insertions(+), 210 deletions(-) diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 88d466a5cd..565fb4490a 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -3845,8 +3845,8 @@ LLSD LLAppViewer::getViewerInfo() const // Moved hack adjustment to Windows memory size into llsys.cpp info["CONCURRENCY"] = LLSD::Integer((S32) std::thread::hardware_concurrency()); info["OS_VERSION"] = LLOSInfo::instance().getOSString(); - info["GRAPHICS_CARD_VENDOR"] = ll_safe_string((const char*)(glGetString(GL_VENDOR))); - info["GRAPHICS_CARD"] = ll_safe_string((const char*)(glGetString(GL_RENDERER))); + info["GRAPHICS_CARD_VENDOR"] = gGLManager.mGLVendorString; + info["GRAPHICS_CARD"] = gGLManager.mGLRendererString; #if LL_WINDOWS std::string drvinfo; @@ -3889,7 +3889,7 @@ LLSD LLAppViewer::getViewerInfo() const // [RLVa:KB] - Checked: 2010-04-18 (RLVa-1.2.0) info["RLV_VERSION"] = (rlv_handler_t::isEnabled()) ? RlvStrings::getVersionAbout() : "(disabled)"; // [/RLVa:KB] - info["OPENGL_VERSION"] = ll_safe_string((const char*)(glGetString(GL_VERSION))); + info["OPENGL_VERSION"] = gGLManager.mGLVersionString; // Settings diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp index 4554f00a7a..e5046fb5cc 100644 --- a/indra/newview/llcefaccelinterop.cpp +++ b/indra/newview/llcefaccelinterop.cpp @@ -28,9 +28,10 @@ #if LL_WINDOWS +#include "altextureblit.h" #include "llgl.h" // viewer GL entry points + the global wglDX* pointers +#include "llimagegl.h" #include "llrender.h" -#include "llrendertarget.h" #include "lldxhardware.h" // shared D3D11 device + GL interop device (gDXHardware) #include @@ -62,14 +63,13 @@ namespace int height = 0; bool logged_register = false; - // FBOs for the flip+convert blit (created once, reused). - GLuint read_fbo = 0; - GLuint draw_fbo = 0; + // The flip+convert blit, whose framebuffers are made once and reused. + ALTextureBlit blit; void releaseStable() { if (local_obj) { wglDXUnregisterObjectNV(interopDevice(), local_obj); local_obj = nullptr; } - if (local_gl) { glDeleteTextures(1, &local_gl); local_gl = 0; } + if (local_gl) { LLImageGL::deleteTextures(1, &local_gl); local_gl = 0; } if (local) { local->Release(); local = nullptr; } if (mutex) { mutex->Release(); mutex = nullptr; } if (stable) { stable->Release(); stable = nullptr; } @@ -104,8 +104,7 @@ void LLCEFAccelInterop::shutdown() } WinAccel* w = (WinAccel*)mImpl; w->releaseStable(); - if (w->read_fbo) { glDeleteFramebuffers(1, &w->read_fbo); w->read_fbo = 0; } - if (w->draw_fbo) { glDeleteFramebuffers(1, &w->draw_fbo); w->draw_fbo = 0; } + w->blit.release(); delete w; mImpl = nullptr; mValid = false; @@ -158,7 +157,7 @@ bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, i return false; } - glGenTextures(1, &w->local_gl); + LLImageGL::generateTextures(1, &w->local_gl); w->local_obj = wglDXRegisterObjectNV(interopDevice(), w->local, w->local_gl, GL_TEXTURE_2D, WGL_ACCESS_READ_ONLY_NV); if (!w->local_obj) { @@ -229,21 +228,7 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) return false; } - if (!w->read_fbo) glGenFramebuffers(1, &w->read_fbo); - if (!w->draw_fbo) glGenFramebuffers(1, &w->draw_fbo); - - 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); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); - - glBlitFramebuffer(0, 0, cw, ch, // src - 0, ch, cw, 0, // dst, Y-flipped - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - 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_FRAMEBUFFER, LLRenderTarget::sCurFBO); + w->blit.blit(GL_TEXTURE_2D, w->local_gl, dst_tex, cw, ch, true); wglDXUnlockObjectsNV(gl_dx, 1, &w->local_obj); return true; @@ -251,9 +236,10 @@ 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 "altextureblit.h" #include "llgl.h" +#include "llimagegl.h" #include "llrender.h" -#include "llrendertarget.h" #include #include #include @@ -270,8 +256,7 @@ namespace IOSurfaceRef surface = nullptr; int width = 0; int height = 0; - GLuint read_fbo = 0; - GLuint draw_fbo = 0; + ALTextureBlit blit; }; } @@ -287,7 +272,7 @@ bool LLCEFAccelInterop::init() return false; } MacAccel* m = new MacAccel(); - glGenTextures(1, &m->tex); + LLImageGL::generateTextures(1, &m->tex); mImpl = m; mValid = true; return true; @@ -300,9 +285,8 @@ void LLCEFAccelInterop::shutdown() return; } MacAccel* m = (MacAccel*)mImpl; - if (m->tex) { glDeleteTextures(1, &m->tex); } - if (m->read_fbo) { glDeleteFramebuffers(1, &m->read_fbo); } - if (m->draw_fbo) { glDeleteFramebuffers(1, &m->draw_fbo); } + if (m->tex) { LLImageGL::deleteTextures(1, &m->tex); } + m->blit.release(); if (m->surface) { CFRelease(m->surface); } delete m; mImpl = nullptr; @@ -377,29 +361,17 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) return false; } - if (!m->read_fbo) glGenFramebuffers(1, &m->read_fbo); - if (!m->draw_fbo) glGenFramebuffers(1, &m->draw_fbo); - - 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); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); - - glBlitFramebuffer(0, 0, cw, ch, // src - 0, ch, cw, 0, // dst, Y-flipped - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - 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_FRAMEBUFFER, LLRenderTarget::sCurFBO); + // Y-flipped: the IOSurface is top-down, the media texture bottom-up. + m->blit.blit(GL_TEXTURE_RECTANGLE_ARB, m->tex, dst_tex, cw, ch, true); return true; } #elif LL_LINUX // import the plugin's dma-buf via EGL, blit to the media texture +#include "altextureblit.h" #include "llgl.h" +#include "llimagegl.h" #include "llrender.h" -#include "llrendertarget.h" #include #include #include @@ -428,8 +400,7 @@ namespace EGLImageKHR image = EGL_NO_IMAGE_KHR; int width = 0; int height = 0; - GLuint read_fbo = 0; - GLuint draw_fbo = 0; + ALTextureBlit blit; void releaseImage() { @@ -472,7 +443,7 @@ bool LLCEFAccelInterop::init() const char* exts = eglQueryString ? eglQueryString(l->display, EGL_EXTENSIONS) : nullptr; l->has_import_modifiers = exts && strstr(exts, "EGL_EXT_image_dma_buf_import_modifiers") != nullptr; - glGenTextures(1, &l->tex); + LLImageGL::generateTextures(1, &l->tex); mImpl = l; mValid = true; return true; @@ -486,9 +457,8 @@ void LLCEFAccelInterop::shutdown() } LinuxAccel* l = (LinuxAccel*)mImpl; l->releaseImage(); - if (l->tex) { glDeleteTextures(1, &l->tex); } - if (l->read_fbo) { glDeleteFramebuffers(1, &l->read_fbo); } - if (l->draw_fbo) { glDeleteFramebuffers(1, &l->draw_fbo); } + if (l->tex) { LLImageGL::deleteTextures(1, &l->tex); } + l->blit.release(); delete l; mImpl = nullptr; mValid = false; @@ -616,21 +586,8 @@ bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) return false; } - if (!l->read_fbo) glGenFramebuffers(1, &l->read_fbo); - if (!l->draw_fbo) glGenFramebuffers(1, &l->draw_fbo); - - 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); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); - - glBlitFramebuffer(0, 0, cw, ch, // src - 0, ch, cw, 0, // dst, Y-flipped - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - 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_FRAMEBUFFER, LLRenderTarget::sCurFBO); + // Y-flipped: the dma-buf is top-down, the media texture bottom-up. + l->blit.blit(GL_TEXTURE_2D, l->tex, dst_tex, cw, ch, true); return true; } diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp index e433f38819..1a1f76d45e 100644 --- a/indra/newview/lldrawpoolwater.cpp +++ b/indra/newview/lldrawpoolwater.cpp @@ -136,8 +136,7 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) if (glint_glow) { gGL.blendFunc(LLRender::BF_ONE, LLRender::BF_ZERO, LLRender::BF_ONE, LLRender::BF_ONE); - gGL.flush(); - glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX); + gGL.setBlendEquation(GL_FUNC_ADD, GL_MAX); } gGL.setColorMask(true, glint_glow); @@ -364,8 +363,7 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) if (glint_glow) { - gGL.flush(); - glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + gGL.setBlendEquation(GL_FUNC_ADD, GL_FUNC_ADD); gGL.setSceneBlendType(LLRender::BT_ALPHA); } gGL.setColorMask(true, false); diff --git a/indra/newview/lldynamictexture.cpp b/indra/newview/lldynamictexture.cpp index 46b6a9dc04..d25f9e2717 100644 --- a/indra/newview/lldynamictexture.cpp +++ b/indra/newview/lldynamictexture.cpp @@ -132,10 +132,10 @@ void LLViewerDynamicTexture::preRender(bool clear_depth) mCamera.setView(camera->getView()); mCamera.setNear(camera->getNear()); - glViewport(mOrigin.mX, mOrigin.mY, mFullWidth, mFullHeight); + gGL.setViewport(mOrigin.mX, mOrigin.mY, mFullWidth, mFullHeight); if (clear_depth) { - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); } } @@ -201,7 +201,7 @@ bool LLViewerDynamicTexture::updateAllInstances() llassert(dynamicTexture->getFullWidth() <= (S32)renderTarget.getWidth()); llassert(dynamicTexture->getFullHeight() <= (S32)renderTarget.getHeight()); - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); gGL.color4f(1.f, 1.f, 1.f, 1.f); dynamicTexture->setBoundTarget(&renderTarget); diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp index 26f17f340f..9b039b30bc 100644 --- a/indra/newview/llfeaturemanager.cpp +++ b/indra/newview/llfeaturemanager.cpp @@ -552,7 +552,7 @@ bool checkRDNA35() */ bool extractGLDeviceModel(std::string& device_name) { - const std::string gl_string = ll_safe_string((const char*)(glGetString(GL_RENDERER))); + const std::string gl_string = gGLManager.mGLRendererString; #if LL_LINUX // The kernel release, 'uname -r'. The OS string leads with the // distribution's name. diff --git a/indra/newview/llfloaterimagepreview.cpp b/indra/newview/llfloaterimagepreview.cpp index f7e7f58193..c444c98be2 100644 --- a/indra/newview/llfloaterimagepreview.cpp +++ b/indra/newview/llfloaterimagepreview.cpp @@ -1010,7 +1010,7 @@ bool LLImagePreviewSculpted::render() gGL.matrixMode(LLRender::MM_MODELVIEW); gGL.popMatrix(); - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); LLVector3 target_pos(0, 0, 0); diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index 77fb7f9603..b61e1f6ee4 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -613,7 +613,7 @@ bool LLGLTFPreviewTexture::render() // Final render // Nested post-processing targets restore the full preview target's viewport. - glViewport(mOrigin.mX, mOrigin.mY, mFullWidth, mFullHeight); + gGL.setViewport(mOrigin.mX, mOrigin.mY, mFullWidth, mFullHeight); gDeferredPostNoDoFProgram.bind(); // From LLPipeline::renderFinalize: "Whatever is last in the above post processing chain should _always_ be rendered directly here. If not, expect problems." diff --git a/indra/newview/llheroprobemanager.cpp b/indra/newview/llheroprobemanager.cpp index c33ea1d4c9..04e4511f1f 100644 --- a/indra/newview/llheroprobemanager.cpp +++ b/indra/newview/llheroprobemanager.cpp @@ -476,7 +476,7 @@ void LLHeroProbeManager::generateRadiance(LLReflectionMap* probe) if (i != mMipChain.size() - 1) { res /= 2; - glViewport(0, 0, res, res); + gGL.setViewport(0, 0, res, res); } } diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index 26d6c864dc..e13fc7e09d 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -3600,9 +3600,9 @@ bool LLModelPreview::render() if (show_edges) { gGL.setLineWidth(PREVIEW_EDGE_WIDTH); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); buffer->drawRange(LLRender::TRIANGLES, 0, buffer->getNumVerts() - 1, buffer->getNumIndices(), 0); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); gGL.setLineWidth(1.f); } buffer->unmapBuffer(); @@ -3612,7 +3612,7 @@ bool LLModelPreview::render() if (show_physics) { - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); for (U32 pass = 0; pass < 2; pass++) { @@ -3727,10 +3727,10 @@ bool LLModelPreview::render() gGL.diffuseColor4fv(PREVIEW_PSYH_EDGE_COL.mV); gGL.setLineWidth(PREVIEW_PSYH_EDGE_WIDTH); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); buffer->drawRange(LLRender::TRIANGLES, 0, buffer->getNumVerts() - 1, buffer->getNumIndices(), 0); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); gGL.setLineWidth(1.f); buffer->unmapBuffer(); @@ -3744,7 +3744,7 @@ bool LLModelPreview::render() if (mHasDegenerate) { gGL.setLineWidth(PREVIEW_DEG_EDGE_WIDTH); - glPointSize(PREVIEW_DEG_POINT_SIZE); + gGL.setPointSize(PREVIEW_DEG_POINT_SIZE); gPipeline.enableLightsFullbright(); //show degenerate triangles LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); @@ -3814,7 +3814,7 @@ bool LLModelPreview::render() gGL.popMatrix(); } gGL.setLineWidth(1.f); - glPointSize(1.f); + gGL.setPointSize(1.f); gPipeline.enableLightsPreview(); gGL.setSceneBlendType(LLRender::BT_ALPHA); } @@ -3936,9 +3936,9 @@ bool LLModelPreview::render() gGL.getTextureSlot(0)->unbind(); gGL.diffuseColor4fv(PREVIEW_EDGE_COL.mV); gGL.setLineWidth(PREVIEW_EDGE_WIDTH); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); buffer->draw(LLRender::TRIANGLES, buffer->getNumIndices(), 0); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); gGL.setLineWidth(1.f); } } diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp index 129de9b017..0af3c8c415 100644 --- a/indra/newview/llreflectionmapmanager.cpp +++ b/indra/newview/llreflectionmapmanager.cpp @@ -1167,7 +1167,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face) if (i != mMipChain.size() - 1) { res /= 2; - glViewport(0, 0, res, res); + gGL.setViewport(0, 0, res, res); } } @@ -1196,7 +1196,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face) program.uniform1i(LLShaderMgr::U_WIDTH, (S32)sh_res); mSHCoeffs.bindTarget(); - glViewport(0, probe->mCubeIndex, LL_SH_PACKED_TEXELS, 1); + gGL.setViewport(0, probe->mCubeIndex, LL_SH_PACKED_TEXELS, 1); mVertexBuffer->setBuffer(); mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4); @@ -1226,7 +1226,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face) mSHPartial.bindTarget(); // bindTarget sets the viewport to the whole target, which is exactly this // strip; restated because the draw depends on it. - glViewport(0, 0, LL_SH_COEFF_COUNT, sh_rows); + gGL.setViewport(0, 0, LL_SH_COEFF_COUNT, sh_rows); mVertexBuffer->setBuffer(); mVertexBuffer->drawArrays(gGL.TRIANGLE_STRIP, 0, 4); diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index 3fa4f0b4b9..ee23e84c86 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -304,17 +304,7 @@ void LLSceneMonitor::capture() last_capture_frame = gFrameCount; LLRenderTarget& cur_target = getCaptureTarget(); - - U32 old_FBO = LLRenderTarget::sCurFBO; - - gGL.getTextureSlot(0)->bind(&cur_target); - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); //point to the main frame buffer. - - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, cur_target.getWidth(), cur_target.getHeight()); //copy the content - - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBindFramebuffer(GL_FRAMEBUFFER, old_FBO); + cur_target.copyFromWindow(0, 0, cur_target.getWidth(), cur_target.getHeight()); mDiffState = NEED_DIFF; } diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp index b266963e18..2f0a3ae3b2 100644 --- a/indra/newview/llspatialpartition.cpp +++ b/indra/newview/llspatialpartition.cpp @@ -1678,7 +1678,7 @@ void renderOctree(LLSpatialGroup* group) { LLGLDepthTest gl_depth(false, false); gGL.flush(); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); gGL.diffuseColor4f(1,0,0,group->mBuilt); gGL.setLineWidth(5.f); @@ -1779,7 +1779,7 @@ void renderOctree(LLSpatialGroup* group) gGL.popMatrix(); } } - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); gDebugProgram.bind(); // make sure non-rigged variant is bound gGL.diffuseColor4f(1,1,1,1); } @@ -2598,14 +2598,14 @@ void renderPhysicsShapes(LLSpatialGroup* group, bool wireframe) LLVertexBuffer* buff = face->getVertexBuffer(); if (buff) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); buff->setBuffer(); gGL.diffuseColor4f(0.2f, 0.5f, 0.3f, 0.5f); buff->draw(LLRender::TRIANGLES, buff->getNumIndices(), 0); gGL.diffuseColor4f(0.2f, 1.f, 0.3f, 0.75f); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); buff->draw(LLRender::TRIANGLES, buff->getNumIndices(), 0); } } @@ -3001,7 +3001,7 @@ void renderRaycast(LLDrawable* drawablep) dir.setSub(end, start); gGL.flush(); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); { //render face positions @@ -3020,7 +3020,7 @@ void renderRaycast(LLDrawable* drawablep) } gGL.popMatrix(); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); } } } @@ -3608,7 +3608,7 @@ void LLSpatialPartition::renderDebug() LLGLEnable blend(GL_BLEND); LLGLDepthTest depth_under(GL_TRUE, GL_FALSE, GL_GREATER); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); gGL.diffuseColor4f(0.5f, 0.0f, 0, 0.25f); LLGLEnable offset(GL_POLYGON_OFFSET_LINE); @@ -3617,7 +3617,7 @@ void LLSpatialPartition::renderDebug() LLOctreeRenderXRay xray(camera); xray.traverse(mOctree); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); } } gDebugProgram.unbind(); diff --git a/indra/newview/llterrainpaintmap.cpp b/indra/newview/llterrainpaintmap.cpp index 90d5fd3da1..bc8b26599d 100644 --- a/indra/newview/llterrainpaintmap.cpp +++ b/indra/newview/llterrainpaintmap.cpp @@ -111,7 +111,7 @@ bool LLTerrainPaintMap::bakeHeightNoiseIntoPBRPaintMapRGB(const LLViewerRegion& camera.lookAt(camera_origin, region_center, LLVector3::y_axis); camera.setAspect(F32(scratch_target.getWidth()) / F32(scratch_target.getHeight())); const LLRect texture_rect(0, scratch_target.getHeight(), scratch_target.getWidth(), 0); - glViewport(texture_rect.mLeft, texture_rect.mBottom, texture_rect.getWidth(), texture_rect.getHeight()); + gGL.setViewport(texture_rect.mLeft, texture_rect.mBottom, texture_rect.getWidth(), texture_rect.getHeight()); const LLMatrix4a modelview = camera.frameModelview(); gGL.matrixMode(LLRender::MM_MODELVIEW); gGL.loadMatrix(modelview); diff --git a/indra/newview/llviewercamera.cpp b/indra/newview/llviewercamera.cpp index 62300f2bba..5075cea295 100644 --- a/indra/newview/llviewercamera.cpp +++ b/indra/newview/llviewercamera.cpp @@ -358,7 +358,7 @@ void LLViewerCamera::setPerspective(bool for_selection, constexpr F32 PREVIEW_FAR = 512.f; z_far = PREVIEW_FAR; } - glViewport(x, y_from_bot, width, height); + gGL.setViewport(x, y_from_bot, width, height); gGLViewport[0] = x; gGLViewport[1] = y_from_bot; gGLViewport[2] = width; diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 683154fa1f..524e7313d9 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -181,7 +181,7 @@ void display_startup() LLGLState::checkStates(); - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // | GL_STENCIL_BUFFER_BIT); LLGLSUIDefault gls_ui; gPipeline.disableLights(); @@ -519,8 +519,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { //skip render on frames where window has been resized LL_DEBUGS("Window") << "Resizing window" << LL_ENDL; LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Resize Window"); - gGL.flush(); - glClear(GL_COLOR_BUFFER_BIT); + gGL.clear(GL_COLOR_BUFFER_BIT); gViewerWindow->getWindow()->swapBuffers(); gGL.endFrame(); gPipeline.applyRenderInputs(); @@ -782,7 +781,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) if (LLViewerDynamicTexture::updateAllInstances()) { gGL.setColorMask(true, true); - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); } } @@ -920,7 +919,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) LLGLState::checkStates(); } - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); } ////////////////////////////////////// @@ -1003,7 +1002,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) if(gUseWireframe) { gGL.setClearColor(LLColor4(0.5f, 0.5f, 0.5f, 0.f)); - glClear(GL_COLOR_BUFFER_BIT); + gGL.clear(GL_COLOR_BUFFER_BIT); } LLAppViewer::instance()->pingMainloopTimeout("Display:RenderStart"); @@ -1313,7 +1312,7 @@ void display_cube_face() gGL.setClearColor(LLColor4::transparent); gPipeline.generateSunShadow(*LLViewerCamera::getInstance()); - glClear(GL_DEPTH_BUFFER_BIT); // | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); // | GL_STENCIL_BUFFER_BIT); { LLViewerCamera::sCurCameraID = LLViewerCamera::CAMERA_WORLD; @@ -1880,7 +1879,7 @@ void render_ui_2d() // Render 2D UI elements that overlay the world (no z compare) // Disable wireframe mode below here, as this is HUD/menus - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); // Menu overlays, HUD, etc gViewerWindow->setup2DRender(); diff --git a/indra/newview/llviewerjoint.cpp b/indra/newview/llviewerjoint.cpp index 5d5e536d9c..d215299dd3 100644 --- a/indra/newview/llviewerjoint.cpp +++ b/indra/newview/llviewerjoint.cpp @@ -129,13 +129,13 @@ U32 LLViewerJoint::render( F32 pixelArea, bool first_pass, bool is_dummy ) else { // Render Inside (no Z buffer write) - glCullFace(GL_FRONT); + gGL.setCullFace(GL_FRONT); { LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE); triangle_count += drawShape( pixelArea, first_pass, is_dummy ); } // Render Outside (write to the Z buffer) - glCullFace(GL_BACK); + gGL.setCullFace(GL_BACK); { triangle_count += drawShape( pixelArea, false, is_dummy ); } diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index b2a93b9a65..43da1727f7 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "llagent.h" #include "llagentcamera.h" @@ -442,6 +443,13 @@ class LLDebugText LLRect mBackRectCamera1; LLRect mBackRectCamera2; + // DebugShowColor's reads of the pixel under the mouse, collected a frame or two after they are made, so the + // overlay waits on neither the GPU nor a threaded driver's own thread: where each read in flight was made, and + // the line for the newest that has landed. + ALGPUReadback mColorReads; + std::deque> mColorReadAt; + std::string mColorText; + void addText(S32 x, S32 y, const std::string &text) { mLineList.push_back(Line(text, x, y)); @@ -834,16 +842,38 @@ class LLDebugText static LLCachedControl debug_show_color(gSavedSettings, "DebugShowColor", false); if (debug_show_color() && !LLRender::sNsightDebugSupport) { - U8 color[4]; LLCoordGL coord = gViewerWindow->getCurrentMouse(); // Convert x,y to raw pixel coords S32 x_raw = (S32)ll_round(coord.mX * gViewerWindow->getWindowWidthRaw() / (F32)gViewerWindow->getWindowWidthScaled()); S32 y_raw = (S32)ll_round(coord.mY * gViewerWindow->getWindowHeightRaw() / (F32)gViewerWindow->getWindowHeightScaled()); - glReadPixels(x_raw, y_raw, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color); - addText(xpos, ypos, llformat("Pixel <%1d, %1d> R:%1d G:%1d B:%1d A:%1d", x_raw, y_raw, color[0], color[1], color[2], color[3])); - ypos += y_inc; + U8 color[4]; + if (mColorReads.isAllocated() || mColorReads.allocate(sizeof(color), 3, "DebugShowColor")) + { + // Every read that has landed, the newest last; one still in flight is collected on a later frame. + while (!mColorReadAt.empty() && mColorReads.collect(color, sizeof(color))) + { + mColorText = llformat("Pixel <%1d, %1d> R:%1d G:%1d B:%1d A:%1d", mColorReadAt.front().first, + mColorReadAt.front().second, color[0], color[1], color[2], color[3]); + mColorReadAt.pop_front(); + } + if (mColorReads.requestPixels(x_raw, y_raw, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, sizeof(color))) + { + mColorReadAt.emplace_back(x_raw, y_raw); + } + } + if (!mColorText.empty()) + { + addText(xpos, ypos, mColorText); + ypos += y_inc; + } + } + else if (mColorReads.isAllocated()) + { + mColorReads.release(); + mColorReadAt.clear(); + mColorText.clear(); } // only display these messages if we are actually rendering beacons at this moment @@ -4614,9 +4644,9 @@ void LLViewerWindow::renderSelections( bool for_gl_pick, bool pick_parcel_walls, gSphere.render(); // Render Inside - glCullFace(GL_FRONT); + gGL.setCullFace(GL_FRONT); gSphere.render(); - glCullFace(GL_BACK); + gGL.setCullFace(GL_BACK); gGL.popMatrix(); } @@ -5601,7 +5631,7 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei gSnapshotNoPost = no_post; gDisplaySwapBuffers = false; - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); setCursor(UI_CURSOR_WAIT); // Hide all the UI widgets first and draw a frame @@ -5899,7 +5929,7 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei { if (type == LLSnapshotModel::SNAPSHOT_TYPE_COLOR) { - glReadPixels( + gGL.readPixels( subimage_x_offset, out_y + subimage_y_offset, read_width, 1, cutout ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, @@ -5909,7 +5939,7 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei else if (type == LLSnapshotModel::SNAPSHOT_TYPE_DEPTH24) { LLPointer depth_line_buffer = new LLImageRaw(read_width, 1, sizeof(GLfloat)); // need to store floating point values - glReadPixels( + gGL.readPixels( subimage_x_offset, out_y + subimage_y_offset, read_width, 1, GL_DEPTH_COMPONENT, GL_FLOAT, @@ -5939,7 +5969,7 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei else // LLSnapshotModel::SNAPSHOT_TYPE_DEPTH { LLPointer depth_line_buffer = new LLImageRaw(read_width, 1, sizeof(GLfloat)); // need to store floating point values - glReadPixels( + gGL.readPixels( subimage_x_offset, out_y + subimage_y_offset, read_width, 1, GL_DEPTH_COMPONENT, GL_FLOAT, @@ -6021,7 +6051,7 @@ bool LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_ LL_PROFILE_ZONE_SCOPED_CATEGORY_APP; gDisplaySwapBuffers = false; - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); setCursor(UI_CURSOR_WAIT); bool prev_draw_ui = gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI); @@ -6093,7 +6123,7 @@ bool LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_ LLImageDataSharedLock lock(raw); - glReadPixels( + gGL.readPixels( 0, 0, image_width, image_height, @@ -6171,7 +6201,7 @@ bool LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMapArray* cubea gPipeline.pushRenderTypeMask(); - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // stencil buffer is deprecated | GL_STENCIL_BUFFER_BIT); U32 dynamic_render_types[] = { LLPipeline::RENDER_TYPE_AVATAR, @@ -6438,7 +6468,7 @@ void LLViewerWindow::setup2DViewport(S32 x_offset, S32 y_offset) gGLViewport[1] = mWindowRectRaw.mBottom + y_offset; gGLViewport[2] = mWindowRectRaw.getWidth(); gGLViewport[3] = mWindowRectRaw.getHeight(); - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); } @@ -6456,7 +6486,7 @@ void LLViewerWindow::setup3DViewport(S32 x_offset, S32 y_offset) gGLViewport[1] = mWorldViewRectRaw.mBottom + y_offset; gGLViewport[2] = mWorldViewRectRaw.getWidth(); gGLViewport[3] = mWorldViewRectRaw.getHeight(); - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); } void LLViewerWindow::revealIntroPanel() diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 14030de91a..028347e81d 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -350,8 +350,6 @@ static LLPipelineListener sPipelineListener; static LLCullResult* sCull = NULL; -void validate_framebuffer_object(); - // The colour attachments after the first that the main G-buffer has, in its formats: an avatar's impostor holds the // G-buffer's channels (generateImpostor). bool addDeferredAttachments(LLRenderTarget& target) @@ -3452,18 +3450,12 @@ void LLPipeline::updateReverseZ(bool reverse_z) recap_draw_distance_controls(); } - // Issued unconditionally, not just on a change. These are the only writers of clip control - // and clear depth in the tree, so if GL is ever reset underneath us -- a context restore, - // an external state reset -- a change-gated latch would have no path back and would leave - // reversed projections rasterizing against a [-1,1] clip volume. Four GL calls once per - // setShaders() is not worth gating. Safe when the cap is absent: sReverseZ is then false, - // the forward constants are what GL already has, and glClipControl is only reached with - // mHasClipControl true (which guarantees the entry point resolved). - if (gGLManager.mHasClipControl) - { - glClipControl(GL_LOWER_LEFT, effective ? GL_ZERO_TO_ONE : GL_NEGATIVE_ONE_TO_ONE); - } - glClearDepth(effective ? 0.0 : 1.0); + // Issued unconditionally, not just on a change. If GL is ever reset underneath us -- a + // context restore, an external state reset -- a change-gated latch would have no path back + // and would leave reversed projections rasterizing against a [-1,1] clip volume. Four GL + // calls once per setShaders() is not worth gating. Safe when the cap is absent: sReverseZ is + // then false, and the forward constants are what GL already has. + LLRender::applyDepthConvention(); // The ambient depth func and polygon offset are tracked in the semantic convention, so the // physical state has to be re-derived through the current translation. @@ -4036,7 +4028,7 @@ void LLPipeline::shiftObjects(const LLVector3 &offset) LL_PROFILE_ZONE_SCOPED_CATEGORY_PIPELINE; assertInitialized(); - glClear(GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT); gDepthDirty = true; LLVector4a offseta; @@ -5064,7 +5056,7 @@ void LLPipeline::renderGeomDeferred(LLCamera& camera, bool do_occlusion) if (gUseWireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); } if (&camera == LLViewerCamera::getInstance()) @@ -5211,7 +5203,7 @@ void LLPipeline::renderGeomDeferred(LLCamera& camera, bool do_occlusion) if (gUseWireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); } } @@ -5323,8 +5315,7 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) // water exclusion would stencil only the outlines of its prims. if (gUseWireframe) { - gGL.flush(); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); } gGLLastMatrix = NULL; @@ -5355,8 +5346,7 @@ void LLPipeline::renderGeomPostDeferred(LLCamera& camera) if (gUseWireframe) { - gGL.flush(); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); } } else @@ -5516,7 +5506,7 @@ void LLPipeline::renderPhysicsDisplay() if (wireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); } for (LLWorld::region_list_t::const_iterator iter = LLWorld::getInstance()->getRegionList().begin(); @@ -5539,7 +5529,7 @@ void LLPipeline::renderPhysicsDisplay() if (wireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + gGL.setPolygonMode(GL_FILL); } } gGL.setLineWidth(1.f); @@ -5618,9 +5608,9 @@ void LLPipeline::renderDebug() const LLColor4 clearColor = gSavedSettings.getColor4("PathfindingNavMeshClear"); gGL.setColorMask(true, true); gGL.setClearColor(LLColor4(clearColor.mV[0], clearColor.mV[1], clearColor.mV[2], 0.f)); - glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // no stencil -- deprecated | GL_STENCIL_BUFFER_BIT); + gGL.clear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // no stencil -- deprecated | GL_STENCIL_BUFFER_BIT); gGL.setColorMask(true, false); - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); } //NavMesh @@ -5649,7 +5639,7 @@ void LLPipeline::renderDebug() gPathfindingProgram.bind(); gGL.flush(); - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); gGL.setLineWidth(1.0f); gGL.flush(); } @@ -5706,7 +5696,7 @@ void LLPipeline::renderDebug() LLGLDisable cull(i >= 2 ? GL_CULL_FACE : 0); gGL.flush(); - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); //get rid of some z-fighting LLGLEnable polyOffset(GL_POLYGON_OFFSET_FILL); @@ -5732,7 +5722,7 @@ void LLPipeline::renderDebug() } LLGLEnable lineOffset(GL_POLYGON_OFFSET_LINE); - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + gGL.setPolygonMode(GL_LINE); F32 offset = gSavedSettings.getF32("PathfindingLineOffset"); @@ -5752,10 +5742,10 @@ void LLPipeline::renderDebug() } else { - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); gPathfindingProgram.uniform1f(LLShaderMgr::AMBIANCE, ambiance); llPathingLibInstance->renderNavMeshShapesVBO( render_order[i] ); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gGL.setPolygonMode(GL_LINE); } } @@ -5772,7 +5762,7 @@ void LLPipeline::renderDebug() gGL.setLineWidth(1.f); } - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); } } } @@ -5800,10 +5790,10 @@ void LLPipeline::renderDebug() if (gSavedSettings.getBOOL("PathfindingXRayWireframe")) { //draw hidden wireframe as darker and less opaque - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + gGL.setPolygonMode(GL_LINE); gPathfindingProgram.uniform1f(LLShaderMgr::AMBIANCE, 1.f); llPathingLibInstance->renderNavMesh(); - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + gGL.setPolygonMode(GL_FILL); } else { @@ -5843,7 +5833,7 @@ void LLPipeline::renderDebug() gGL.getTextureSlot(0)->bindSampled(LLViewerFetchedTexture::sWhiteImagep, ALSamplers::AnisoWrap, true); - glPointSize(8.f); + gGL.setPointSize(8.f); LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); gGL.begin(LLRender::POINTS); @@ -5868,7 +5858,7 @@ void LLPipeline::renderDebug() } gGL.end(); gGL.flush(); - glPointSize(1.f); + gGL.setPointSize(1.f); } // Debug stuff. @@ -6070,7 +6060,7 @@ void LLPipeline::renderDebug() { //render visible point cloud gGL.flush(); - glPointSize(8.f); + gGL.setPointSize(8.f); gGL.begin(LLRender::POINTS); F32* c = col+i*4; @@ -6084,7 +6074,7 @@ void LLPipeline::renderDebug() gGL.end(); gGL.flush(); - glPointSize(1.f); + gGL.setPointSize(1.f); LLVector3* ext = mShadowExtents[i]; LLVector3 pos = (ext[0]+ext[1])*0.5f; @@ -8124,33 +8114,6 @@ void apply_cube_face_rotation(U32 face) } } -void validate_framebuffer_object() -{ - GLenum status; - status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - switch(status) - { - case GL_FRAMEBUFFER_COMPLETE: - //framebuffer OK, no error. - break; - case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - // frame buffer not OK: probably means unsupported depth buffer format - LL_ERRS() << "Framebuffer Incomplete Missing Attachment." << LL_ENDL; - break; - case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - // frame buffer not OK: probably means unsupported depth buffer format - LL_ERRS() << "Framebuffer Incomplete Attachment." << LL_ENDL; - break; - case GL_FRAMEBUFFER_UNSUPPORTED: - /* choose different formats */ - LL_ERRS() << "Framebuffer unsupported." << LL_ENDL; - break; - default: - LL_ERRS() << "Unknown framebuffer status." << LL_ENDL; - break; - } -} - void LLPipeline::bindScreenToTexture() { @@ -8231,7 +8194,7 @@ void LLPipeline::serviceScenePixelProbe(LLRenderTarget* src) F32 texel[4] = { 0.f, 0.f, 0.f, 0.f }; src->bindTarget(); - glReadPixels(px, py, 1, 1, GL_RGBA, GL_FLOAT, texel); + gGL.readPixels(px, py, 1, 1, GL_RGBA, GL_FLOAT, texel); src->flush(); // A synchronous read stalls the pipe. That is the right trade here: it @@ -10114,7 +10077,7 @@ LLPipeline::SMAATargets LLPipeline::generateSMAABuffers(LLRenderTarget* src, ALT if (use_stencil) { // The stencil clear writes only the bits the stencil write mask allows. - glStencilMask(0xFF); + gGL.setStencilMask(0xFF); } dest.clear(use_stencil ? (GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) : GL_COLOR_BUFFER_BIT); @@ -10749,7 +10712,7 @@ void LLPipeline::renderFinalize() gGLViewport[1] = gViewerWindow->getWorldViewRectRaw().mBottom; gGLViewport[2] = gViewerWindow->getWorldViewRectRaw().getWidth(); gGLViewport[3] = gViewerWindow->getWorldViewRectRaw().getHeight(); - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); } Bloom bloom; @@ -10867,7 +10830,7 @@ void LLPipeline::renderFinalize() gGLViewport[1] = gViewerWindow->getWorldViewRectRaw().mBottom; gGLViewport[2] = gViewerWindow->getWorldViewRectRaw().getWidth(); gGLViewport[3] = gViewerWindow->getWorldViewRectRaw().getHeight(); - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); // [RLVa:KB] - @setsphere if (rlv_sphere && gRlvSphereProgram.isComplete() && other_target()) @@ -14769,7 +14732,7 @@ void LLPipeline::copyCutoutCoverageToAlpha() // Across the whole frame, sampled as the final blit sampled the colour, so // the two agree texel for texel whatever the scene target's resolution. const LLRect& rect = gViewerWindow->getWorldViewRectRaw(); - glViewport(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight()); + gGL.setViewport(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight()); gCopyProgram.bind(); gCopyProgram.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mRT->screen); @@ -14787,7 +14750,7 @@ void LLPipeline::clearSceneMap() LLGLSColorMask mask(true, true); LLGLDepthTest depth(GL_TRUE, GL_TRUE); mSceneMap.bindTarget(); - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); mSceneMap.clear(); mSceneMap.flush(); } diff --git a/indra/newview/rlveffects.cpp b/indra/newview/rlveffects.cpp index cfe79260d3..5bb2db6cfd 100644 --- a/indra/newview/rlveffects.cpp +++ b/indra/newview/rlveffects.cpp @@ -343,7 +343,7 @@ void RlvSphereEffect::renderPass(LLGLSLShader* pShader, const LLShaderEffectPara gGLViewport[1] = gViewerWindow->getWorldViewRectRaw().mBottom; gGLViewport[2] = gViewerWindow->getWorldViewRectRaw().getWidth(); gGLViewport[3] = gViewerWindow->getWorldViewRectRaw().getHeight(); - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + gGL.setViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); } RLV_ASSERT_DBG(pParams->m_pSrcBuffer); From c1f81b0cee95d80798d4f8192d9d695d8550d0d1 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 06:14:16 -0400 Subject: [PATCH 3/8] A pre-commit hook holds newview to the raw GL calls it makes scripts/code_tools/check_raw_gl.py counts the raw gl* calls each source file under indra/newview makes, leaving out comments, string literals and #if 0 blocks, and checks them against raw_gl_baseline.txt. A file that makes more than the baseline allows fails and names each call; one that makes fewer fails too, asking for --update, so the baseline comes down as calls move into llrender and never back up unnoticed. The baseline holds the two left on purpose. Two local pre-commit hooks run it: one on the newview sources a commit touches, and one on the whole tree when the baseline or the script changes. ARCHITECTURE.md states the rule where newview is described. test_check_raw_gl.py (unittest) covers what is and is not counted and the baseline's checks; three mutants of the checker each fail it. Co-Authored-By: Claude Opus 5.5 --- .pre-commit-config.yaml | 14 ++ doc/ARCHITECTURE.md | 2 +- scripts/code_tools/check_raw_gl.py | 200 ++++++++++++++++++++++++ scripts/code_tools/raw_gl_baseline.txt | 3 + scripts/code_tools/test_check_raw_gl.py | 119 ++++++++++++++ 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 scripts/code_tools/check_raw_gl.py create mode 100644 scripts/code_tools/raw_gl_baseline.txt create mode 100644 scripts/code_tools/test_check_raw_gl.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11cc5ac05f..5c12a13fb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,3 +18,17 @@ repos: - id: mixed-line-ending - id: trailing-whitespace files: \.(cpp|c|m|mm|h|inl|py|glsl|cmake|yaml|sh)$ + - repo: local + hooks: + - id: raw-gl-newview + name: Raw GL calls in newview + entry: python scripts/code_tools/check_raw_gl.py + language: python + files: ^indra/newview/.*\.(c|cc|cpp|cxx|h|hh|hpp|inl|m|mm)$ + exclude: ^indra/newview/tests/ + - id: raw-gl-baseline + name: Raw GL baseline + entry: python scripts/code_tools/check_raw_gl.py + language: python + files: ^scripts/code_tools/(check_raw_gl\.py|raw_gl_baseline\.txt)$ + pass_filenames: false diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 8fde2c2e87..016f1df11c 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -38,7 +38,7 @@ - **alscript** - Script Studio's language services, free of UI (depends on llcommon, llmath, and SLua's Luau with the Tailslide it carries). One folder a part, each on the include path: `core/` (problems, symbols, the lexicon, messages, weights, the formatter), `lint/` (the studio's lints and fixes), `lsl/` (the LSL analyzer over Tailslide, the SLua converter, and `ALLSLDepth`, the bound on how deep a parsed tree may go for any of them to walk it), `lsl/optimizer/` (the LSL optimizer, a pass a file, sharing `allsloptimizerpass.h`), `luau/` (the SLua analyzer), `masters/` (files on disk as the masters of scripts in the world: the links and their index, a master found from a script's `@file` hint or its name under the blessed folders, and what a send does) and `preprocessor/` (the preprocessor and its includes) - **lscript** - LL's LSL compiler and LSO VM, restored as a test-only reference for the optimizer and the weigher; built with tests, never linked into the viewer - **rust** - The Rust the viewer builds: one Cargo workspace and lock file, imported by Corrosion, a vcpkg port. It holds velopack_libc, Velopack's C API over the `velopack` crate from crates.io, its sources and headers vendored from the release with one buffer fix marked Alchemy (`cmake/Velopack.cmake`): a DLL on Windows, a static library on macOS and Linux, the release build for every configuration -- **newview** - The main viewer application. Contains all viewer-specific logic: the rendering pipeline, scene graph, UI floaters/panels, settings, object/avatar systems, and the application entry point. Links against all libraries above plus third-party: Tracy, OpenXR, Discord SDK, NVAPI, TinyEXR, Velopack +- **newview** - The main viewer application. Contains all viewer-specific logic: the rendering pipeline, scene graph, UI floaters/panels, settings, object/avatar systems, and the application entry point. Links against all libraries above plus third-party: Tracy, OpenXR, Discord SDK, NVAPI, TinyEXR, Velopack. It reaches GL only through llrender (gGL, LLRenderTarget, ALGPUReadback, ...): a pre-commit hook (`scripts/code_tools/check_raw_gl.py`) holds each of its files to the raw gl* calls `scripts/code_tools/raw_gl_baseline.txt` allows it ### Application Lifecycle diff --git a/scripts/code_tools/check_raw_gl.py b/scripts/code_tools/check_raw_gl.py new file mode 100644 index 0000000000..bf9d56a5fc --- /dev/null +++ b/scripts/code_tools/check_raw_gl.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""\ +Counts the raw OpenGL calls (gl* functions) each source file under indra/newview makes and checks +them against raw_gl_baseline.txt: a file may make no more than the baseline gives it, and none at all +where it gives nothing. llrender owns GL; newview reaches it through gGL, LLRenderTarget, +ALGPUReadback and the rest. + +Comments, string literals and #if 0 blocks are not counted. A file that makes fewer calls than the +baseline gives it fails too, so the baseline is lowered as calls move out: run with --update. + + check_raw_gl.py check every file under indra/newview + check_raw_gl.py FILE... check these files, those outside indra/newview ignored + check_raw_gl.py --update write the baseline from the tree as it is + +$LicenseInfo:firstyear=2026&license=viewerlgpl$ +Alchemy Viewer Source Code +Copyright (C) 2026, Rye + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; +version 2.1 of the License only. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +$/LicenseInfo$ +""" + +import argparse +import re +import sys +from pathlib import Path, PurePosixPath + +REPO = Path(__file__).resolve().parents[2] +BASELINE = Path(__file__).resolve().with_name("raw_gl_baseline.txt") +SCANNED = PurePosixPath("indra/newview") +SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".inl", ".m", ".mm"} +CALL = re.compile(r"\bgl[A-Z]\w*(?=\s*\()") +DIRECTIVE = re.compile(r"^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b(.*)$") + + +def strip_comments_and_strings(text): + """The text with comments blanked and string and character literals emptied, lines kept.""" + out = [] + i, n = 0, len(text) + while i < n: + if text.startswith("//", i): + end = text.find("\n", i) + i = n if end < 0 else end + elif text.startswith("/*", i): + end = text.find("*/", i + 2) + end = n if end < 0 else end + 2 + out.append("\n" * text.count("\n", i, end)) + i = end + elif text.startswith('R"', i) and (i == 0 or not (text[i - 1].isalnum() or text[i - 1] == "_")): + open_paren = text.find("(", i + 2) + if open_paren < 0: + out.append(text[i]) + i += 1 + continue + closing = ")" + text[i + 2:open_paren] + '"' + end = text.find(closing, open_paren) + end = n if end < 0 else end + len(closing) + out.append('""' + "\n" * text.count("\n", i, end)) + i = end + elif text[i] in "\"'": + quote = text[i] + j = i + 1 + while j < n and text[j] != quote and text[j] != "\n": + j += 2 if text[j] == "\\" else 1 + out.append(quote + quote) + # An unclosed one, a digit separator's quote among them, ends at its line. + i = j if j < n and text[j] == "\n" else j + 1 + else: + out.append(text[i]) + i += 1 + return "".join(out) + + +def strip_if_zero(text): + """The text with the lines #if 0 leaves out blanked, up to its #else, #elif or #endif.""" + lines = text.split("\n") + stack = [] # per open conditional: whether it was entered dead, and whether its live part is the #else + dead = 0 + for index, line in enumerate(lines): + match = DIRECTIVE.match(line) + if match: + kind, rest = match.group(1), match.group(2).strip() + if kind in ("if", "ifdef", "ifndef"): + zero = kind == "if" and re.match(r"^0\b", rest) is not None + stack.append(zero) + if zero: + dead += 1 + elif kind in ("elif", "else") and stack and stack[-1]: + stack[-1] = False + dead -= 1 + elif kind == "endif" and stack: + if stack.pop(): + dead -= 1 + lines[index] = "" + elif dead: + lines[index] = "" + return "\n".join(lines) + + +def raw_calls(text): + """(line, name) for each raw gl* call the source makes.""" + code = strip_if_zero(strip_comments_and_strings(text)) + calls = [] + for number, line in enumerate(code.split("\n"), 1): + calls.extend((number, match.group(0)) for match in CALL.finditer(line)) + return calls + + +def read_baseline(path): + baseline = {} + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.split("#", 1)[0].strip() + if line: + name, count = line.rsplit(None, 1) + baseline[name] = int(count) + return baseline + + +def write_baseline(path, counts): + lines = ["# Raw gl* calls each file under indra/newview may make; checked by check_raw_gl.py"] + lines += [f"{name} {count}" for name, count in sorted(counts.items()) if count] + path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") + + +def scanned_files(root): + return sorted(p for p in (root / SCANNED).rglob("*") if p.suffix in SUFFIXES and "tests" not in + p.relative_to(root / SCANNED).parts) + + +def key(root, path): + return path.resolve().relative_to(root).as_posix() + + +def check(root, baseline, files): + """The messages for every file whose count is not its baseline's.""" + problems = [] + for path in files: + name = key(root, path) + calls = raw_calls(path.read_text(encoding="utf-8", errors="replace")) + allowed = baseline.get(name, 0) + if len(calls) > allowed: + sites = "\n".join(f" {name}:{line}: {call}" for line, call in calls) + problems.append(f"{name}: {len(calls)} raw GL calls where {allowed} are allowed. Reach GL through " + f"llrender (gGL, LLRenderTarget, ALGPUReadback, ...) instead:\n{sites}") + elif len(calls) < allowed: + problems.append(f"{name}: {len(calls)} raw GL calls where the baseline allows {allowed}: run " + f"scripts/code_tools/check_raw_gl.py --update") + return problems + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("files", nargs="*", type=Path, help="files to check, rather than all of indra/newview") + parser.add_argument("--update", action="store_true", help="write the baseline from the tree as it is") + parser.add_argument("--root", type=Path, default=REPO, help="the repository's root") + parser.add_argument("--baseline", type=Path, default=BASELINE, help="the baseline file") + args = parser.parse_args(argv) + root = args.root.resolve() + + if args.update: + counts = {key(root, p): len(raw_calls(p.read_text(encoding="utf-8", errors="replace"))) + for p in scanned_files(root)} + write_baseline(args.baseline, counts) + print(f"{args.baseline}: {sum(counts.values())} raw GL calls in {sum(1 for c in counts.values() if c)} files") + return 0 + + baseline = read_baseline(args.baseline) + if args.files: + newview = (root / SCANNED).resolve() + files = [f.resolve() for f in args.files if f.suffix in SUFFIXES and f.exists() and + newview in f.resolve().parents and "tests" not in f.resolve().relative_to(newview).parts] + else: + files = scanned_files(root) + present = {key(root, p) for p in files} + missing = [name for name in baseline if name not in present] + if missing: + print("The baseline names files that are gone: run scripts/code_tools/check_raw_gl.py --update\n " + + "\n ".join(missing)) + return 1 + + problems = check(root, baseline, files) + for problem in problems: + print(problem) + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/code_tools/raw_gl_baseline.txt b/scripts/code_tools/raw_gl_baseline.txt new file mode 100644 index 0000000000..e4497ddec3 --- /dev/null +++ b/scripts/code_tools/raw_gl_baseline.txt @@ -0,0 +1,3 @@ +# Raw gl* calls each file under indra/newview may make; checked by check_raw_gl.py +indra/newview/llappviewer.cpp 1 +indra/newview/llcefaccelinterop.cpp 1 diff --git a/scripts/code_tools/test_check_raw_gl.py b/scripts/code_tools/test_check_raw_gl.py new file mode 100644 index 0000000000..9521cee5bb --- /dev/null +++ b/scripts/code_tools/test_check_raw_gl.py @@ -0,0 +1,119 @@ +from pathlib import Path +import contextlib +import io +import tempfile +import unittest + +import check_raw_gl as check + + +class RawCallTests(unittest.TestCase): + def names(self, text): + return [name for _, name in check.raw_calls(text)] + + def test_calls_are_found_with_their_lines(self): + text = "void f()\n{\n glClear(GL_COLOR_BUFFER_BIT);\n glViewport (0, 0, 1, 1);\n}\n" + self.assertEqual(check.raw_calls(text), [(3, "glClear"), (4, "glViewport")]) + + def test_what_is_not_a_raw_call_is_not_counted(self): + text = ("gGL.clear(GL_COLOR_BUFFER_BIT);\n" + "if (!glEGLImageTargetTexture2DOES) {}\n" + "PFNGLCLEARPROC glClear = nullptr;\n" + "LLGLEnable blend(GL_BLEND);\n" + "global(1);\n") + self.assertEqual(self.names(text), []) + + def test_comments_and_strings_are_not_counted(self): + text = ('// glClear(0);\n' + '/* glViewport(0, 0, 1, 1);\n glFlush(); */\n' + 'LL_INFOS() << "glReadPixels(" << \'(\' << LL_ENDL;\n' + 'auto s = R"(glFinish())";\n' + 'glFlush();\n') + self.assertEqual(check.raw_calls(text), [(6, "glFlush")]) + + def test_if_zero_blocks_are_not_counted_and_their_else_is(self): + text = ("#if 0 // DEPRECATED\n" + "glClear(0);\n" + "#if LL_WINDOWS\n" + "glFlush();\n" + "#else\n" + "glFinish();\n" + "#endif\n" + "#else\n" + "glViewport(0, 0, 1, 1);\n" + "#endif\n" + "#if LL_DARWIN\n" + "glPointSize(1.f);\n" + "#endif\n") + self.assertEqual(check.raw_calls(text), [(9, "glViewport"), (12, "glPointSize")]) + + def test_an_unclosed_quote_keeps_the_lines_after_it(self): + text = "int n = 1'000;\nglFlush();\n" + self.assertEqual(check.raw_calls(text), [(2, "glFlush")]) + + +class BaselineTests(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.root = Path(self.dir.name) + self.newview = self.root / "indra" / "newview" + (self.newview / "tests").mkdir(parents=True) + self.baseline = self.root / "baseline.txt" + self.write("llkept.cpp", "void f() { glFlush(); glFinish(); }\n") + self.write("llclean.cpp", "void f() { gGL.flush(); }\n") + self.write("tests/llsomething_test.cpp", "void f() { glClear(0); }\n") + + def tearDown(self): + self.dir.cleanup() + + def write(self, name, text): + (self.newview / name).write_text(text, encoding="utf-8") + + def run_check(self, *args): + out = io.StringIO() + with contextlib.redirect_stdout(out): + status = check.main(["--root", str(self.root), "--baseline", str(self.baseline), *map(str, args)]) + return status, out.getvalue() + + def test_update_records_every_file_with_calls_and_no_tests(self): + self.assertEqual(self.run_check("--update")[0], 0) + self.assertEqual(check.read_baseline(self.baseline), {"indra/newview/llkept.cpp": 2}) + self.assertEqual(self.run_check()[0], 0) + + def test_a_new_call_fails_and_names_its_site(self): + self.run_check("--update") + self.write("llclean.cpp", "void f() { gGL.flush(); }\nvoid g() { glClear(0); }\n") + status, out = self.run_check() + self.assertEqual(status, 1) + self.assertIn("indra/newview/llclean.cpp:2: glClear", out) + + def test_a_call_more_than_the_baseline_fails(self): + self.run_check("--update") + self.write("llkept.cpp", "void f() { glFlush(); glFinish(); glFlush(); }\n") + self.assertEqual(self.run_check()[0], 1) + + def test_fewer_calls_than_the_baseline_ask_for_an_update(self): + self.run_check("--update") + self.write("llkept.cpp", "void f() { glFlush(); }\n") + status, out = self.run_check() + self.assertEqual(status, 1) + self.assertIn("--update", out) + + def test_a_file_the_baseline_names_that_is_gone_asks_for_an_update(self): + self.run_check("--update") + (self.newview / "llkept.cpp").unlink() + self.assertEqual(self.run_check()[0], 1) + + def test_files_given_are_checked_alone_and_others_ignored(self): + self.run_check("--update") + self.write("llkept.cpp", "void f() { glFlush(); glFinish(); glFlush(); }\n") + outside = self.root / "indra" / "llrender" + outside.mkdir(parents=True) + (outside / "llrender.cpp").write_text("void f() { glClear(0); }\n", encoding="utf-8") + self.assertEqual(self.run_check(self.newview / "llclean.cpp", outside / "llrender.cpp", + self.newview / "tests" / "llsomething_test.cpp")[0], 0) + self.assertEqual(self.run_check(self.newview / "llkept.cpp")[0], 1) + + +if __name__ == "__main__": + unittest.main() From c3e0048040cb44a625e6f3527a9303862285e44b Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 06:47:09 -0400 Subject: [PATCH 4/8] ALTextureBlit's copy is not clipped by the scissor glBlitFramebuffer writes only where the scissor test passes, so a blit made with the test on copied part of the rectangle. The copy now turns the test off around the blit and back on after it, as LLRenderTarget::copyFromBound does. altextureblit_gl covers it: a copy under a one-texel scissor fills every row, and the test is on again after; with the fix taken out the case fails. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/altextureblit.cpp | 10 +++++++--- indra/llrender/altextureblit.h | 4 ++-- .../llrender/tests/altextureblit_gl_test.cpp | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/indra/llrender/altextureblit.cpp b/indra/llrender/altextureblit.cpp index fb361640c5..1e3b45b8fd 100644 --- a/indra/llrender/altextureblit.cpp +++ b/indra/llrender/altextureblit.cpp @@ -54,9 +54,13 @@ void ALTextureBlit::blit(U32 src_target, U32 src, U32 dst, S32 width, S32 height glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mDrawFBO); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0); - glBlitFramebuffer(0, 0, width, height, - 0, flip_y ? height : 0, width, flip_y ? 0 : height, - GL_COLOR_BUFFER_BIT, GL_NEAREST); + { + // glBlitFramebuffer writes only where the scissor test passes. + LLGLDisable scissor(GL_SCISSOR_TEST); + glBlitFramebuffer(0, 0, width, height, + 0, flip_y ? height : 0, width, flip_y ? 0 : height, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } // Detached, so neither framebuffer keeps a texture its owner may delete or hand to another context. glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, src_target, 0, 0); diff --git a/indra/llrender/altextureblit.h b/indra/llrender/altextureblit.h index 08da12ee6e..92de6529ed 100644 --- a/indra/llrender/altextureblit.h +++ b/indra/llrender/altextureblit.h @@ -43,8 +43,8 @@ class ALTextureBlit // Copy the `width` x `height` rectangle at the origin of `src`, a texture of `src_target` (GL_TEXTURE_2D or // GL_TEXTURE_RECTANGLE), into the same rectangle of `dst`, a GL_TEXTURE_2D, texel for texel, its rows in the - // opposite order where `flip_y`. What gGL has batched is drawn first. Neither texture stays attached, and the - // framebuffer bound before (LLRenderTarget::sCurFBO) is bound again. + // opposite order where `flip_y`. What gGL has batched is drawn first, and the scissor does not clip the copy. + // Neither texture stays attached, and the framebuffer bound before (LLRenderTarget::sCurFBO) is bound again. void blit(U32 src_target, U32 src, U32 dst, S32 width, S32 height, bool flip_y); // Delete the framebuffers, if any. Call from a teardown that still has the context; the destructor calls it as a diff --git a/indra/llrender/tests/altextureblit_gl_test.cpp b/indra/llrender/tests/altextureblit_gl_test.cpp index 87b96d06c5..ae99237606 100644 --- a/indra/llrender/tests/altextureblit_gl_test.cpp +++ b/indra/llrender/tests/altextureblit_gl_test.cpp @@ -28,6 +28,7 @@ #include "../altextureblit.h" #include "../llgl.h" #include "../llglheaders.h" +#include "../llglstates.h" #include "../llrender.h" #include "../llrendertarget.h" @@ -161,6 +162,24 @@ namespace CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } + // A scissor in force does not clip the copy, and is in force again after it. + TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: the scissor does not clip the copy", "[altextureblit_gl]") + { + setUp(GL_TEXTURE_2D); + { + LLGLSScissor corner(0, 0, 1, 1); + mBlit.blit(GL_TEXTURE_2D, mSrc, mDst, W, H, false); + UNSCOPED_INFO("the scissor test is on again"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + } + for (S32 y = 0; y < H; ++y) + { + CAPTURE(y); + CHECK(dstRow(y) == rowColour(y)); + } + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + // The framebuffer bound before the copy is bound after it, for drawing and reading, and the copy's framebuffers // are kept for the next copy until released. TEST_CASE_METHOD(altextureblit_data, "altextureblit_gl: the framebuffer bound before is bound after, and release drops the copy's", "[altextureblit_gl]") From 9481b74b6ee3f77ca95ab3d1f1fc5d69ca784362 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 06:47:11 -0400 Subject: [PATCH 5/8] The raw GL check skips #elif 0 and counts a call split before its parenthesis An #elif after #if 0 reopened the conditional whatever its condition, so the calls under an #elif 0 were counted; and calls were matched a line at a time, so a gl* name with its parenthesis on the next line was missed. Each open conditional now records whether the branch in force is a 0, #elif included, and calls are matched across the whole stripped source, each counted on the line its name is on. Two new cases in test_check_raw_gl.py, both failing on the checker before this. Co-Authored-By: Claude Opus 5.5 --- scripts/code_tools/check_raw_gl.py | 28 ++++++++++--------------- scripts/code_tools/test_check_raw_gl.py | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/scripts/code_tools/check_raw_gl.py b/scripts/code_tools/check_raw_gl.py index bf9d56a5fc..6366290604 100644 --- a/scripts/code_tools/check_raw_gl.py +++ b/scripts/code_tools/check_raw_gl.py @@ -84,38 +84,32 @@ def strip_comments_and_strings(text): def strip_if_zero(text): - """The text with the lines #if 0 leaves out blanked, up to its #else, #elif or #endif.""" + """The text with the branches of #if 0 and #elif 0 blanked. Any other condition is taken as compiled.""" lines = text.split("\n") - stack = [] # per open conditional: whether it was entered dead, and whether its live part is the #else - dead = 0 + stack = [] # per open conditional: whether the branch in force is a 0 for index, line in enumerate(lines): match = DIRECTIVE.match(line) if match: kind, rest = match.group(1), match.group(2).strip() + zero = re.match(r"^0\b", rest) is not None if kind in ("if", "ifdef", "ifndef"): - zero = kind == "if" and re.match(r"^0\b", rest) is not None - stack.append(zero) - if zero: - dead += 1 - elif kind in ("elif", "else") and stack and stack[-1]: + stack.append(kind == "if" and zero) + elif kind == "elif" and stack: + stack[-1] = zero + elif kind == "else" and stack: stack[-1] = False - dead -= 1 elif kind == "endif" and stack: - if stack.pop(): - dead -= 1 + stack.pop() lines[index] = "" - elif dead: + elif any(stack): lines[index] = "" return "\n".join(lines) def raw_calls(text): - """(line, name) for each raw gl* call the source makes.""" + """(line, name) for each raw gl* call the source makes, its parenthesis on the same line or a later one.""" code = strip_if_zero(strip_comments_and_strings(text)) - calls = [] - for number, line in enumerate(code.split("\n"), 1): - calls.extend((number, match.group(0)) for match in CALL.finditer(line)) - return calls + return [(code.count("\n", 0, match.start()) + 1, match.group(0)) for match in CALL.finditer(code)] def read_baseline(path): diff --git a/scripts/code_tools/test_check_raw_gl.py b/scripts/code_tools/test_check_raw_gl.py index 9521cee5bb..2277347431 100644 --- a/scripts/code_tools/test_check_raw_gl.py +++ b/scripts/code_tools/test_check_raw_gl.py @@ -47,6 +47,27 @@ def test_if_zero_blocks_are_not_counted_and_their_else_is(self): "#endif\n") self.assertEqual(check.raw_calls(text), [(9, "glViewport"), (12, "glPointSize")]) + def test_elif_zero_branches_are_not_counted(self): + text = ("#if 0\n" + "glClear(0);\n" + "#elif 0\n" + "glFlush();\n" + "#elif LL_WINDOWS\n" + "glFinish();\n" + "#endif\n" + "#if LL_DARWIN\n" + "glViewport(0, 0, 1, 1);\n" + "#elif 0\n" + "glPointSize(1.f);\n" + "#else\n" + "glCullFace(GL_BACK);\n" + "#endif\n") + self.assertEqual(check.raw_calls(text), [(6, "glFinish"), (9, "glViewport"), (13, "glCullFace")]) + + def test_a_call_whose_parenthesis_is_on_a_later_line_is_counted_on_its_name_line(self): + text = "void f()\n{\n glClear\n (GL_COLOR_BUFFER_BIT);\n glFlush();\n}\n" + self.assertEqual(check.raw_calls(text), [(3, "glClear"), (5, "glFlush")]) + def test_an_unclosed_quote_keeps_the_lines_after_it(self): text = "int n = 1'000;\nglFlush();\n" self.assertEqual(check.raw_calls(text), [(2, "glFlush")]) From 389fe147f4e634f735f2deef4ee79d57094f1038 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 08:04:51 -0400 Subject: [PATCH 6/8] The stencil state checks read GL with a stencil buffer bound GL answers a query of the stencil reference held to the stencil bits of the draw framebuffer. The hidden window's has none under Mesa, so the Linux CI runners read the reference 3 back as 0 and failed two alrenderstate_gl cases, where NVIDIA does not hold the query and passed. Those cases now bind a target with depth and stencil while they read the stencil state back. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/tests/alrenderstate_gl_test.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index fee7964056..0a42f24c32 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -121,7 +121,8 @@ namespace } // GL's stencil state, both faces, against what is given. The masks are held to the 8 bits a stencil buffer has: - // past them, what glGetIntegerv answers for an unsigned mask is the driver's choice. + // past them, what glGetIntegerv answers for an unsigned mask is the driver's choice. GL answers the reference + // held to the stencil bits of the draw framebuffer, so a check of one above 0 binds a target with stencil. void checkStencil(GLenum func, GLint ref, GLuint read_mask, GLenum fail, GLenum depth_fail, GLenum pass, GLuint write_mask) { @@ -170,9 +171,10 @@ namespace mTarget.release(); } - void setUp() + // The target, with depth and stencil where `stencil`. + void setUp(bool stencil = false) { - REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8, stencil, stencil)); } ll_test::SharedGLScope mGL; @@ -334,6 +336,8 @@ namespace CHECK((U32)glInteger(GL_BLEND_EQUATION_ALPHA) == (U32)GL_MAX); }; + setUp(true); + mTarget.bindTarget(); set(); { INFO("what gGL is given, GL holds"); @@ -371,6 +375,7 @@ namespace INFO("refreshState puts back what gGL holds"); check_set(); } + mTarget.flush(); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -432,6 +437,8 @@ namespace // defaults. TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: ALGLStencil sets its state through gGL and leaves GL's defaults", "[alrenderstate_gl]") { + setUp(true); + mTarget.bindTarget(); { ALGLStencil stencil(GL_EQUAL, 3, 0x0F, GL_KEEP, GL_INCR, GL_REPLACE, 0xF0); UNSCOPED_INFO("the test is on in the scope"); @@ -443,6 +450,7 @@ namespace CHECK(glIsEnabled(GL_STENCIL_TEST) == GL_FALSE); checkCachedStencil(GL_ALWAYS, 0, ~0u, GL_KEEP, GL_KEEP, GL_KEEP, ~0u); checkStencil(GL_ALWAYS, 0, ~0u, GL_KEEP, GL_KEEP, GL_KEEP, ~0u); + mTarget.flush(); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } From e6be9b5b7a289e1bcd1785965d5089a019745ba1 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 08:13:06 -0400 Subject: [PATCH 7/8] The raw GL check takes only a bare 0 as a branch that is not compiled A condition was read as 0 if it began with one, so the calls under #if 0 || LL_WINDOWS, which can be compiled, were not counted. Only a condition that is 0 alone, comments stripped, blanks its branch now. A new case in test_check_raw_gl.py fails on the checker before this. Co-Authored-By: Claude Opus 5.5 --- scripts/code_tools/check_raw_gl.py | 2 +- scripts/code_tools/test_check_raw_gl.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/code_tools/check_raw_gl.py b/scripts/code_tools/check_raw_gl.py index 6366290604..23dd1b96b6 100644 --- a/scripts/code_tools/check_raw_gl.py +++ b/scripts/code_tools/check_raw_gl.py @@ -91,7 +91,7 @@ def strip_if_zero(text): match = DIRECTIVE.match(line) if match: kind, rest = match.group(1), match.group(2).strip() - zero = re.match(r"^0\b", rest) is not None + zero = rest == "0" if kind in ("if", "ifdef", "ifndef"): stack.append(kind == "if" and zero) elif kind == "elif" and stack: diff --git a/scripts/code_tools/test_check_raw_gl.py b/scripts/code_tools/test_check_raw_gl.py index 2277347431..4e69d6e72a 100644 --- a/scripts/code_tools/test_check_raw_gl.py +++ b/scripts/code_tools/test_check_raw_gl.py @@ -64,6 +64,17 @@ def test_elif_zero_branches_are_not_counted(self): "#endif\n") self.assertEqual(check.raw_calls(text), [(6, "glFinish"), (9, "glViewport"), (13, "glCullFace")]) + def test_a_condition_that_only_starts_with_zero_is_counted(self): + text = ("#if 0 || LL_WINDOWS\n" + "glClear(0);\n" + "#elif 0 && LL_DARWIN\n" + "glFlush();\n" + "#endif\n" + "#if 0 // DEPRECATED\n" + "glFinish();\n" + "#endif\n") + self.assertEqual(check.raw_calls(text), [(2, "glClear"), (4, "glFlush")]) + def test_a_call_whose_parenthesis_is_on_a_later_line_is_counted_on_its_name_line(self): text = "void f()\n{\n glClear\n (GL_COLOR_BUFFER_BIT);\n glFlush();\n}\n" self.assertEqual(check.raw_calls(text), [(3, "glClear"), (5, "glFlush")]) From 6b578b177260a89b0bffefddcc094eb5fc54b6a0 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 09:12:27 -0400 Subject: [PATCH 8/8] The stencil state checks clear their target before reading the reference Binding a target with stencil was not enough under Mesa: its query of the stencil reference holds it to the stencil bits of the draw framebuffer as of the last draw or clear into it, and a bind alone leaves the last framebuffer's, the hidden window's, which has none. The arm64 Linux runner still read 3 back as 0. Both cases now clear the target after binding it, before any read. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/tests/alrenderstate_gl_test.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index 0a42f24c32..ee46375e89 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -122,7 +122,8 @@ namespace // GL's stencil state, both faces, against what is given. The masks are held to the 8 bits a stencil buffer has: // past them, what glGetIntegerv answers for an unsigned mask is the driver's choice. GL answers the reference - // held to the stencil bits of the draw framebuffer, so a check of one above 0 binds a target with stencil. + // held to the stencil bits of the draw framebuffer, which Mesa takes as of the last draw or clear into it, not + // its bind: a check of one above 0 binds a target with stencil and clears it first. void checkStencil(GLenum func, GLint ref, GLuint read_mask, GLenum fail, GLenum depth_fail, GLenum pass, GLuint write_mask) { @@ -338,6 +339,7 @@ namespace setUp(true); mTarget.bindTarget(); + mTarget.clear(); set(); { INFO("what gGL is given, GL holds"); @@ -439,6 +441,7 @@ namespace { setUp(true); mTarget.bindTarget(); + mTarget.clear(); { ALGLStencil stencil(GL_EQUAL, 3, 0x0F, GL_KEEP, GL_INCR, GL_REPLACE, 0xF0); UNSCOPED_INFO("the test is on in the scope");