diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11cc5ac05f0..5c12a13fb26 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 8fde2c2e877..016f1df11c5 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/indra/alrender/altransienttargetpool.cpp b/indra/alrender/altransienttargetpool.cpp index ee0d1be6e8f..2c5f9391f3b 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 330fb9b4906..9e88b2257b3 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 85d0b7530eb..a034a07bb60 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 00000000000..1e3b45b8fdd --- /dev/null +++ b/indra/llrender/altextureblit.cpp @@ -0,0 +1,87 @@ +/** + * @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 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); + 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 00000000000..92de6529ed3 --- /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, 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 + // 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 1e5d8a59f30..e8c5e569c70 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 c5926675ecc..402c0512fec 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 48d12d03ac7..2217ea20003 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 5dc3bfe69a8..1171ccc9bf3 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 5b15465c88a..d61258f4f43 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 a2090e2d2f2..4ced5cd6b99 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 db31db4b688..7446b7c2818 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 294abaffbda..6bc87034011 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 9eefa7bdf54..ee46375e891 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,92 @@ 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. GL answers the reference + // 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) + { + 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 { @@ -80,9 +172,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; @@ -219,4 +312,250 @@ 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); + }; + + setUp(true); + mTarget.bindTarget(); + mTarget.clear(); + 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(); + } + mTarget.flush(); + 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]") + { + 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"); + 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); + mTarget.flush(); + 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 00000000000..ae992376060 --- /dev/null +++ b/indra/llrender/tests/altextureblit_gl_test.cpp @@ -0,0 +1,208 @@ +/** + * @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 "../llglstates.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); + } + + // 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]") + { + 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 57b11afd634..baf9307072f 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; diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 88d466a5cdf..565fb4490a5 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 4554f00a7ab..e5046fb5cc6 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 e433f388194..1a1f76d45eb 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 46b6a9dc04f..d25f9e27178 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 26f17f340f1..9b039b30bc7 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 f7e7f581939..c444c98be2c 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 77fb7f96037..b61e1f6ee45 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 c33ea1d4c95..04e4511f1f3 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 26d6c864dc6..e13fc7e09d7 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 129de9b0176..0af3c8c4157 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 3fa4f0b4b93..ee23e84c86c 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 b266963e188..2f0a3ae3b2e 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 90d5fd3da12..bc8b26599d6 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 62300f2bba6..5075cea295e 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 683154fa1f4..524e7313d9c 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 5d5e536d9c8..d215299dd39 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 b2a93b9a65a..43da1727f72 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 14030de91af..028347e81dc 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 cfe79260d33..5bb2db6cfd5 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); diff --git a/scripts/code_tools/check_raw_gl.py b/scripts/code_tools/check_raw_gl.py new file mode 100644 index 00000000000..23dd1b96b62 --- /dev/null +++ b/scripts/code_tools/check_raw_gl.py @@ -0,0 +1,194 @@ +#!/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 branches of #if 0 and #elif 0 blanked. Any other condition is taken as compiled.""" + lines = text.split("\n") + 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 = rest == "0" + if kind in ("if", "ifdef", "ifndef"): + stack.append(kind == "if" and zero) + elif kind == "elif" and stack: + stack[-1] = zero + elif kind == "else" and stack: + stack[-1] = False + elif kind == "endif" and stack: + stack.pop() + lines[index] = "" + elif any(stack): + lines[index] = "" + return "\n".join(lines) + + +def raw_calls(text): + """(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)) + return [(code.count("\n", 0, match.start()) + 1, match.group(0)) for match in CALL.finditer(code)] + + +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 00000000000..e4497ddec38 --- /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 00000000000..4e69d6e72af --- /dev/null +++ b/scripts/code_tools/test_check_raw_gl.py @@ -0,0 +1,151 @@ +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_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_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")]) + + 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()