From f2b8b91c5e7da85ef85b45e85f53a416f05b454e Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 9 Jun 2026 01:42:32 -0400 Subject: [PATCH 1/9] Add LLSwapChain. More or less mirrors swap chains from other APIs - and acts as an abstraction for future work. Also gets us away from directly writing to FBO 0, which was complicating some other compositor work. --- indra/llrender/CMakeLists.txt | 2 + indra/llrender/llrendertarget.cpp | 76 ++++++++++--- indra/llrender/llrendertarget.h | 45 ++++++++ indra/llrender/llswapchain.cpp | 156 ++++++++++++++++++++++++++ indra/llrender/llswapchain.h | 103 +++++++++++++++++ indra/newview/llappviewer.cpp | 38 +++++++ indra/newview/llappviewer.h | 8 ++ indra/newview/llscenemonitor.cpp | 13 ++- indra/newview/llviewerassetupload.cpp | 12 +- indra/newview/llviewerdisplay.cpp | 54 ++++++++- indra/newview/llviewertexturelist.cpp | 19 ++++ indra/newview/llviewerwindow.cpp | 4 + 12 files changed, 502 insertions(+), 28 deletions(-) create mode 100644 indra/llrender/llswapchain.cpp create mode 100644 indra/llrender/llswapchain.h diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index fcd287bbb39..5e67f4c5e40 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -28,6 +28,7 @@ set(llrender_SOURCE_FILES llrendernavprim.cpp llrendersphere.cpp llrendertarget.cpp + llswapchain.cpp llshadermgr.cpp lltexture.cpp lltexturemanagerbridge.cpp @@ -59,6 +60,7 @@ set(llrender_HEADER_FILES llrender2dutils.h llrendernavprim.h llrendersphere.h + llswapchain.h llshadermgr.h lltexture.h lltexturemanagerbridge.h diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 0b0d69812f0..ec460d1405f 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -52,6 +52,7 @@ void check_framebuffer_status() bool LLRenderTarget::sUseFBO = false; U32 LLRenderTarget::sCurFBO = 0; +bool LLRenderTarget::sFlushRequiresParent = false; extern S32 gGLViewport[4]; @@ -352,6 +353,8 @@ void LLRenderTarget::release() LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(!isBoundInStack()); + mIsSwapChainImage = false; + if (mDepth) { LLImageGL::deleteTextures(1, &mDepth); @@ -415,8 +418,8 @@ void LLRenderTarget::release() void LLRenderTarget::bindTarget() { LL_PROFILE_GPU_ZONE("bindTarget"); - llassert(mFBO); llassert(!isBoundInStack()); + llassert(mFBO); glBindFramebuffer(GL_FRAMEBUFFER, mFBO); sCurFBO = mFBO; @@ -439,9 +442,24 @@ void LLRenderTarget::bindTarget() } check_framebuffer_status(); - glViewport(0, 0, mResX, mResY); - sCurResX = mResX; - sCurResY = mResY; + if (mIsSwapChainImage) + { + // Bottom-of-stack swap chain image: restore the world-view viewport + // (gGLViewport) so HUD/UI and renderFinalize's fullscreen triangle + // render into the same sub-rect they did when we drew straight to + // FBO 0. The off-screen image is window-sized; the viewport is the + // world-view rect inside it. + // - Geenz 2026-06-08 + glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + sCurResX = gGLViewport[2]; + sCurResY = gGLViewport[3]; + } + else + { + glViewport(0, 0, mResX, mResY); + sCurResX = mResX; + sCurResY = mResY; + } mPreviousRT = sBoundTarget; sBoundTarget = this; @@ -499,9 +517,9 @@ void LLRenderTarget::flush() { LL_PROFILE_GPU_ZONE("rt flush"); gGL.flush(); + llassert(sBoundTarget == this); llassert(mFBO); llassert(sCurFBO == mFBO); - llassert(sBoundTarget == this); if (mGenerateMipMaps == LLTexUnit::TMG_AUTO) { @@ -516,18 +534,48 @@ void LLRenderTarget::flush() // the previous frame back on to play nice with the GL state machine sBoundTarget = mPreviousRT->mPreviousRT; mPreviousRT->bindTarget(); + return; } - else + + if (mIsSwapChainImage) { + // Bottom of the stack for a render batch. Just pop the bookkeeping + // and leave the FBO bound — LLSwapChain::present() will rebind FBO 0 + // for the blit, and the next acquireNextImage() will set up the next + // image's bind. sBoundTarget = nullptr; - glBindFramebuffer(GL_FRAMEBUFFER, 0); - sCurFBO = 0; - glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); - sCurResX = gGLViewport[2]; - sCurResY = gGLViewport[3]; - glReadBuffer(GL_BACK); - glDrawBuffer(GL_BACK); - } + return; + } + + // No parent on the stack and not a swap chain image. While the swap chain + // is attached (steady-state rendering) this is a missed bind site — + // assert so the call site can be wrapped. Before the chain attaches or + // after release (feature-manager GPU benchmark, late shutdown cleanup) + // fall back to the OS default framebuffer the way the old code did. + // - Geenz 2026-06-08 + llassert(!sFlushRequiresParent); + + sBoundTarget = nullptr; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + sCurFBO = 0; + glViewport(gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + sCurResX = gGLViewport[2]; + sCurResY = gGLViewport[3]; + glReadBuffer(GL_BACK); + glDrawBuffer(GL_BACK); +} + +void LLRenderTarget::bindForRead() +{ + llassert(mFBO); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mFBO); + glReadBuffer(GL_COLOR_ATTACHMENT0); +} + +void LLRenderTarget::unbindRead() +{ + glBindFramebuffer(GL_READ_FRAMEBUFFER, sCurFBO); + glReadBuffer(sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); } bool LLRenderTarget::isComplete() const diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index cd3290cf663..2813998609e 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -68,6 +68,15 @@ class LLRenderTarget static U32 sCurResX; static U32 sCurResY; + // When true, flush() requires a parent on the RT stack — i.e. the viewer + // has reached steady-state rendering with the swap chain attached. When + // false (early init before swap chain attach, or after shutdown release), + // flush() falls back to the OS default framebuffer so pre-attach code + // paths (feature-manager GPU benchmark, etc.) keep working. Toggled by + // LLSwapChain attach/release. + // - Geenz 2026-06-08 + static bool sFlushRequiresParent; + LLRenderTarget(); ~LLRenderTarget(); @@ -82,6 +91,25 @@ class LLRenderTarget // usage - deprecated, should always be TT_TEXTURE bool allocate(U32 resx, U32 resy, U32 color_fmt, bool depth = false, LLTexUnit::eTextureType usage = LLTexUnit::TT_TEXTURE, LLTexUnit::eTextureMipGeneration generateMipMaps = LLTexUnit::TMG_NONE); + // Mark this RT as an LLSwapChain image. The flag changes bindTarget's + // viewport restore behavior: when an intermediate RT flushes back to a + // swap-chain image, glViewport is restored to gGLViewport (the world + // view rect) instead of (0,0,mResX,mResY). HUD/UI rendering and + // renderFinalize's fullscreen triangle expect that. + void markAsSwapChainImage(bool yes = true) { mIsSwapChainImage = yes; } + bool isSwapChainImage() const { return mIsSwapChainImage; } + + // Bind this RT's FBO as GL_READ_FRAMEBUFFER (preserves the current draw + // FBO). Pair with unbindRead() to restore the read FB to the current + // draw target. Used by readback consumers (scene monitor) to copy from + // a swap chain image without ever naming FBO 0 themselves. + void bindForRead(); + + // Restore GL_READ_FRAMEBUFFER to the current draw FBO (sCurFBO). Pairs + // with bindForRead(). Doesn't read instance state; the symmetric API + // just keeps the call sites tidy. + void unbindRead(); + //resize existing attachments to use new resolution and color format // CAUTION: if the GL runs out of memory attempting to resize, this render target will be undefined // DO NOT use for screen space buffers or for scratch space for an image that might be uploaded @@ -149,6 +177,11 @@ class LLRenderTarget U32 getDepth(void) const { return mDepth; } + // Underlying GL framebuffer object name. Exposed for LLSwapChain's + // present-time blit; viewer code should bind through bindTarget / + // bindForRead instead of touching this directly. + U32 getFBO() const { return mFBO; } + void bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options = LLTexUnit::TFO_BILINEAR); //flush rendering operations @@ -188,6 +221,18 @@ class LLRenderTarget U32 mMipLevels; LLTexUnit::eTextureType mUsage; + + // True when this RT is one of LLSwapChain's images. Affects only + // viewport restore semantics in bindTarget (see markAsSwapChainImage). + // + // Future cleanup: replace this flag with a per-RT viewport member + // (mViewportX/Y/W/H) and have LLSwapChain push gGLViewport updates + // into its images. Generalizes sub-rect viewports for any RT and maps + // naturally to Vk/XR state. Held off today because gGLViewport changes + // more often than window resize (per-frame setup3DViewport, UI scale, + // sidebar) — syncing it everywhere is intrusive. Worth taking on as + // part of the broader render-state cleanup for Vulkan port prep. + bool mIsSwapChainImage = false; }; #endif diff --git a/indra/llrender/llswapchain.cpp b/indra/llrender/llswapchain.cpp new file mode 100644 index 00000000000..526d17ce6e0 --- /dev/null +++ b/indra/llrender/llswapchain.cpp @@ -0,0 +1,156 @@ +/** + * @file llswapchain.cpp + * @brief LLSwapChain implementation (OpenGL backend). + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llswapchain.h" + +#include "llgl.h" +#include "llwindow.h" + +LLSwapChain::~LLSwapChain() +{ + release(); +} + +void LLSwapChain::attachToWindow(LLWindow* window, U32 width, U32 height) +{ + llassert(window != nullptr); + llassert(mImages.empty()); // call release() before re-attaching + llassert(width > 0 && height > 0); + + mWindow = window; + mWidth = width; + mHeight = height; + + // Allocate kImageCount off-screen color+depth RTs at window resolution. + // Each carries the swap-chain-image flag so its bindTarget restores the + // world-view viewport when intermediate RTs pop back to it during rendering. + mImages.resize(kImageCount); + for (auto& img : mImages) + { + if (!img.allocate(width, height, GL_RGBA, /*depth=*/true)) + { + LL_WARNS("SwapChain") << "Failed to allocate swap chain image at " + << width << "x" << height << LL_ENDL; + } + img.markAsSwapChainImage(true); + } + + mCurrentIndex = 0; + + // From now on, top-level RT flushes must have a parent on the stack. + // Pre-attach paths (gpu_benchmark in feature-manager init, etc.) ran + // before this point and used the FBO-0 fallback inside flush(). + LLRenderTarget::sFlushRequiresParent = true; +} + +void LLSwapChain::resize(U32 width, U32 height) +{ + if (width == 0 || height == 0) + { + return; // minimized / iconified — keep existing storage + } + + mWidth = width; + mHeight = height; + + for (auto& img : mImages) + { + img.resize(width, height); + } +} + +LLRenderTarget& LLSwapChain::acquireNextImage() +{ + llassert(!mImages.empty()); + + // Rotate to the next image. GL has no real "acquire" — the driver owns + // the back buffer rotation under SwapBuffers — so this is just structural + // cycling. Vk/XR backends will do the real WSI acquire here. + mCurrentIndex = (mCurrentIndex + 1) % (U32)mImages.size(); + return mImages[mCurrentIndex]; +} + +LLRenderTarget& LLSwapChain::getCurrentImage() +{ + llassert(!mImages.empty()); + return mImages[mCurrentIndex]; +} + +LLRenderTarget& LLSwapChain::getPreviousImage() +{ + llassert(!mImages.empty()); + const U32 n = (U32)mImages.size(); + const U32 prev = (mCurrentIndex + n - 1) % n; + return mImages[prev]; +} + +void LLSwapChain::present() +{ + llassert(!mImages.empty()); + llassert(mWindow != nullptr); + + LLRenderTarget& img = mImages[mCurrentIndex]; + + // Blit the current image's color into FBO 0, then SwapBuffers. + // This is the only place in the codebase that names FBO 0 by literal. + { + LL_PROFILE_GPU_ZONE("swapchain present blit"); + + // Save current read FB so we don't disturb anyone else's state. + // (sCurFBO tracks the current draw FB; flush() asserts it matches.) + const U32 prev_fbo = LLRenderTarget::sCurFBO; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, img.getFBO()); + glReadBuffer(GL_COLOR_ATTACHMENT0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDrawBuffer(GL_BACK); + + glBlitFramebuffer(0, 0, (GLint)mWidth, (GLint)mHeight, + 0, 0, (GLint)mWidth, (GLint)mHeight, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // Restore unified read+draw binding to whatever was current before. + glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); + } + + mWindow->swapBuffers(); +} + +void LLSwapChain::release() +{ + // Drop the strict-flush requirement so any late-shutdown top-level + // flushes don't trip the assert. + LLRenderTarget::sFlushRequiresParent = false; + + // LLRenderTarget destructors release GL resources. + mImages.clear(); + mWindow = nullptr; + mWidth = 0; + mHeight = 0; + mCurrentIndex = 0; +} diff --git a/indra/llrender/llswapchain.h b/indra/llrender/llswapchain.h new file mode 100644 index 00000000000..a4d66ce6512 --- /dev/null +++ b/indra/llrender/llswapchain.h @@ -0,0 +1,103 @@ +/** + * @file llswapchain.h + * @brief Backend-agnostic swap chain wrapping the window's presentation images. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLSWAPCHAIN_H +#define LL_LLSWAPCHAIN_H + +#include "llrendertarget.h" + +#include + +class LLWindow; + +// Owns the collection of LLRenderTargets that act as the window's presentation +// images, plus the present primitive itself. The viewer acquires an image +// each frame, renders into it via the usual LLRenderTarget interface, then +// asks the swap chain to present. +// +// GL backend (today): the chain holds N off-screen color+depth FBOs sized to +// the window. acquireNextImage() rotates the index. present() blits the +// current image's color to FBO 0 and calls swapBuffers(). The literal 0 +// only appears inside present() — viewer code never names it. +// +// Vulkan / OpenXR backends (future): mImages map onto the runtime's actual +// swap chain images; acquireNextImage / present become the real WSI calls +// (vkAcquireNextImageKHR / vkQueuePresentKHR or xrAcquire/Release pairs). +// The viewer code that uses this interface doesn't change. +class LLSwapChain +{ +public: + // How many images cycle in the chain. 2 is the minimum useful number; + // there is no parallelism gain on GL above 2 (the driver does its own + // back-buffer rotation under SwapBuffers). + static constexpr U32 kImageCount = 2; + + LLSwapChain() = default; + ~LLSwapChain(); + + LLSwapChain(const LLSwapChain&) = delete; + LLSwapChain& operator=(const LLSwapChain&) = delete; + + // GL backend: allocates kImageCount off-screen color+depth RTs at + // (width, height) and remembers the window so present() can swapBuffers. + void attachToWindow(LLWindow* window, U32 width, U32 height); + + // Resize all images to (width, height). No-op on zero dimensions. + void resize(U32 width, U32 height); + + // Rotate to the next image in the chain and return it. Render into the + // returned RT this frame; call present() when done. Each call to + // acquireNextImage() should be balanced 1:1 with a present(). + LLRenderTarget& acquireNextImage(); + + // Image currently being rendered into (or just rendered). Use for + // readback consumers that want the in-progress frame. + LLRenderTarget& getCurrentImage(); + + // Most recently presented image — i.e. the one whose contents are on + // screen right now. Useful for consumers (scene monitor) that want the + // last visible frame rather than the half-rendered current one. + LLRenderTarget& getPreviousImage(); + + // Blit the current image's color to FBO 0 and call window->swapBuffers(). + void present(); + + // Drop image resources and detach. Safe to call redundantly. + void release(); + + U32 getWidth() const { return mWidth; } + U32 getHeight() const { return mHeight; } + U32 getImageCount() const { return (U32)mImages.size(); } + +private: + std::vector mImages; + U32 mCurrentIndex = 0; + LLWindow* mWindow = nullptr; + U32 mWidth = 0; + U32 mHeight = 0; +}; + +#endif // LL_LLSWAPCHAIN_H diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 4a6739bb40f..99572ac07ea 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -1497,9 +1497,36 @@ bool LLAppViewer::doFrame() LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Snapshot"); pingMainloopTimeout("Main:Snapshot"); + + // These updaters allocate and bind their own render + // targets while no parent target is on the stack. Bind + // the swap chain image around the calls so they pop back + // to it instead of tripping the missing-parent assert in + // LLRenderTarget::flush(). Only wrap when the stack is + // empty — these updaters can recurse into display() (e.g. + // snapshot live preview → rawSnapshot → display) which + // may bring its own parent. + bool sc_bound = false; + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + mSwapChain.acquireNextImage().bindTarget(); + sc_bound = true; + } + gPipeline.mReflectionMapManager.update(); LLFloaterSnapshot::update(); // take snapshots LLFloaterSimpleSnapshot::update(); + + if (sc_bound && + LLRenderTarget::getCurrentBoundTarget() == &mSwapChain.getCurrentImage()) + { + // Only flush if our bind is still on top. Updaters + // can internally call swap() (e.g. rawSnapshot's + // non-deferred path), which will already have flushed + // the image. + mSwapChain.getCurrentImage().flush(); + } + gGLActive = false; } @@ -2034,6 +2061,10 @@ bool LLAppViewer::cleanup() LL_INFOS() << "Shutting down OpenGL" << LL_ENDL; + // Drop the swap chain before window teardown — its images are bound to + // the OS window and must not outlive it. + mSwapChain.release(); + // Shut down OpenGL if (gViewerWindow) { @@ -3369,6 +3400,13 @@ bool LLAppViewer::initWindow() stop_glerror(); gViewerWindow->initGLDefaults(); + // Stand up the presentation swap chain now that GL is up. From here on the + // viewer acquires an image from this chain each frame instead of touching + // FBO 0 directly. + mSwapChain.attachToWindow(gViewerWindow->getWindow(), + gViewerWindow->getWindowWidthRaw(), + gViewerWindow->getWindowHeightRaw()); + gSavedSettings.setBOOL("RenderInitError", false); gSavedSettings.saveToFile( gSavedSettings.getString("ClientSettingsFile"), true ); diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 6ecafae0363..a6b4148b370 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -49,6 +49,7 @@ #include "llsys.h" // for LLOSInfo #include "lltimer.h" #include "llappcorehttp.h" +#include "llswapchain.h" #include "threadpool_fwd.h" #include @@ -149,6 +150,12 @@ class LLAppViewer : public LLApp std::string getSecondLifeTitle() const; // The Second Life title. std::string getWindowTitle() const; // The window display name. + // The OS window's presentation swap chain. Owns the LLRenderTarget(s) that + // back FBO 0 (or the equivalent on future Vk/XR backends). Viewer code + // acquires an image from this chain per frame instead of touching FBO 0 + // directly. + LLSwapChain& getSwapChain() { return mSwapChain; } + void forceDisconnect(const std::string& msg); // Force disconnection, with a message to the user. // sendSimpleLogoutRequest does not create a marker file. @@ -373,6 +380,7 @@ class LLAppViewer : public LLApp S32 mNumSessions; std::string mSerialNumber; + LLSwapChain mSwapChain; bool mPurgeCache; bool mPurgeCacheOnExit; bool mPurgeUserDataOnExit; diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index 7498c2d524f..a9e5559254a 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -311,16 +311,17 @@ void LLSceneMonitor::capture() LLRenderTarget& cur_target = getCaptureTarget(); - U32 old_FBO = LLRenderTarget::sCurFBO; - gGL.getTexUnit(0)->bind(&cur_target); - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); //point to the main frame buffer. + + // Read from the most recently presented swap chain image — i.e. the + // frame currently on screen. The current image is mid-render at this + // point; the previous one is what the user sees. + LLRenderTarget& src = LLAppViewer::instance()->getSwapChain().getPreviousImage(); + src.bindForRead(); 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); + src.unbindRead(); mDiffState = NEED_DIFF; } diff --git a/indra/newview/llviewerassetupload.cpp b/indra/newview/llviewerassetupload.cpp index 65a69acc88f..1631a755142 100644 --- a/indra/newview/llviewerassetupload.cpp +++ b/indra/newview/llviewerassetupload.cpp @@ -934,7 +934,11 @@ void LLViewerAssetUpload::AssetInventoryUploadCoproc(LLCoreHttpUtil::HttpCorouti LLSD args; args["AMOUNT"] = llformat("%d", uploadPrice); - LLNotificationsUtil::add("UploadPayment", args); + // Notification add hits UI work that must run on the main coro. + LLAppViewer::instance()->postToMainCoro([args]() + { + LLNotificationsUtil::add("UploadPayment", args); + }); } } else @@ -1032,7 +1036,11 @@ void LLViewerAssetUpload::HandleUploadError(LLCore::HttpStatus status, LLSD &res args["ERROR"] = reason; } - LLNotificationsUtil::add(label, args); + // Notification add hits UI work that must run on the main coro. + LLAppViewer::instance()->postToMainCoro([label, args]() + { + LLNotificationsUtil::add(label, args); + }); if (uploadInfo->failedUpload(result, reason)) { diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 9dfa9a0efd2..271d19a4b65 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -168,6 +168,12 @@ void display_startup() LLGLState::checkStates(); + // Route startup-screen rendering through the swap chain so we never name + // FBO 0 here, AND so dynamic-texture updates below have a parent on the + // RT stack when they flush. + LLSwapChain& startup_sc = LLAppViewer::instance()->getSwapChain(); + startup_sc.acquireNextImage().bindTarget(); + if (frame_count++ > 1) // make sure we have rendered a frame first { LLViewerDynamicTexture::updateAllInstances(); @@ -193,8 +199,11 @@ void display_startup() LLGLState::checkStates(); - if (gViewerWindow && gViewerWindow->getWindow()) - gViewerWindow->getWindow()->swapBuffers(); + if (LLRenderTarget::getCurrentBoundTarget() == &startup_sc.getCurrentImage()) + { + startup_sc.getCurrentImage().flush(); + } + startup_sc.present(); glClear(GL_DEPTH_BUFFER_BIT); } @@ -417,13 +426,23 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) LLPerfStats::RecordSceneTime T (LLPerfStats::StatType_t::RENDER_DISPLAY); // render time capture - This is the main stat for overall rendering. - if (gWindowResized) + if (gWindowResized && !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); - gViewerWindow->getWindow()->swapBuffers(); + // Route the resize-skip black clear through the swap chain so we + // don't reach for FBO 0 here either. Only run when no other RT is on + // the stack — snapshots set their own bottom-of-stack target. + LLSwapChain& sc = LLAppViewer::instance()->getSwapChain(); + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + LLRenderTarget& img = sc.acquireNextImage(); + img.bindTarget(); + img.clear(GL_COLOR_BUFFER_BIT); + img.flush(); + sc.present(); + } LLPipeline::refreshCachedSettings(); gPipeline.resizeScreenTexture(); gResizeScreenTexture = false; @@ -695,6 +714,19 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) // Actually push all of our triangles to the screen. // + // Bind the OS window's presentation image as the bottom of the render + // target stack for this frame. Everything downstream — dynamic-texture + // updates, pipeline G-buffers, lighting, post-proc ping-pongs, + // renderFinalize's fullscreen triangle, and the UI — sits on top of this + // in the LLRenderTarget stack and pops back to it on flush. swap() + // flushes the image and calls present(). Snapshot renders go to their + // own off-screen target, so we skip the bind in that case. + LLSwapChain& swap_chain = LLAppViewer::instance()->getSwapChain(); + if (!for_snapshot && LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + swap_chain.acquireNextImage().bindTarget(); + } + // do render-to-texture stuff here if (gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_DYNAMIC_TEXTURES)) { @@ -1557,9 +1589,19 @@ void swap() LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SWAP ); // render time capture - Swap buffer time - can signify excessive data transfer to/from GPU LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Swap"); LL_PROFILE_GPU_ZONE("swap"); + + // Flush the swap chain's current image off the RT stack before presenting. + // The OS framebuffer stays bound (that's what present needs); this just + // pops the LLRenderTarget bookkeeping. + LLSwapChain& swap_chain = LLAppViewer::instance()->getSwapChain(); + if (LLRenderTarget::getCurrentBoundTarget() == &swap_chain.getCurrentImage()) + { + swap_chain.getCurrentImage().flush(); + } + if (gDisplaySwapBuffers) { - gViewerWindow->getWindow()->swapBuffers(); + swap_chain.present(); } gDisplaySwapBuffers = true; } diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7dd32074cfe..4a5ba7fec2e 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1175,6 +1175,19 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) LLGLDisable blend(GL_BLEND); gGL.setColorMask(true, true); + // Give mDownResMap a parent on the RT stack so its flush below pops + // back to the swap chain image instead of falling off the bottom. + // This pump can be called from idle() (no parent bound) or from + // display() (swap chain image already bound) — only wrap when the + // stack is empty. + LLSwapChain& dr_sc = LLAppViewer::instance()->getSwapChain(); + bool dr_sc_bound = false; + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + dr_sc.acquireNextImage().bindTarget(); + dr_sc_bound = true; + } + // just in case we downres textures, bind downresmap and copy program gPipeline.mDownResMap.bindTarget(); gCopyProgram.bind(); @@ -1211,6 +1224,12 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) gCopyProgram.unbind(); gPipeline.mDownResMap.flush(); + + if (dr_sc_bound && + LLRenderTarget::getCurrentBoundTarget() == &dr_sc.getCurrentImage()) + { + dr_sc.getCurrentImage().flush(); + } } return create_timer.getElapsedTimeF32(); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 4229ffcfb54..57ff3a863ff 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -2596,6 +2596,10 @@ void LLViewerWindow::reshape(S32 width, S32 height) mWindowRectRaw.mRight = mWindowRectRaw.mLeft + width; mWindowRectRaw.mTop = mWindowRectRaw.mBottom + height; + // Keep the presentation swap chain in sync with the window size so + // its image's viewport tracks the OS window. + LLAppViewer::instance()->getSwapChain().resize((U32)width, (U32)height); + //glViewport(0, 0, width, height ); LLViewerCamera * camera = LLViewerCamera::getInstance(); // simpleton, might not exist From a9a48d5a6839aec8734a19a2c74aa37b8c5d9037 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 9 Jun 2026 04:01:47 -0400 Subject: [PATCH 2/9] Introduce LLGPUResource Ported from a previous LLCompositor branch - idea is to introduce thread safety between multiple OpenGL contexts. Currently only used on LLRenderTarget and LLImageGL. --- doc/llpipeline-library-feasibility.md | 334 ++++++++++++++++++++++++ indra/llrender/CMakeLists.txt | 3 + indra/llrender/llcubemap.cpp | 4 +- indra/llrender/llcubemaparray.cpp | 3 +- indra/llrender/llglslshader.cpp | 3 +- indra/llrender/llgltexture.cpp | 2 +- indra/llrender/llgltexture.h | 3 +- indra/llrender/llgpuresource.cpp | 131 ++++++++++ indra/llrender/llgpuresource.h | 118 +++++++++ indra/llrender/llimagegl.cpp | 147 +++++++---- indra/llrender/llimagegl.h | 29 +- indra/llrender/llpostprocess.cpp | 13 +- indra/llrender/llrender.cpp | 191 +++++++++----- indra/llrender/llrendertarget.cpp | 195 +++++++++++--- indra/llrender/llrendertarget.h | 53 +++- indra/llrender/llresourcelease.h | 79 ++++++ indra/llrender/llswapchain.cpp | 10 +- indra/llrender/llswapchain.h | 4 +- indra/newview/gltfscenemanager.cpp | 6 +- indra/newview/llappviewer.cpp | 6 +- indra/newview/lldrawpoolbump.cpp | 5 +- indra/newview/llfloaterimagepreview.cpp | 23 +- indra/newview/llmaniptranslate.cpp | 11 +- indra/newview/llscenemonitor.cpp | 2 +- indra/newview/llviewerdisplay.cpp | 10 +- indra/newview/llviewertexture.cpp | 4 +- indra/newview/llviewertexturelist.cpp | 4 +- 27 files changed, 1183 insertions(+), 210 deletions(-) create mode 100644 doc/llpipeline-library-feasibility.md create mode 100644 indra/llrender/llgpuresource.cpp create mode 100644 indra/llrender/llgpuresource.h create mode 100644 indra/llrender/llresourcelease.h diff --git a/doc/llpipeline-library-feasibility.md b/doc/llpipeline-library-feasibility.md new file mode 100644 index 00000000000..a201fa317d4 --- /dev/null +++ b/doc/llpipeline-library-feasibility.md @@ -0,0 +1,334 @@ +# Feasibility Study: Extracting LLPipeline / the Render System into a Library + +**Status:** Draft for review +**Scope:** Architectural assessment only — no implementation +**Sibling document:** `doc/compositor-thread-feasibility.md` (the threading split; this +study assumes that work as orthogonal but compatible) + +--- + +## 1. Executive Summary + +The stated goal has three parts: + +1. `newview` / `secondlife-bin` becomes **headless by default**. +2. A **common renderer interface** exists so different rendering backends can be + selected at runtime (driver version, hardware, API availability, …). +3. OpenGL state is **removed from `secondlife-bin` and isolated** into a new + `llpipeline` library. + +**These are three different efforts of three very different sizes, and the study's +central finding is that they should not be pursued as one move.** Two of the three +are tractable and are *already in motion*. The third — literally extracting +`LLPipeline` and the draw-pool system into a standalone library — is the hard one, +and its feasibility reduces to a single question: + +> **Can we afford to insert an abstract drawable/scene interface that cuts the +> `LLDrawable ↔ LLViewerObject` knot?** + +That knot is the crux. `LLDrawable` does not *abstract* a scene object — it *wraps* +one: `lldrawable.h:41` includes `llviewerobject.h` (and `:43` includes +`llappviewer.h`). The draw pools downcast drawables to concrete `LLVO*` types +(terrain→`LLVOSurfacePatch`, water→`LLVOWater`, avatar→`LLVOAvatar`, +tree→`LLVOTree`). `LLSpatialPartition` branches on `getVObj()->getPCode()`. So any +"`llpipeline` library" that preserves that knot must drag `LLViewerObject`, +`LLVOVolume`, `LLVOAvatar`, `LLWorld`, `LLViewerRegion`, `LLViewerTexture`, … with +it — i.e. *most of `newview`* — which defeats the headless-bin goal, because the +app logic still needs its object model. **Without the seam: not feasible as +stated. With the seam: feasible, but a multi-month structural refactor.** + +**Verdict, by goal:** + +| Goal | Feasibility | Where the work lives | +|---|---|---| +| Backend-agnostic renderer interface | **Feasible, in progress** | inside `llrender` (extend `LLSwapChain`/`LLGPUResource`) | +| Headless by default | **Feasible, modest** | gate GL init in `llappviewer`; null backend | +| Extract `LLPipeline` + draw pools into a library | **Not feasible as stated; feasible only after a scene-interface refactor** | a new `llrender`-adjacent lib, *after* the knot is cut | + +**Recommendation:** pursue the backend interface and headless gating now — they sit +in `llrender`, which is already a clean lower-level library, and they deliver most +of the practical value the goal is reaching for. Treat the literal `LLPipeline` +extraction as a *later, optional* phase, explicitly gated on first building the +abstract drawable/scene seam. This is not a redirect away from the goal — it is the +path the in-flight `LLGPUResource` / `LLResourceLease` / `LLSwapChain` work is +already on. + +--- + +## 2. What "extract LLPipeline" actually entails + +`LLPipeline` is not a self-contained renderer. It is the **orchestrator of the +viewer's scene-to-screen process**, and it is fused to the viewer's domain model on +both sides: + +``` + INBOUND (~100 files reach in) OUTBOUND (pipeline reaches out) + UI / floaters / tools / settings gAgent, LLWorld, LLViewerRegion, + LLVO* scene objects ─── gPipeline ───► LLViewerCamera, LLSelectMgr, + startup / app / display │ LLEnvironment, LLViewerShaderMgr, + ▼ the entire LLViewerObject / LLVO* + draw pools, LLSpatialPartition, hierarchy, LLFace, ... + LLDrawable, LLFace +``` + +`pipeline.cpp` is ~11.5k lines and `#include`s ~90 headers, the large majority of +them viewer-domain: `llagent.h`, `llselectmgr.h`, `llworld.h`, `llviewerregion.h`, +`llviewerobject.h`, the whole `llvo*` family, plus UI (`llfloatertools.h`, +`llpanelface.h`, `llui.h`, `lltoolmgr.h`). This is the defining fact: **the pipeline +is woven through the viewer's object, world, agent, selection, and UI systems**, not +isolated behind them. + +### 2.1 Inbound coupling + +Roughly **~100 files** in `newview` reference `gPipeline` / `LLPipeline::`. The +traffic falls into three bands: + +- **Clean public API (fine across a boundary).** The dominant calls are drawable + lifecycle: `markRebuild`, `markMoved`, `markTextured`, plus render-type control + (`hasRenderType` / `toggleRenderType`) and `createObject`. These are proper + methods and would survive a library boundary unchanged. This band is the bulk of + the call *volume*. +- **Leaky static flags (a real problem).** External code reads — and in places + *writes* — pipeline statics directly: `sRenderingHUDs`, `sImpostorRender`, + `sShadowRender`, `sUnderWaterRender`, `sReflectionRender`, `sRenderDeferred`, + `sRenderTransparentWater`, and more. Some writers are UI/feature code (e.g. the + 360-capture floater toggling `sRenderAttachedLights` / `sUseOcclusion`; + settings-handlers in `llviewercontrol.cpp`). These are an informal shared-state + contract, not an API. +- **Direct member reach-in (the leakiest).** External code touches pipeline member + objects and render targets directly: `mReflectionMapManager`, `mHeroProbeManager`, + internal `LLRenderTarget`s (water reflection, exposure/luminance/scene maps used by + the GLTF material preview), and even increments instrumentation counters + (`mTextureMatrixOps`). The GLTF material-preview path chains calls through several + internal render targets to do its own post-processing. + +The clean band is library-ready. The other two bands are an **encapsulation debt** +that must be paid down (accessors / a render-control facade / a state +snapshot-restore API) *before* a boundary can be drawn — independent of any backend +question. + +> Note on precision: the counts above are deliberately given as magnitudes. They +> come from grep-style occurrence counting, which is fine for ranking the coupling +> but is not a measurement to base a schedule on. + +### 2.2 Outbound coupling — the knot + +The harder direction. The render system's core data structures *are* scene-graph +structures: + +- **`LLDrawable` wraps `LLViewerObject`.** `lldrawable.h:41` `#include + "llviewerobject.h"`; the constructor takes `LLViewerObject*`; accessors like + `getRegion()`, `getTextureEntry()`, `getVOVolume()`, `isAvatar()` forward straight + to the wrapped object. It is a companion struct, not an abstraction. +- **`LLFace` binds geometry to a viewer object.** Constructed from + `LLViewerObject*`; `getWorldMatrix()` / `getTextureEntry()` call through to it; it + downcasts to `LLVOVolume` / `LLVOAvatar` for volume face data. +- **Draw pools are specialized per `LLVO*` subclass.** Terrain reads + `LLVOSurfacePatch` and `gAgent.getRegion()`; water casts faces to `LLVOWater` and + reads water settings; the avatar pool pulls in `llvoavatar.h` + `llagent.h`; tree + reads `getRegion()->mRenderMatrix`. +- **`LLSpatialPartition` choreographs LOD/visibility by object type**, branching on + `getVObj()->getPCode()` and downcasting to `LLVOVolume` / `LLVOAvatar` / + `LLControlAvatar`. + +The **one genuine abstraction seam already present** is `LLDrawInfo` (the per-batch +descriptor in `llspatialpartition.h`: vertex buffer + texture + model matrix + +shader + counts). Everything at `LLDrawInfo` level and below is backend-shaped data; +everything above it (`LLDrawable`, `LLFace`, `LLSpatialPartition`, the pools' +*setup*) is scene-graph logic. But `LLDrawInfo` is a thin slice — the issuing of +draw calls — not the bulk of the pipeline. + +`LLVertexBuffer` already lives in `llrender`, so the lowest layer is in the right +place. The problem is everything between `LLDrawInfo` and `LLViewerObject`. + +--- + +## 3. The backend-agnostic renderer interface + +This is the genuinely valuable and *separable* piece — and the part already moving. + +### 3.1 Where things stand + +`llrender` is **already a clean lower-level library**: it links `llcommon`, +`llimage`, `llmath`, `llwindow`, etc., and pulls in **no `newview` headers**. Where +it needs viewer services it inverts the dependency — e.g. `LLTextureManagerBridge` +(`gTextureManagerBridgep`) lets `llrender` call back into the viewer's texture +manager without compile-time coupling. `llrender.h`'s own header comment states the +intent outright: *"to define an interface for a multiple rendering API +abstraction."* And the in-flight files in this branch — `llgpuresource.h/.cpp`, +`llresourcelease.h`, `llswapchain.h/.cpp` — are the first courses of exactly that +wall, with `LLImageGL` as the first `LLGPUResource` adopter. + +So the right home for the renderer interface is **inside / beneath `llrender`**, not +a new `llpipeline` library. The backend question is about *how draw commands and GPU +resources are realized*, which is `llrender`'s job; it is not about *what scene to +draw*, which is `LLPipeline`'s job. + +### 3.2 The size of the backend job + +OpenGL is **pervasively assumed**, not funneled through one thin layer. There is +partial indirection — enum-translation tables (`LLRender` enums → `GL_*`), RAII +state wrappers (`LLGLEnable`, `LLGLDepthTest`, `LLGLSPipeline`), and the immediate- +mode `gGL` emulator that buffers vertices and caches matrix/texture/light state. But +beneath that: + +- Raw `glXxx` calls are scattered across `llrendertarget.cpp` (FBO management), + `llvertexbuffer.cpp` (VBOs), `llglslshader.cpp` (program/uniform calls), + `llrender.cpp` (texture units, blend, cull), and `llimagegl.cpp` (textures). +- GL types leak into public signatures (`GLuint`, `GLenum`, `GLint` in + `LLImageGL` / `LLVertexBuffer` / `LLRender` APIs). +- **Shaders are GLSL-specific end to end.** `LLGLSLShader` calls + `glAttachShader`/`glLinkProgram`/`glGetUniformLocation`/`glUniform*` directly; + `LLViewerShaderMgr` (in `newview`) loads `.glsl` files and bakes permutations in as + preprocessor `#define`s. A non-GL backend needs a different shader story + (SPIR-V / cross-compilation), and this is the single largest sub-cost. + +This is **systematic refactoring, not a rewrite**: route the scattered `glXxx` +through a backend interface, replace GL types in public APIs with opaque resource +handles (the `LLGPUResource` direction), and add a shader-compilation abstraction. A +useful completeness test is to stand up a **null backend** first (see §4) and a +single alternative backend second to validate the interface — before claiming it is +backend-agnostic. + +--- + +## 4. Headless by default + +This is the smallest and most independently shippable of the three goals — and a +prerequisite that falls out naturally from the backend interface (a "null" backend +*is* headless). + +**Current state is shallow.** `gHeadlessClient` is read in `llappviewer.cpp:3270`, +but the GL bring-up immediately after runs **unconditionally**: `gPipeline.init()` +(`:3397`), `gViewerWindow->initGLDefaults()` (`:3401`), and +`mSwapChain.attachToWindow()` (`:3406`) have no headless guard. The *only* thing +`gHeadlessClient` actually gates is the per-frame `display()` call +(`llappviewer.cpp:1488`; early-out at `llviewerdisplay.cpp:551`). So today "headless" +means "still build the whole GL pipeline, just don't draw each frame." + +**True headless-by-default** therefore requires gating *initialization*, not just +the frame loop: + +- A null renderer backend selected when no GL context is available/desired, so + `gPipeline.init()`, shader compilation, and swap-chain attach become no-ops or + route to stubs. +- Confirming that feature/GPU detection (`llfeaturemanager` via `gGLManager`) and + `LLViewerShaderMgr` tolerate the null backend. + +Critically, this is achievable **without moving `LLPipeline` anywhere** — it is a +gating + null-backend exercise on top of §3. It is the cleanest near-term win and a +good forcing function for the backend interface's completeness. + +--- + +## 5. Why the "one new llpipeline library" framing misleads + +The goal pictures: `secondlife-bin` = headless app logic with no GL, and a new +`llpipeline` library = all rendering + all GL, selectable by backend. The trouble is +that **"all rendering" in this codebase is not separable from "the scene/world/ +objects."** Moving `LLPipeline` + draw pools + `LLDrawable` + `LLSpatialPartition` +into a library drags `LLViewerObject`, the `LLVO*` hierarchy, `LLWorld`, +`LLViewerRegion`, `LLFace`, and `LLViewerTexture` along — and those depend back on +the pipeline (the ~100 inbound files), producing a **circular dependency** that a +static library boundary cannot express. You would not end up with "thin headless bin ++ render lib"; you would end up with "two libraries that each need the other," i.e. +the monolith with a seam drawn through its middle. + +The backend/GL-isolation value the goal is reaching for does **not** require that +move. GL isolation belongs in `llrender` (§3); headless belongs in app-init gating +(§4). The `LLPipeline`-as-library move only becomes coherent *after* the +`LLDrawable ↔ LLViewerObject` knot is cut — and at that point the library boundary +is the abstract scene interface, not "`LLPipeline`." + +--- + +## 6. If the literal extraction is still wanted: the seam + +Should the team decide the `LLPipeline` library is a hard requirement, the +unavoidable enabling work is an **abstract drawable/scene interface** that the render +system consumes *instead of* `LLViewerObject`: + +1. Define an abstract render-item interface (geometry, transform, material, LOD, + visibility) that exposes what pools and the spatial partition need, with **no + `LLViewerObject` knowledge**. `LLDrawInfo` is the model for the batch level; the + harder part is abstracting what `LLDrawable`/`LLFace` expose. +2. Make `LLViewerObject` / `LLVO*` *implement* (or feed) that interface; invert the + downcasts in the pools and spatial partition so they call the interface, not + concrete `LLVO*` types. +3. Pay down the §2.1 inbound debt: replace direct static-flag and member reach-ins + with a render-control facade and an explicit save/restore state API. +4. Only then move `LLPipeline` + pools + the now-abstract drawable/partition into a + library that depends on the interface, leaving `LLViewerObject` and the world + model in `newview`. + +Steps 1–3 are independently valuable (they improve the monolith even if the library +is never built) and are where the multi-month cost lives. Step 4 is comparatively +mechanical once 1–3 are done. + +--- + +## 7. Risk register + +| # | Risk | Severity | Note | +|---|------|----------|------| +| 1 | Treating the three goals as one move | **High** | Forces the impossible-as-stated extraction to gate the tractable wins; decouple them | +| 2 | `LLDrawable`/`LLFace` wrap `LLViewerObject` directly | **High** | The structural blocker; only an abstract scene interface resolves it | +| 3 | Circular dependency between a render lib and the object model | **High** | A static-lib boundary cannot express it; §5 | +| 4 | Leaky pipeline statics / member reach-ins (~2 of 3 coupling bands) | Medium | Encapsulation debt; payable independently of backend work | +| 5 | GLSL baked through `LLGLSLShader` + `LLViewerShaderMgr` | Medium-High | Largest sub-cost of the backend interface; needs a shader-IR story | +| 6 | "Headless" today only gates `display()`, not init | Medium | Verified; true headless needs init gating + null backend (§4) | +| 7 | Pool specialization per `LLVO*` subtype | Medium | Terrain/water/avatar/tree pools encode object-type logic, not generic geometry | +| 8 | Backend interface declared "done" without a second backend | Medium | Stand up null + one real alternate backend to prove completeness | + +--- + +## 8. Recommended path + +**Phase A — Backend interface + GL isolation, inside `llrender` (in progress).** +Continue the `LLGPUResource` / `LLResourceLease` / `LLSwapChain` work: opaque GPU +resource handles, a backend seam under `LLRender`/`LLRenderTarget`/`LLVertexBuffer`, +GL types out of public APIs. This isolates GL *below an interface* without moving +`LLPipeline`. + +**Phase B — Null backend + headless-by-default.** Add a null backend and gate +`gPipeline.init()` / shader compile / swap-chain attach on backend availability +(§4). Ship headless-by-default as a standalone capability. Doubles as the +completeness test for Phase A. + +**Phase C — Shader abstraction.** Decouple `LLGLSLShader` / `LLViewerShaderMgr` from +GLSL specifics (compilation + permutation strategy). Largest single backend sub-cost; +required before any non-GL backend is real. + +**Phase D — Encapsulation debt paydown.** Replace pipeline static-flag and +render-target reach-ins (§2.1 bands 2–3) with accessors / a render-control facade / +state snapshot API. Improves the monolith regardless of later steps. + +**Phase E (optional, gated) — The scene interface and the `llpipeline` library.** +Only if a true library split is still required: build the abstract drawable/scene +interface (§6), invert the pool/partition downcasts, then lift `LLPipeline` + pools ++ abstract drawable/partition into a library. This is the multi-month structural +effort; do not start it until A–D have proven the seams. + +A–D deliver essentially all of the stated *intent* — selectable backends, GL +isolated out of the bin's logic, headless by default — and are where current work +already points. E is the only part that is "extract `LLPipeline`" literally, and it +should be entered deliberately, with eyes open, as a scene-graph refactor rather than +a packaging change. + +--- + +## 9. Recommendation + +**Decouple the goal into its three real efforts and pursue them in dependency +order.** The backend interface and GL isolation (Phase A) and headless-by-default +(Phase B) are feasible, high-value, and already underway in `llrender` — that is the +correct home for them. The literal extraction of `LLPipeline` and the draw pools into +a library is **not feasible as stated**, because the render system's core types wrap +the viewer's object model rather than abstracting it; it becomes feasible only after +an abstract drawable/scene interface cuts the `LLDrawable ↔ LLViewerObject` knot, and +that interface — not "`LLPipeline`" — is the real library boundary. + +Frame the program as: *isolate the backend now, go headless now, and treat the +pipeline library as a later option that the scene-interface work unlocks.* That keeps +every near-term step shippable and avoids letting the hardest, least-certain piece +block the value that is already within reach. diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index 5e67f4c5e40..3f3c969f86b 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -29,6 +29,7 @@ set(llrender_SOURCE_FILES llrendersphere.cpp llrendertarget.cpp llswapchain.cpp + llgpuresource.cpp llshadermgr.cpp lltexture.cpp lltexturemanagerbridge.cpp @@ -61,6 +62,8 @@ set(llrender_HEADER_FILES llrendernavprim.h llrendersphere.h llswapchain.h + llgpuresource.h + llresourcelease.h llshadermgr.h lltexture.h lltexturemanagerbridge.h diff --git a/indra/llrender/llcubemap.cpp b/indra/llrender/llcubemap.cpp index b15cec58045..70696708934 100644 --- a/indra/llrender/llcubemap.cpp +++ b/indra/llrender/llcubemap.cpp @@ -243,9 +243,11 @@ void LLCubeMap::generateMipMaps() disable(); } +// Snapshot-return: caller is responsible for the texture's lifetime / lease +// if it crosses thread boundaries. GLuint LLCubeMap::getGLName() { - return mImages[0]->getTexName(); + return mImages[0]->getTexName().get(); } void LLCubeMap::bind() diff --git a/indra/llrender/llcubemaparray.cpp b/indra/llrender/llcubemaparray.cpp index 998b57217d4..9224b1c4a65 100644 --- a/indra/llrender/llcubemaparray.cpp +++ b/indra/llrender/llcubemaparray.cpp @@ -208,9 +208,10 @@ void LLCubeMapArray::unbind() mTextureStage = -1; } +// Snapshot-return: caller owns lifetime / lease for cross-thread use. GLuint LLCubeMapArray::getGLName() { - return mImage->getTexName(); + return mImage->getTexName().get(); } void LLCubeMapArray::destroyGL() diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index a268ea07bb4..0f2c2959fae 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -1165,7 +1165,8 @@ S32 LLGLSLShader::bindTexture(S32 uniform, LLRenderTarget* texture, bool depth, } else { bool has_mips = mode == LLTexUnit::TFO_TRILINEAR || mode == LLTexUnit::TFO_ANISOTROPIC; - gGL.getTexUnit(uniform)->bindManual(texture->getUsage(), texture->getTexture(index), has_mips); + auto guard = texture->getTexture(index); + gGL.getTexUnit(uniform)->bindManual(texture->getUsage(), guard.get(), has_mips); } gGL.getTexUnit(uniform)->setTextureFilteringOption(mode); diff --git a/indra/llrender/llgltexture.cpp b/indra/llrender/llgltexture.cpp index 4dcca5a726a..a967347ede5 100644 --- a/indra/llrender/llgltexture.cpp +++ b/indra/llrender/llgltexture.cpp @@ -216,7 +216,7 @@ S8 LLGLTexture::getComponents() const return mGLTexturep->getComponents() ; } -LLGLuint LLGLTexture::getTexName() const +LLScopedTexName LLGLTexture::getTexName() const { llassert(mGLTexturep.notNull()) ; diff --git a/indra/llrender/llgltexture.h b/indra/llrender/llgltexture.h index 122d2a7f9ca..2c753d1b0f3 100644 --- a/indra/llrender/llgltexture.h +++ b/indra/llrender/llgltexture.h @@ -30,6 +30,7 @@ #include "lltexture.h" #include "llgl.h" +#include "llgpuresource.h" class LLImageRaw; @@ -115,7 +116,7 @@ class LLGLTexture : public LLTexture /*virtual*/S32 getHeight(S32 discard_level = -1) const; bool hasGLTexture() const ; - LLGLuint getTexName() const ; + LLScopedTexName getTexName() const; bool createGLTexture() ; // Create a GL Texture from an image raw diff --git a/indra/llrender/llgpuresource.cpp b/indra/llrender/llgpuresource.cpp new file mode 100644 index 00000000000..b69a089d728 --- /dev/null +++ b/indra/llrender/llgpuresource.cpp @@ -0,0 +1,131 @@ +/** + * @file llgpuresource.cpp + * @brief LLGPUResource + lease implementation. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llgpuresource.h" + +LLGPUResource::~LLGPUResource() +{ + // mLeaseMutex is null if this resource was moved-from. + if (!mLeaseMutex) + { + return; + } + // Verify no leases are live. If a holder outlives the resource, the + // destructor proceeds through dangling state -- UB. + // try_lock returns true iff no shared or exclusive lock is held. + llassert(mLeaseMutex->try_lock()); + mLeaseMutex->unlock(); +} + +// -- LLSharedLease ---------------------------------------------------------- + +LLSharedLease::LLSharedLease(LLGPUResource* res) + : mResource(res) +{ + if (mResource) + { + mResource->mLeaseMutex->lock_shared(); + mResource->onSharedAcquire(); + } +} + +LLSharedLease::~LLSharedLease() +{ + release(); +} + +LLSharedLease::LLSharedLease(LLSharedLease&& other) noexcept + : mResource(other.mResource) +{ + other.mResource = nullptr; +} + +LLSharedLease& LLSharedLease::operator=(LLSharedLease&& other) noexcept +{ + if (this != &other) + { + release(); + mResource = other.mResource; + other.mResource = nullptr; + } + return *this; +} + +void LLSharedLease::release() +{ + if (mResource) + { + mResource->onSharedRelease(); + mResource->mLeaseMutex->unlock_shared(); + mResource = nullptr; + } +} + +// -- LLUniqueLease ---------------------------------------------------------- + +LLUniqueLease::LLUniqueLease(LLGPUResource* res) + : mResource(res) +{ + if (mResource) + { + mResource->mLeaseMutex->lock(); + mResource->onUniqueAcquire(); + } +} + +LLUniqueLease::~LLUniqueLease() +{ + release(); +} + +LLUniqueLease::LLUniqueLease(LLUniqueLease&& other) noexcept + : mResource(other.mResource) +{ + other.mResource = nullptr; +} + +LLUniqueLease& LLUniqueLease::operator=(LLUniqueLease&& other) noexcept +{ + if (this != &other) + { + release(); + mResource = other.mResource; + other.mResource = nullptr; + } + return *this; +} + +void LLUniqueLease::release() +{ + if (mResource) + { + mResource->onUniqueRelease(); + mResource->mLeaseMutex->unlock(); + mResource = nullptr; + } +} diff --git a/indra/llrender/llgpuresource.h b/indra/llrender/llgpuresource.h new file mode 100644 index 00000000000..5f717c28cb0 --- /dev/null +++ b/indra/llrender/llgpuresource.h @@ -0,0 +1,118 @@ +/** + * @file llgpuresource.h + * @brief Base class for GPU-bound resources with lease-based synchronization. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLGPURESOURCE_H +#define LL_LLGPURESOURCE_H + +#include "llresourcelease.h" +#include "llgltypes.h" + +#include +#include + +// Base class for GPU resources that need cross-thread synchronization. +// +// Provides two kinds of RAII lease: +// - shared (many concurrent holders, read-only access) +// - unique (exclusive, for mutation) +// +// Backed by std::shared_mutex. Subclasses override the on*Acquire/Release +// hooks to inject GL fences at the lease boundary when GPU-side ordering +// needs to follow CPU-side serialization -- e.g. a worker context's writes +// must be GPU-visible before a different context reads. Defaults are no-op, +// so resources that don't need cross-context fences pay nothing. +// +// Leases follow std::shared_mutex semantics: non-recursive, same-thread +// release. A thread that holds a lease must not acquire another lease on +// the same resource (deadlock). +class LLGPUResource +{ +public: + LLGPUResource() : mLeaseMutex(std::make_unique()) {} + virtual ~LLGPUResource(); + + LLGPUResource(const LLGPUResource&) = delete; + LLGPUResource& operator=(const LLGPUResource&) = delete; + + // Movable: the mutex hides behind a unique_ptr so the default move + // ctor works. After a move the source's mLeaseMutex is null -- further + // lease ops on it are UB. Only safe to move when no leases are live; + // in practice that's container setup time, never mid-render. Needed so + // vector et al. can reallocate. + LLGPUResource(LLGPUResource&& other) noexcept = default; + LLGPUResource& operator=(LLGPUResource&&) = delete; + + LLSharedLease getSharedLease() const { return LLSharedLease(const_cast(this)); } + LLUniqueLease getUniqueLease() { return LLUniqueLease(this); } + +protected: + // Hooks called by the lease lifecycle. Defaults are no-op. Subclasses + // override the ones they care about. Called with the appropriate lock + // held (shared lock for shared hooks, exclusive lock for unique hooks). + virtual void onSharedAcquire() {} + virtual void onSharedRelease() {} + virtual void onUniqueAcquire() {} + virtual void onUniqueRelease() {} + +private: + friend class LLSharedLease; + friend class LLUniqueLease; + mutable std::unique_ptr mLeaseMutex; +}; + +// Guarded GL texture name. Returned by accessors that would otherwise be +// snapshot-return -- the lease lives in this guard, keyed to the caller's +// scope. Use .get() to read the value; the guard's lifetime is what holds +// the lock. +// +// Common safe patterns: +// glBindTexture(target, img->getTexName().get()); // expression scope +// auto name = img->getTexName(); // statement scope +// ... use name.get() ... +// +// Unsafe (don't): assigning .get() into an LLGLuint local -- the guard temp +// destructs at the ;, leaving you using the value without the lock. +class LLScopedTexName +{ +public: + LLScopedTexName() = default; + LLScopedTexName(LLSharedLease lease, LLGLuint name) + : mLease(std::move(lease)), mName(name) {} + + LLScopedTexName(const LLScopedTexName&) = delete; + LLScopedTexName& operator=(const LLScopedTexName&) = delete; + LLScopedTexName(LLScopedTexName&&) = default; + LLScopedTexName& operator=(LLScopedTexName&&) = default; + + LLGLuint get() const { return mName; } + explicit operator bool() const { return mName != 0; } + +private: + LLSharedLease mLease; + LLGLuint mName = 0; +}; + +#endif // LL_LLGPURESOURCE_H diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ffc..ecd4f1d30f5 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -189,7 +189,8 @@ void LLImageGL::checkTexSize(bool forced) const bool error = false; if (texname != mTexName) { - LL_INFOS() << "Bound: " << texname << " Should bind: " << mTexName << " Default: " << LLImageGL::sDefaultGLTexture->getTexName() << LL_ENDL; + auto default_guard = LLImageGL::sDefaultGLTexture->getTexName(); + LL_INFOS() << "Bound: " << texname << " Should bind: " << mTexName << " Default: " << default_guard.get() << LL_ENDL; error = true; if (gDebugSession) @@ -684,11 +685,13 @@ void LLImageGL::dump() //---------------------------------------------------------------------------- void LLImageGL::forceUpdateBindStats(void) const { + LLSharedLease lease = getSharedLease(); mLastBindTime = sLastFrameTime; } bool LLImageGL::updateBindStats() const { + LLSharedLease lease = getSharedLease(); if (mTexName != 0) { #ifdef DEBUG_MISS @@ -709,6 +712,7 @@ bool LLImageGL::updateBindStats() const F32 LLImageGL::getTimePassedSinceLastBound() { + LLSharedLease lease = getSharedLease(); return sLastFrameTime - mLastBindTime ; } @@ -1506,14 +1510,15 @@ bool LLImageGL::createGLTexture() llassert(gGLManager.mInited); stop_glerror(); - if(mTexName) { - LLImageGL::deleteTextures(1, (reinterpret_cast(&mTexName))) ; - mTexName = 0; + LLUniqueLease lease = getUniqueLease(); + if(mTexName) + { + LLImageGL::deleteTextures(1, (reinterpret_cast(&mTexName))) ; + mTexName = 0; + } + LLImageGL::generateTextures(1, &mTexName); } - - - LLImageGL::generateTextures(1, &mTexName); stop_glerror(); if (!mTexName) { @@ -1718,12 +1723,9 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ } else { - //not on background thread, immediately set mTexName - if (old_texname != 0 && old_texname != new_texname) - { - LLImageGL::deleteTextures(1, &old_texname); - } - mTexName = new_texname; + // syncTexName takes the unique lease, deletes the prior name + // if different, and installs new_texname. + syncTexName(new_texname); } } @@ -1742,40 +1744,12 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) LL_PROFILE_ZONE_SCOPED; llassert(!on_main_thread()); + // Cross-context handoff via lease: the release fires onUniqueRelease + // which places the fence the main-thread consumer will wait on. { LL_PROFILE_ZONE_NAMED("cglt - sync"); - if (gGLManager.mIsNVIDIA) - { - // wait for texture upload to finish before notifying main thread - // upload is complete - auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(sync); - } - else - { - // post a sync to the main thread (will execute before tex name swap lambda below) - // glFlush calls here are partly superstitious and partly backed by observation - // on AMD hardware - glFlush(); - auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - LL::WorkQueue::postMaybe( - mMainQueue, - [=]() - { - LL_PROFILE_ZONE_NAMED("cglt - wait sync"); - { - LL_PROFILE_ZONE_NAMED("glWaitSync"); - glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); - } - { - LL_PROFILE_ZONE_NAMED("glDeleteSync"); - glDeleteSync(sync); - } - }); - } + LLUniqueLease lease = getUniqueLease(); + // release places fence + glFlush via the hook } ref(); @@ -1784,6 +1758,8 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) [=, this]() { LL_PROFILE_ZONE_NAMED("cglt - delete callback"); + // syncTexName takes the unique lease; its acquire-side hook + // server-side-waits on the fence the worker placed above. syncTexName(new_tex_name); unref(); }); @@ -1791,9 +1767,62 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) LL_PROFILER_GPU_COLLECT; } +// ---- LLGPUResource hooks -------------------------------------------------- + +void LLImageGL::onUniqueRelease() +{ + if (on_main_thread()) + { + // No cross-context handoff on main thread. + return; + } + + // Replace any prior fence. Safe to delete under unique lock -- shared + // and unique are mutually exclusive, so no waiter is mid-wait. + if (mPendingFence != nullptr) + { + glDeleteSync(mPendingFence); + mPendingFence = nullptr; + } + + glFlush(); + mPendingFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + mPendingFenceThread.store(std::this_thread::get_id(), std::memory_order_release); + glFlush(); +} + +void LLImageGL::onSharedAcquire() +{ + // Concurrent shared holders all wait on the same fence -- fine, glWaitSync + // is idempotent. The next unique holder deletes (see mPendingFence doc). + if (mPendingFence != nullptr && + mPendingFenceThread.load(std::memory_order_acquire) != std::this_thread::get_id()) + { + glWaitSync(mPendingFence, 0, GL_TIMEOUT_IGNORED); + } +} + +void LLImageGL::onUniqueAcquire() +{ + if (mPendingFence != nullptr && + mPendingFenceThread.load(std::memory_order_acquire) != std::this_thread::get_id()) + { + glWaitSync(mPendingFence, 0, GL_TIMEOUT_IGNORED); + } + // Exclusive -- drop the fence so the next release can place a fresh one. + if (mPendingFence != nullptr) + { + glDeleteSync(mPendingFence); + mPendingFence = nullptr; + } +} + void LLImageGL::syncTexName(LLGLuint texname) { + // Mutates mTexName -- unique lease. Acquire waits on any pending + // cross-thread fence before we touch it. + LLUniqueLease lease = getUniqueLease(); if (texname != 0) { if (mTexName != 0 && mTexName != texname) @@ -1804,6 +1833,12 @@ void LLImageGL::syncTexName(LLGLuint texname) } } +void LLImageGL::setTexName(GLuint texName) +{ + LLUniqueLease lease = getUniqueLease(); + mTexName = texName; +} + bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compressed_ok) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; @@ -1939,6 +1974,7 @@ void LLImageGL::destroyGLTexture() { checkActiveThread(); + LLUniqueLease lease = getUniqueLease(); if (mTexName != 0) { if(mTextureMemory != S64Bytes(0)) @@ -2002,6 +2038,7 @@ void LLImageGL::setFilteringOption(LLTexUnit::eTextureFilterOptions option) bool LLImageGL::getIsResident(bool test_now) { + LLSharedLease lease = getSharedLease(); if (test_now) { if (mTexName != 0) @@ -2019,6 +2056,7 @@ bool LLImageGL::getIsResident(bool test_now) S32 LLImageGL::getHeight(S32 discard_level) const { + LLSharedLease lease = getSharedLease(); if (discard_level < 0) { discard_level = mCurrentDiscardLevel; @@ -2030,6 +2068,7 @@ S32 LLImageGL::getHeight(S32 discard_level) const S32 LLImageGL::getWidth(S32 discard_level) const { + LLSharedLease lease = getSharedLease(); if (discard_level < 0) { discard_level = mCurrentDiscardLevel; @@ -2041,6 +2080,7 @@ S32 LLImageGL::getWidth(S32 discard_level) const S64 LLImageGL::getBytes(S32 discard_level) const { + LLSharedLease lease = getSharedLease(); if (discard_level < 0) { discard_level = mCurrentDiscardLevel; @@ -2054,6 +2094,7 @@ S64 LLImageGL::getBytes(S32 discard_level) const S64 LLImageGL::getMipBytes(S32 discard_level) const { + LLSharedLease lease = getSharedLease(); if (discard_level < 0) { discard_level = mCurrentDiscardLevel; @@ -2075,16 +2116,19 @@ S64 LLImageGL::getMipBytes(S32 discard_level) const bool LLImageGL::isJustBound() const { + LLSharedLease lease = getSharedLease(); return sLastFrameTime - mLastBindTime < 0.5f; } bool LLImageGL::getBoundRecently() const { + LLSharedLease lease = getSharedLease(); return (bool)(sLastFrameTime - mLastBindTime < MIN_TEXTURE_LIFETIME); } bool LLImageGL::getIsAlphaMask() const { + LLSharedLease lease = getSharedLease(); llassert_always(!sSkipAnalyzeAlpha); return mIsMask; } @@ -2321,6 +2365,7 @@ void LLImageGL::freePickMask() bool LLImageGL::isCompressed() { + LLSharedLease lease = getSharedLease(); llassert(mFormatPrimary != 0); // *NOTE: Not all compressed formats are included here. bool is_compressed = false; @@ -2386,8 +2431,20 @@ void LLImageGL::updatePickMask(S32 width, S32 height, const U8* data_in) } } +bool LLImageGL::getHasGLTexture() const +{ + LLSharedLease lease = getSharedLease(); + return mTexName != 0; +} + +LLScopedTexName LLImageGL::getTexName() const +{ + return LLScopedTexName(getSharedLease(), mTexName); +} + bool LLImageGL::getMask(const LLVector2 &tc) { + LLSharedLease lease = getSharedLease(); bool res = true; if (mPickMask) diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e7..c428da2c55b 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -37,8 +37,11 @@ #include "llunits.h" #include "llthreadsafequeue.h" #include "llrender.h" +#include "llgpuresource.h" #include "threadpool.h" #include "workqueue.h" +#include +#include #include #define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL @@ -57,7 +60,7 @@ namespace LLImageGLMemory } //============================================================================ -class LLImageGL : public LLRefCount +class LLImageGL : public LLRefCount, public LLGPUResource { friend class LLTexUnit; public: @@ -114,6 +117,14 @@ class LLImageGL : public LLRefCount void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); + // LLGPUResource hooks: place a GL fence when an off-main thread + // releases a unique lease, and server-side-wait on that fence when a + // different thread next acquires either kind of lease. Same-thread + // acquires after a same-thread release pay nothing. + void onUniqueRelease() override; + void onSharedAcquire() override; + void onUniqueAcquire() override; + public: virtual void dump(); // debugging info to LL_INFOS() @@ -168,8 +179,10 @@ class LLImageGL : public LLRefCount LLGLenum getPrimaryFormat() const { return mFormatPrimary; } LLGLenum getFormatType() const { return mFormatType; } - bool getHasGLTexture() const { return mTexName != 0; } - LLGLuint getTexName() const { return mTexName; } + bool getHasGLTexture() const; + // Returns a guard that owns a shared lease for the caller's scope. See + // LLScopedTexName doc for the safe / unsafe usage patterns. + LLScopedTexName getTexName() const; bool getIsAlphaMask() const; @@ -251,6 +264,14 @@ class LLImageGL : public LLRefCount U16 mHeight; S8 mCurrentDiscardLevel; + // Cross-context fence from the last off-thread unique release. Next + // cross-thread acquire glWaitSyncs on it. Only the unique lock writes + // or deletes the GLsync -- shared waiters just read. The mutex orders + // shared waiters and the unique deleter so they never overlap. + // - Geenz 2026-06-09 + GLsync mPendingFence = nullptr; + std::atomic mPendingFenceThread; + bool mAllowCompression; protected: @@ -321,7 +342,7 @@ class LLImageGL : public LLRefCount void setCategory(S32 category) {mCategory = category;} S32 getCategory()const {return mCategory;} - void setTexName(GLuint texName) { mTexName = texName; } + void setTexName(GLuint texName); //similar to setTexName, but will call deleteTextures on mTexName if mTexName is not 0 or texname void syncTexName(LLGLuint texname); diff --git a/indra/llrender/llpostprocess.cpp b/indra/llrender/llpostprocess.cpp index eef7193c92b..dc06b56cc61 100644 --- a/indra/llrender/llpostprocess.cpp +++ b/indra/llrender/llpostprocess.cpp @@ -210,7 +210,8 @@ void LLPostProcess::applyShaders(void) /// If any of the above shaders have been called update the frame buffer; if (tweaks.useColorFilter()) { - U32 tex = mSceneRenderTexture->getTexName() ; + auto guard = mSceneRenderTexture->getTexName(); + U32 tex = guard.get(); copyFrameBuffer(tex, screenW, screenH); } applyNightVisionShader(); @@ -220,7 +221,8 @@ void LLPostProcess::applyShaders(void) /// If any of the above shaders have been called update the frame buffer; if (tweaks.useColorFilter().asBoolean() || tweaks.useNightVisionShader().asBoolean()) { - U32 tex = mSceneRenderTexture->getTexName() ; + auto guard = mSceneRenderTexture->getTexName(); + U32 tex = guard.get(); copyFrameBuffer(tex, screenW, screenH); } applyBloomShader(); @@ -301,7 +303,8 @@ void LLPostProcess::doEffects(void) /// Copy the screen buffer to the render texture { - U32 tex = mSceneRenderTexture->getTexName() ; + auto guard = mSceneRenderTexture->getTexName(); + U32 tex = guard.get(); copyFrameBuffer(tex, screenW, screenH); } @@ -370,7 +373,7 @@ void LLPostProcess::createTexture(LLPointer& texture, unsigned int wi texture = new LLImageGL(false) ; if(texture->createGLTexture()) { - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, texture->getTexName()); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, texture->getTexName().get()); glTexImage2D(GL_TEXTURE_RECTANGLE, 0, 4, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &data[0]); gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR); @@ -390,7 +393,7 @@ void LLPostProcess::createNoiseTexture(LLPointer& texture) texture = new LLImageGL(false) ; if(texture->createGLTexture()) { - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, texture->getTexName()); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, texture->getTexName().get()); LLImageGL::setManualImage(GL_TEXTURE_2D, 0, GL_LUMINANCE, NOISE_SIZE, NOISE_SIZE, GL_LUMINANCE, GL_UNSIGNED_BYTE, &buffer[0]); gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR); gGL.getTexUnit(0)->setTextureAddressMode(LLTexUnit::TAM_WRAP); diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 57be8570afb..9058a817e31 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -196,22 +196,45 @@ void LLTexUnit::bindFast(LLTexture* texture) texture->setActive(); glActiveTexture(GL_TEXTURE0 + mIndex); gGL.mCurrTextureUnitIndex = mIndex; - mCurrTexture = gl_tex->getTexName(); - if (!mCurrTexture) + + bool needs_default = false; + { + auto guard = gl_tex->getTexName(); + mCurrTexture = guard.get(); + if (!mCurrTexture) + { + needs_default = true; + } + else + { + glBindTexture(sGLTextureType[gl_tex->getTarget()], mCurrTexture); + mHasMipMaps = gl_tex->mHasMipMaps; + if (gl_tex->mTexOptionsDirty) + { + gl_tex->mTexOptionsDirty = false; + setTextureAddressModeFast(gl_tex->mAddressMode, gl_tex->getTarget()); + setTextureFilteringOptionFast(gl_tex->mFilterOption, gl_tex->getTarget()); + } + } + } + if (needs_default) { LL_PROFILE_ZONE_NAMED("MISSING TEXTURE"); //if deleted, will re-generate it immediately + // No lease here -- re-entrant calls take their own unique. Trailing + // glBindTexture uses whatever mCurrTexture bindDefaultImage left, + // same as pre-lease. texture->forceImmediateUpdate(); gl_tex->forceUpdateBindStats(); texture->bindDefaultImage(mIndex); - } - glBindTexture(sGLTextureType[gl_tex->getTarget()], mCurrTexture); - mHasMipMaps = gl_tex->mHasMipMaps; - if (gl_tex->mTexOptionsDirty) - { - gl_tex->mTexOptionsDirty = false; - setTextureAddressModeFast(gl_tex->mAddressMode, gl_tex->getTarget()); - setTextureFilteringOptionFast(gl_tex->mFilterOption, gl_tex->getTarget()); + glBindTexture(sGLTextureType[gl_tex->getTarget()], mCurrTexture); + mHasMipMaps = gl_tex->mHasMipMaps; + if (gl_tex->mTexOptionsDirty) + { + gl_tex->mTexOptionsDirty = false; + setTextureAddressModeFast(gl_tex->mAddressMode, gl_tex->getTarget()); + setTextureFilteringOptionFast(gl_tex->mFilterOption, gl_tex->getTarget()); + } } } @@ -227,35 +250,49 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind) if (texture != NULL && (gl_tex = texture->getGLTexture())) { - if (gl_tex->getTexName()) //if texture exists + bool needs_default = false; + bool did_bind = false; { - //in audit, replace the selected texture by the default one. - if ((mCurrTexture != gl_tex->getTexName()) || forceBind) + // Guard scoped to texname read + glBindTexture only. + // Drop before updateBindStats (it takes its own shared lease). + auto guard = gl_tex->getTexName(); + U32 texname = guard.get(); + if (texname) { - activate(); - enable(gl_tex->getTarget()); - mCurrTexture = gl_tex->getTexName(); - glBindTexture(sGLTextureType[gl_tex->getTarget()], mCurrTexture); - if(gl_tex->updateBindStats()) + if ((mCurrTexture != texname) || forceBind) { - texture->setActive() ; - texture->updateBindStatsForTester() ; - } - mHasMipMaps = gl_tex->mHasMipMaps; - if (gl_tex->mTexOptionsDirty) - { - gl_tex->mTexOptionsDirty = false; - setTextureAddressMode(gl_tex->mAddressMode); - setTextureFilteringOption(gl_tex->mFilterOption); + activate(); + enable(gl_tex->getTarget()); + mCurrTexture = texname; + glBindTexture(sGLTextureType[gl_tex->getTarget()], mCurrTexture); + did_bind = true; } } + else + { + needs_default = true; + } } - else + if (did_bind) + { + if (gl_tex->updateBindStats()) + { + texture->setActive(); + texture->updateBindStatsForTester(); + } + mHasMipMaps = gl_tex->mHasMipMaps; + if (gl_tex->mTexOptionsDirty) + { + gl_tex->mTexOptionsDirty = false; + setTextureAddressMode(gl_tex->mAddressMode); + setTextureFilteringOption(gl_tex->mFilterOption); + } + } + if (needs_default) { //if deleted, will re-generate it immediately - texture->forceImmediateUpdate() ; - - gl_tex->forceUpdateBindStats() ; + texture->forceImmediateUpdate(); + gl_tex->forceUpdateBindStats(); return texture->bindDefaultImage(mIndex); } } @@ -284,36 +321,53 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 { stop_glerror(); if (mIndex < 0) return false; + if (!texture) return false; - U32 texname = usename ? usename : texture->getTexName(); - - if(!texture) + U32 texname = 0; + bool did_bind = false; { - LL_DEBUGS() << "NULL LLTexUnit::bind texture" << LL_ENDL; - return false; + // Guard scoped to texname read + glBindTexture only. Internal + // updateBindStats below takes its own shared lease -- can't nest. + auto guard = texture->getTexName(); + texname = usename ? usename : guard.get(); + + if (!texname) + { + // Fall through to the default-texture path; guard drops first. + } + else if ((mCurrTexture != texname) || forceBind) + { + gGL.flush(); + stop_glerror(); + activate(); + stop_glerror(); + enable(texture->getTarget()); + stop_glerror(); + mCurrTexture = texname; + glBindTexture(sGLTextureType[texture->getTarget()], mCurrTexture); + stop_glerror(); + did_bind = true; + } } - if(!texname) + if (!texname) { - if(LLImageGL::sDefaultGLTexture && LLImageGL::sDefaultGLTexture->getTexName()) + bool default_has_name = false; + if (LLImageGL::sDefaultGLTexture) { - return bind(LLImageGL::sDefaultGLTexture) ; + auto default_guard = LLImageGL::sDefaultGLTexture->getTexName(); + default_has_name = default_guard.get() != 0; + } + if (default_has_name) + { + return bind(LLImageGL::sDefaultGLTexture); } stop_glerror(); - return false ; + return false; } - if ((mCurrTexture != texname) || forceBind) + if (did_bind) { - gGL.flush(); - stop_glerror(); - activate(); - stop_glerror(); - enable(texture->getTarget()); - stop_glerror(); - mCurrTexture = texname; - glBindTexture(sGLTextureType[texture->getTarget()], mCurrTexture); - stop_glerror(); texture->updateBindStats(); mHasMipMaps = texture->mHasMipMaps; if (texture->mTexOptionsDirty) @@ -327,7 +381,6 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 } stop_glerror(); - return true; } @@ -343,28 +396,33 @@ bool LLTexUnit::bind(LLCubeMap* cubeMap) return false; } - if (mCurrTexture != cubeMap->mImages[0]->getTexName()) + bool changed = false; { - if (LLCubeMap::sUseCubeMaps) + auto guard = cubeMap->mImages[0]->getTexName(); + U32 texname = guard.get(); + if (mCurrTexture != texname) { + if (!LLCubeMap::sUseCubeMaps) + { + LL_WARNS() << "Using cube map without extension!" << LL_ENDL; + return false; + } activate(); enable(LLTexUnit::TT_CUBE_MAP); - mCurrTexture = cubeMap->mImages[0]->getTexName(); + mCurrTexture = texname; glBindTexture(GL_TEXTURE_CUBE_MAP, mCurrTexture); mHasMipMaps = cubeMap->mImages[0]->mHasMipMaps; - cubeMap->mImages[0]->updateBindStats(); - if (cubeMap->mImages[0]->mTexOptionsDirty) - { - cubeMap->mImages[0]->mTexOptionsDirty = false; - setTextureAddressMode(cubeMap->mImages[0]->mAddressMode); - setTextureFilteringOption(cubeMap->mImages[0]->mFilterOption); - } - return true; + changed = true; } - else + } + if (changed) + { + cubeMap->mImages[0]->updateBindStats(); + if (cubeMap->mImages[0]->mTexOptionsDirty) { - LL_WARNS() << "Using cube map without extension!" << LL_ENDL; - return false; + cubeMap->mImages[0]->mTexOptionsDirty = false; + setTextureAddressMode(cubeMap->mImages[0]->mAddressMode); + setTextureFilteringOption(cubeMap->mImages[0]->mFilterOption); } } return true; @@ -385,7 +443,8 @@ bool LLTexUnit::bind(LLRenderTarget* renderTarget, bool bindDepth) } else { - bindManual(renderTarget->getUsage(), renderTarget->getTexture()); + auto guard = renderTarget->getTexture(); + bindManual(renderTarget->getUsage(), guard.get()); } return true; diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index ec460d1405f..65dd8fb72db 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -75,9 +75,15 @@ LLRenderTarget::~LLRenderTarget() release(); } +// --------------------------------------------------------------------------- +// Public mutators -- each takes a unique lease and delegates to a _locked +// helper if it needs to call into another mutator. +// --------------------------------------------------------------------------- + void LLRenderTarget::resize(U32 resx, U32 resy) { - //for accounting, get the number of pixels added/subtracted + LLUniqueLease lease = getUniqueLease(); + S32 pix_diff = (resx*resy)-(mResX*mResY); mResX = resx; @@ -86,7 +92,7 @@ void LLRenderTarget::resize(U32 resx, U32 resy) llassert(mInternalFormat.size() == mTex.size()); for (U32 i = 0; i < mTex.size(); ++i) - { //resize color attachments + { gGL.getTexUnit(0)->bindManual(mUsage, mTex[i]); LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, mInternalFormat[i], mResX, mResY, GL_RGBA, GL_UNSIGNED_BYTE, NULL, false); sBytesAllocated += pix_diff*4; @@ -102,17 +108,18 @@ void LLRenderTarget::resize(U32 resx, U32 resy) } } - bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLTexUnit::eTextureType usage, LLTexUnit::eTextureMipGeneration generateMipMaps) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(usage == LLTexUnit::TT_TEXTURE); llassert(!isBoundInStack()); + LLUniqueLease lease = getUniqueLease(); + resx = llmin(resx, (U32) gGLManager.mGLMaxTextureSize); resy = llmin(resy, (U32) gGLManager.mGLMaxTextureSize); - release(); + release_locked(); mResX = resx; mResY = resy; @@ -123,15 +130,23 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT mGenerateMipMaps = generateMipMaps; if (mGenerateMipMaps != LLTexUnit::TMG_NONE) { - // Calculate the number of mip levels based upon resolution that we should have. mMipLevels = 1 + (U32)floor(log10((float)llmax(mResX, mResY)) / log10(2.0)); } if (depth) { - if (!allocateDepth()) + // Inlined allocateDepth body -- still under our unique lease. + LLImageGL::generateTextures(1, &mDepth); + gGL.getTexUnit(0)->bindManual(mUsage, mDepth); + U32 internal_type = LLTexUnit::getInternalType(mUsage); + stop_glerror(); + clear_glerror(); + LLImageGL::setManualImage(internal_type, 0, GL_DEPTH_COMPONENT24, mResX, mResY, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL, false); + gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_POINT); + sBytesAllocated += mResX*mResY*4; + if (glGetError() != GL_NO_ERROR) { - LL_WARNS() << "Failed to allocate depth buffer for render target." << LL_ENDL; + LL_WARNS() << "Unable to allocate depth buffer for render target." << LL_ENDL; return false; } } @@ -147,18 +162,20 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } - return addColorAttachment(color_fmt); + return addColorAttachment_locked(color_fmt); } void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) { LL_PROFILE_ZONE_SCOPED; - llassert(img != nullptr); // img must not be null - llassert(sUseFBO); // FBO support must be enabled - llassert(mDepth == 0); // depth buffers not supported with this mode - llassert(mTex.empty()); // mTex must be empty with this mode (binding target should be done via LLImageGL) + llassert(img != nullptr); + llassert(sUseFBO); + llassert(mDepth == 0); + llassert(mTex.empty()); llassert(!isBoundInStack()); + LLUniqueLease lease = getUniqueLease(); + if (mFBO == 0) { glGenFramebuffers(1, (GLuint*)&mFBO); @@ -168,9 +185,12 @@ void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) mResY = img->getHeight(); mUsage = img->getTarget(); + // Guarded read on img (different LLGPUResource, different mutex -- safe). + LLScopedTexName guard; if (use_name == 0) { - use_name = img->getTexName(); + guard = img->getTexName(); + use_name = guard.get(); } mTex.push_back(use_name); @@ -189,8 +209,10 @@ void LLRenderTarget::releaseColorAttachment() { LL_PROFILE_ZONE_SCOPED; llassert(!isBoundInStack()); - llassert(mTex.size() == 1); //cannot use releaseColorAttachment with LLRenderTarget managed color targets - llassert(mFBO != 0); // mFBO must be valid + llassert(mTex.size() == 1); + llassert(mFBO != 0); + + LLUniqueLease lease = getUniqueLease(); glBindFramebuffer(GL_FRAMEBUFFER, mFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, LLTexUnit::getInternalType(mUsage), 0, 0); @@ -204,6 +226,12 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(!isBoundInStack()); + LLUniqueLease lease = getUniqueLease(); + return addColorAttachment_locked(color_fmt); +} + +bool LLRenderTarget::addColorAttachment_locked(U32 color_fmt) +{ if (color_fmt == 0) { return true; @@ -229,7 +257,6 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) stop_glerror(); - { clear_glerror(); LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, color_fmt, mResX, mResY, GL_RGBA, GL_UNSIGNED_BYTE, NULL, false); @@ -244,14 +271,13 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) stop_glerror(); - if (offset == 0) - { //use bilinear filtering on single texture render targets that aren't multisampled + { gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR); stop_glerror(); } else - { //don't filter data attachments + { gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_POINT); stop_glerror(); } @@ -284,17 +310,18 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) if (gDebugGL) { //bind and unbind to validate target - bindTarget(); - flush(); + bindTarget_locked(); + flush_locked(); } - return true; } bool LLRenderTarget::allocateDepth() { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; + LLUniqueLease lease = getUniqueLease(); + LLImageGL::generateTextures(1, &mDepth); gGL.getTexUnit(0)->bindManual(mUsage, mDepth); @@ -319,6 +346,11 @@ void LLRenderTarget::shareDepthBuffer(LLRenderTarget& target) { llassert(!isBoundInStack()); + // Two-resource lock: acquire this then target -- consistent order across + // all callers prevents lock-order inversion. (Only one such method.) + LLUniqueLease lease_this = getUniqueLease(); + LLUniqueLease lease_other = target.getUniqueLease(); + if (!mFBO || !target.mFBO) { LL_ERRS() << "Cannot share depth buffer between non FBO render targets." << LL_ENDL; @@ -353,6 +385,12 @@ void LLRenderTarget::release() LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(!isBoundInStack()); + LLUniqueLease lease = getUniqueLease(); + release_locked(); +} + +void LLRenderTarget::release_locked() +{ mIsSwapChainImage = false; if (mDepth) @@ -368,7 +406,7 @@ void LLRenderTarget::release() glBindFramebuffer(GL_FRAMEBUFFER, mFBO); if (mUseDepth) - { //detach shared depth buffer + { glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), 0, 0); mUseDepth = false; } @@ -376,8 +414,6 @@ void LLRenderTarget::release() glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } - // Detach any extra color buffers (e.g. SRGB spec buffers) - // if (mFBO && (mTex.size() > 1)) { glBindFramebuffer(GL_FRAMEBUFFER, mFBO); @@ -421,17 +457,22 @@ void LLRenderTarget::bindTarget() llassert(!isBoundInStack()); llassert(mFBO); + LLUniqueLease lease = getUniqueLease(); + bindTarget_locked(); +} + +void LLRenderTarget::bindTarget_locked() +{ glBindFramebuffer(GL_FRAMEBUFFER, mFBO); sCurFBO = mFBO; - //setup multiple render targets GLenum drawbuffers[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3}; if (mTex.empty()) - { //no color buffer to draw to + { glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); } @@ -469,11 +510,13 @@ void LLRenderTarget::clear(U32 mask_in) { LL_PROFILE_GPU_ZONE("clear"); llassert(mFBO); + + LLUniqueLease lease = getUniqueLease(); + U32 mask = GL_COLOR_BUFFER_BIT; if (mUseDepth) { mask |= GL_DEPTH_BUFFER_BIT; - } if (mFBO) { @@ -491,25 +534,38 @@ void LLRenderTarget::clear(U32 mask_in) } } -U32 LLRenderTarget::getTexture(U32 attachment) const +LLScopedTexName LLRenderTarget::getTexture(U32 attachment) const { + LLSharedLease lease = getSharedLease(); if (attachment >= mTex.size()) { LL_WARNS() << "Invalid attachment index " << attachment << " for size " << mTex.size() << LL_ENDL; llassert(false); - return 0; + return LLScopedTexName(std::move(lease), 0); } - return mTex[attachment]; + LLGLuint name = mTex[attachment]; + return LLScopedTexName(std::move(lease), name); } U32 LLRenderTarget::getNumTextures() const { + LLSharedLease lease = getSharedLease(); return static_cast(mTex.size()); } void LLRenderTarget::bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options) { - gGL.getTexUnit(channel)->bindManual(mUsage, getTexture(index), filter_options == LLTexUnit::TFO_TRILINEAR || filter_options == LLTexUnit::TFO_ANISOTROPIC); + LLSharedLease lease = getSharedLease(); + bindTexture_locked(index, channel, filter_options); +} + +void LLRenderTarget::bindTexture_locked(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options) +{ + // Read mTex[index] directly under the caller's lease -- avoids the + // same-thread re-entry that calling getTexture() (also takes shared) + // would cause. + LLGLuint name = (index < mTex.size()) ? mTex[index] : 0; + gGL.getTexUnit(channel)->bindManual(mUsage, name, filter_options == LLTexUnit::TFO_TRILINEAR || filter_options == LLTexUnit::TFO_ANISOTROPIC); gGL.getTexUnit(channel)->setTextureFilteringOption(filter_options); } @@ -521,10 +577,16 @@ void LLRenderTarget::flush() llassert(mFBO); llassert(sCurFBO == mFBO); + LLUniqueLease lease = getUniqueLease(); + flush_locked(); +} + +void LLRenderTarget::flush_locked() +{ if (mGenerateMipMaps == LLTexUnit::TMG_AUTO) { LL_PROFILE_GPU_ZONE("rt generate mipmaps"); - bindTexture(0, 0, LLTexUnit::TFO_TRILINEAR); + bindTexture_locked(0, 0, LLTexUnit::TFO_TRILINEAR); glGenerateMipmap(GL_TEXTURE_2D); } @@ -540,7 +602,7 @@ void LLRenderTarget::flush() if (mIsSwapChainImage) { // Bottom of the stack for a render batch. Just pop the bookkeeping - // and leave the FBO bound — LLSwapChain::present() will rebind FBO 0 + // and leave the FBO bound -- LLSwapChain::present() will rebind FBO 0 // for the blit, and the next acquireNextImage() will set up the next // image's bind. sBoundTarget = nullptr; @@ -548,7 +610,7 @@ void LLRenderTarget::flush() } // No parent on the stack and not a swap chain image. While the swap chain - // is attached (steady-state rendering) this is a missed bind site — + // is attached (steady-state rendering) this is a missed bind site -- // assert so the call site can be wrapped. Before the chain attaches or // after release (feature-manager GPU benchmark, late shutdown cleanup) // fall back to the OS default framebuffer the way the old code did. @@ -567,6 +629,7 @@ void LLRenderTarget::flush() void LLRenderTarget::bindForRead() { + LLSharedLease lease = getSharedLease(); llassert(mFBO); glBindFramebuffer(GL_READ_FRAMEBUFFER, mFBO); glReadBuffer(GL_COLOR_ATTACHMENT0); @@ -574,17 +637,20 @@ void LLRenderTarget::bindForRead() void LLRenderTarget::unbindRead() { + // Doesn't read instance state -- no lease needed. glBindFramebuffer(GL_READ_FRAMEBUFFER, sCurFBO); glReadBuffer(sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); } bool LLRenderTarget::isComplete() const { + LLSharedLease lease = getSharedLease(); return !mTex.empty() || mDepth; } void LLRenderTarget::getViewport(S32* viewport) { + LLSharedLease lease = getSharedLease(); viewport[0] = 0; viewport[1] = 0; viewport[2] = mResX; @@ -593,6 +659,10 @@ void LLRenderTarget::getViewport(S32* viewport) bool LLRenderTarget::isBoundInStack() const { + // No lease: walks the static sBoundTarget chain through other RTs' + // mPreviousRT fields. Locking other RTs from here invites deadlock and + // protects nothing useful; this is a debug-style probe whose racy reads + // are acceptable. Same risk as before the lease refactor. LLRenderTarget* cur = sBoundTarget; while (cur && cur != this) { @@ -604,18 +674,18 @@ bool LLRenderTarget::isBoundInStack() const void LLRenderTarget::swapFBORefs(LLRenderTarget& other) { - // Must be initialized + // Two-resource lock: this then other (consistent order). + LLUniqueLease lease_this = getUniqueLease(); + LLUniqueLease lease_other = other.getUniqueLease(); + llassert(mFBO); llassert(other.mFBO); - // Must be unbound - // *NOTE: mPreviousRT can be non-null even if this target is unbound - presumably for debugging purposes? llassert(sCurFBO != mFBO); llassert(sCurFBO != other.mFBO); llassert(!isBoundInStack()); llassert(!other.isBoundInStack()); - // Must be same type llassert(sUseFBO == other.sUseFBO); llassert(mResX == other.mResX); llassert(mResY == other.mResY); @@ -630,3 +700,50 @@ void LLRenderTarget::swapFBORefs(LLRenderTarget& other) std::swap(mFBO, other.mFBO); std::swap(mTex, other.mTex); } + +// --------------------------------------------------------------------------- +// Self-contained scalar readers -- internal SHARED leases. +// --------------------------------------------------------------------------- + +U32 LLRenderTarget::getWidth() const +{ + LLSharedLease lease = getSharedLease(); + return mResX; +} + +U32 LLRenderTarget::getHeight() const +{ + LLSharedLease lease = getSharedLease(); + return mResY; +} + +LLTexUnit::eTextureType LLRenderTarget::getUsage() const +{ + LLSharedLease lease = getSharedLease(); + return mUsage; +} + +U32 LLRenderTarget::getDepth() const +{ + LLSharedLease lease = getSharedLease(); + return mDepth; +} + +U32 LLRenderTarget::getFBO() const +{ + // Snapshot-return -- caller is responsible for holding a SHARED lease + // externally if the value's use spans potential mutations on this RT. + return mFBO; +} + +void LLRenderTarget::markAsSwapChainImage(bool yes) +{ + LLUniqueLease lease = getUniqueLease(); + mIsSwapChainImage = yes; +} + +bool LLRenderTarget::isSwapChainImage() const +{ + LLSharedLease lease = getSharedLease(); + return mIsSwapChainImage; +} diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index 2813998609e..2802531645e 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -31,6 +31,7 @@ #include "llgl.h" #include "llrender.h" +#include "llgpuresource.h" /* Wrapper around OpenGL framebuffer objects for use in render-to-texture @@ -58,7 +59,10 @@ */ -class LLRenderTarget +// Inherits LLGPUResource: every public mutator takes an internal UNIQUE lease, +// every self-contained reader takes an internal SHARED lease. Snapshot-return +// accessors (getTexture, getFBO) -- see their docs. +class LLRenderTarget : public LLGPUResource { public: // Whether or not to use FBO implementation @@ -68,7 +72,7 @@ class LLRenderTarget static U32 sCurResX; static U32 sCurResY; - // When true, flush() requires a parent on the RT stack — i.e. the viewer + // When true, flush() requires a parent on the RT stack -- i.e. the viewer // has reached steady-state rendering with the swap chain attached. When // false (early init before swap chain attach, or after shutdown release), // flush() falls back to the OS default framebuffer so pre-attach code @@ -81,6 +85,15 @@ class LLRenderTarget LLRenderTarget(); ~LLRenderTarget(); + // Movable so std::vector can reallocate. The + // user-declared destructor would otherwise suppress implicit move + // generation, falling back to copy (deleted via LLGPUResource). + // Only safe to move when no leases are held; see LLGPUResource doc. + LLRenderTarget(LLRenderTarget&&) noexcept = default; + LLRenderTarget(const LLRenderTarget&) = delete; + LLRenderTarget& operator=(const LLRenderTarget&) = delete; + LLRenderTarget& operator=(LLRenderTarget&&) = delete; + //allocate resources for rendering //must be called before use //multiple calls will release previously allocated resources @@ -96,8 +109,8 @@ class LLRenderTarget // swap-chain image, glViewport is restored to gGLViewport (the world // view rect) instead of (0,0,mResX,mResY). HUD/UI rendering and // renderFinalize's fullscreen triangle expect that. - void markAsSwapChainImage(bool yes = true) { mIsSwapChainImage = yes; } - bool isSwapChainImage() const { return mIsSwapChainImage; } + void markAsSwapChainImage(bool yes = true); + bool isSwapChainImage() const; // Bind this RT's FBO as GL_READ_FRAMEBUFFER (preserves the current draw // FBO). Pair with unbindRead() to restore the read FB to the current @@ -165,22 +178,26 @@ class LLRenderTarget void getViewport(S32* viewport); //get X resolution - U32 getWidth() const { return mResX; } + U32 getWidth() const; //get Y resolution - U32 getHeight() const { return mResY; } + U32 getHeight() const; - LLTexUnit::eTextureType getUsage(void) const { return mUsage; } + LLTexUnit::eTextureType getUsage() const; - U32 getTexture(U32 attachment = 0) const; + // Returns a guard that owns a shared lease for the caller's scope. See + // LLScopedTexName for the safe / unsafe usage patterns. + LLScopedTexName getTexture(U32 attachment = 0) const; U32 getNumTextures() const; - U32 getDepth(void) const { return mDepth; } + U32 getDepth() const; // Underlying GL framebuffer object name. Exposed for LLSwapChain's // present-time blit; viewer code should bind through bindTarget / - // bindForRead instead of touching this directly. - U32 getFBO() const { return mFBO; } + // bindForRead instead of touching this directly. Snapshot-return -- + // caller is responsible for holding a SHARED lease on this RT for the + // value's use scope. + U32 getFBO() const; void bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options = LLTexUnit::TFO_BILINEAR); @@ -230,9 +247,21 @@ class LLRenderTarget // into its images. Generalizes sub-rect viewports for any RT and maps // naturally to Vk/XR state. Held off today because gGLViewport changes // more often than window resize (per-frame setup3DViewport, UI scale, - // sidebar) — syncing it everywhere is intrusive. Worth taking on as + // sidebar) -- syncing it everywhere is intrusive. Worth taking on as // part of the broader render-state cleanup for Vulkan port prep. bool mIsSwapChainImage = false; + +private: + // Lockless helpers: do not take a lease. Public wrappers above own the + // lease and call these; internal callers (allocate -> release_locked, + // addColorAttachment debug -> bindTarget_locked / flush_locked, flush mip + // gen -> bindTexture_locked) use them to avoid same-thread re-entry on + // the shared_mutex. + void release_locked(); + bool addColorAttachment_locked(U32 color_fmt); + void bindTarget_locked(); + void flush_locked(); + void bindTexture_locked(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options); }; #endif diff --git a/indra/llrender/llresourcelease.h b/indra/llrender/llresourcelease.h new file mode 100644 index 00000000000..861fe10be47 --- /dev/null +++ b/indra/llrender/llresourcelease.h @@ -0,0 +1,79 @@ +/** + * @file llresourcelease.h + * @brief RAII lease handles for LLGPUResource synchronization. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLRESOURCELEASE_H +#define LL_LLRESOURCELEASE_H + +class LLGPUResource; + +// RAII shared lease: many concurrent holders allowed, blocks while a unique +// lease is held. Non-copyable, movable. Same-thread release (std::shared_mutex +// contract). Non-recursive -- a thread already holding a lease on the same +// resource must not acquire another, including via subroutines. +class LLSharedLease +{ +public: + LLSharedLease() = default; // empty/moved-from state + explicit LLSharedLease(LLGPUResource* res); + ~LLSharedLease(); + + LLSharedLease(LLSharedLease&& other) noexcept; + LLSharedLease& operator=(LLSharedLease&& other) noexcept; + + LLSharedLease(const LLSharedLease&) = delete; + LLSharedLease& operator=(const LLSharedLease&) = delete; + + // True if this handle currently owns a lock. + explicit operator bool() const { return mResource != nullptr; } + +private: + void release(); + LLGPUResource* mResource = nullptr; +}; + +// RAII unique lease: exclusive, blocks all other leases (shared and unique). +// Use for mutations: writes, name swaps, destruction prep. +class LLUniqueLease +{ +public: + LLUniqueLease() = default; + explicit LLUniqueLease(LLGPUResource* res); + ~LLUniqueLease(); + + LLUniqueLease(LLUniqueLease&& other) noexcept; + LLUniqueLease& operator=(LLUniqueLease&& other) noexcept; + + LLUniqueLease(const LLUniqueLease&) = delete; + LLUniqueLease& operator=(const LLUniqueLease&) = delete; + + explicit operator bool() const { return mResource != nullptr; } + +private: + void release(); + LLGPUResource* mResource = nullptr; +}; + +#endif // LL_LLRESOURCELEASE_H diff --git a/indra/llrender/llswapchain.cpp b/indra/llrender/llswapchain.cpp index 526d17ce6e0..0a6de9b7a30 100644 --- a/indra/llrender/llswapchain.cpp +++ b/indra/llrender/llswapchain.cpp @@ -72,7 +72,7 @@ void LLSwapChain::resize(U32 width, U32 height) { if (width == 0 || height == 0) { - return; // minimized / iconified — keep existing storage + return; // minimized / iconified -- keep existing storage } mWidth = width; @@ -88,8 +88,8 @@ LLRenderTarget& LLSwapChain::acquireNextImage() { llassert(!mImages.empty()); - // Rotate to the next image. GL has no real "acquire" — the driver owns - // the back buffer rotation under SwapBuffers — so this is just structural + // Rotate to the next image. GL has no real "acquire" -- the driver owns + // the back buffer rotation under SwapBuffers -- so this is just structural // cycling. Vk/XR backends will do the real WSI acquire here. mCurrentIndex = (mCurrentIndex + 1) % (U32)mImages.size(); return mImages[mCurrentIndex]; @@ -121,6 +121,10 @@ void LLSwapChain::present() { LL_PROFILE_GPU_ZONE("swapchain present blit"); + // Hold a shared lease on the image across the blit -- getFBO is + // snapshot-return, the value must stay valid for the call. + LLSharedLease img_lease = img.getSharedLease(); + // Save current read FB so we don't disturb anyone else's state. // (sCurFBO tracks the current draw FB; flush() asserts it matches.) const U32 prev_fbo = LLRenderTarget::sCurFBO; diff --git a/indra/llrender/llswapchain.h b/indra/llrender/llswapchain.h index a4d66ce6512..9f904d4285d 100644 --- a/indra/llrender/llswapchain.h +++ b/indra/llrender/llswapchain.h @@ -41,7 +41,7 @@ class LLWindow; // GL backend (today): the chain holds N off-screen color+depth FBOs sized to // the window. acquireNextImage() rotates the index. present() blits the // current image's color to FBO 0 and calls swapBuffers(). The literal 0 -// only appears inside present() — viewer code never names it. +// only appears inside present() -- viewer code never names it. // // Vulkan / OpenXR backends (future): mImages map onto the runtime's actual // swap chain images; acquireNextImage / present become the real WSI calls @@ -77,7 +77,7 @@ class LLSwapChain // readback consumers that want the in-progress frame. LLRenderTarget& getCurrentImage(); - // Most recently presented image — i.e. the one whose contents are on + // Most recently presented image -- i.e. the one whose contents are on // screen right now. Useful for consumers (scene monitor) that want the // last visible frame rather than the half-rendered current one. LLRenderTarget& getPreviousImage(); diff --git a/indra/newview/gltfscenemanager.cpp b/indra/newview/gltfscenemanager.cpp index ac452b38a0b..8d24dd06c92 100644 --- a/indra/newview/gltfscenemanager.cpp +++ b/indra/newview/gltfscenemanager.cpp @@ -781,7 +781,7 @@ void GLTFSceneManager::bindTexture(Asset& asset, TextureType texture_type, Textu if (tex) { LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gl bind texture"); - glBindTexture(GL_TEXTURE_2D, tex->getTexName()); + glBindTexture(GL_TEXTURE_2D, tex->getTexName().get()); if (channel != -1 && texture.mSampler != -1) { // set sampler state @@ -802,12 +802,12 @@ void GLTFSceneManager::bindTexture(Asset& asset, TextureType texture_type, Textu } else { - glBindTexture(GL_TEXTURE_2D, fallback->getTexName()); + glBindTexture(GL_TEXTURE_2D, fallback->getTexName().get()); } } else { - glBindTexture(GL_TEXTURE_2D, fallback->getTexName()); + glBindTexture(GL_TEXTURE_2D, fallback->getTexName().get()); } } } diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 99572ac07ea..b6485b41e88 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -1503,8 +1503,8 @@ bool LLAppViewer::doFrame() // the swap chain image around the calls so they pop back // to it instead of tripping the missing-parent assert in // LLRenderTarget::flush(). Only wrap when the stack is - // empty — these updaters can recurse into display() (e.g. - // snapshot live preview → rawSnapshot → display) which + // empty -- these updaters can recurse into display() (e.g. + // snapshot live preview -> rawSnapshot -> display) which // may bring its own parent. bool sc_bound = false; if (LLRenderTarget::getCurrentBoundTarget() == nullptr) @@ -2061,7 +2061,7 @@ bool LLAppViewer::cleanup() LL_INFOS() << "Shutting down OpenGL" << LL_ENDL; - // Drop the swap chain before window teardown — its images are bound to + // Drop the swap chain before window teardown -- its images are bound to // the OS window and must not outlive it. mSwapChain.release(); diff --git a/indra/newview/lldrawpoolbump.cpp b/indra/newview/lldrawpoolbump.cpp index 6c151351ff9..8f451772360 100644 --- a/indra/newview/lldrawpoolbump.cpp +++ b/indra/newview/lldrawpoolbump.cpp @@ -907,9 +907,10 @@ void LLBumpImageList::onSourceUpdated(LLViewerTexture* src, EBumpEffect bump_cod LLImageGL::setManualImage(GL_TEXTURE_2D, 0, dst_img->getPrimaryFormat(), dst_img->getWidth(), dst_img->getHeight(), GL_RGBA, GL_UNSIGNED_BYTE, nullptr, false); - LLGLuint tex_name = dst_img->getTexName(); + // Guard holds the lease across the setColorAttachment. + auto guard = dst_img->getTexName(); // point render target at empty buffer - sRenderTarget.setColorAttachment(bump->getGLTexture(), tex_name); + sRenderTarget.setColorAttachment(bump->getGLTexture(), guard.get()); // generate normal map in empty texture { diff --git a/indra/newview/llfloaterimagepreview.cpp b/indra/newview/llfloaterimagepreview.cpp index bbff3e4c865..6d5d110bd4d 100644 --- a/indra/newview/llfloaterimagepreview.cpp +++ b/indra/newview/llfloaterimagepreview.cpp @@ -334,23 +334,26 @@ void LLFloaterImagePreview::draw() if(mImagep.notNull()) { - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, mImagep->getTexName()); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, mImagep->getTexName().get()); } else { mImagep = LLViewerTextureManager::getLocalTexture(mRawImagep.get(), false) ; gGL.getTexUnit(0)->unbind(mImagep->getTarget()) ; - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, mImagep->getTexName()); - stop_glerror(); - - gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR); - - gGL.getTexUnit(0)->setTextureAddressMode(LLTexUnit::TAM_CLAMP); - if (mAvatarPreview) { - mAvatarPreview->setTexture(mImagep->getTexName()); - mSculptedPreview->setTexture(mImagep->getTexName()); + auto guard = mImagep->getTexName(); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, guard.get()); + stop_glerror(); + + gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR); + + gGL.getTexUnit(0)->setTextureAddressMode(LLTexUnit::TAM_CLAMP); + if (mAvatarPreview) + { + mAvatarPreview->setTexture(guard.get()); + mSculptedPreview->setTexture(guard.get()); + } } } diff --git a/indra/newview/llmaniptranslate.cpp b/indra/newview/llmaniptranslate.cpp index 9bcfd9e2c02..a32e509f3a9 100644 --- a/indra/newview/llmaniptranslate.cpp +++ b/indra/newview/llmaniptranslate.cpp @@ -142,7 +142,13 @@ U32 LLManipTranslate::getGridTexName() restoreGL() ; } - return sGridTex.isNull() ? 0 : sGridTex->getTexName() ; + if (sGridTex.isNull()) + { + return 0; + } + // Snapshot-return: grid texture is a long-lived UI asset; caller doesn't + // race with worker uploads here. + return sGridTex->getTexName().get(); } //static @@ -171,7 +177,8 @@ void LLManipTranslate::restoreGL() GLuint* d = new GLuint[rez*rez]; - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, sGridTex->getTexName(), true); + auto grid_guard = sGridTex->getTexName(); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, grid_guard.get(), true); gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_TRILINEAR); while (rez >= 1) diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index a9e5559254a..be5bbe06fb5 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -313,7 +313,7 @@ void LLSceneMonitor::capture() gGL.getTexUnit(0)->bind(&cur_target); - // Read from the most recently presented swap chain image — i.e. the + // Read from the most recently presented swap chain image -- i.e. the // frame currently on screen. The current image is mid-render at this // point; the previous one is what the user sees. LLRenderTarget& src = LLAppViewer::instance()->getSwapChain().getPreviousImage(); diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 271d19a4b65..882d1d41f7e 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -158,7 +158,7 @@ void display_startup() // if (!LLViewerFetchedTexture::sWhiteImagep.isNull()) { - LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName(); + LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName().get(); } LLGLSDefault gls_default; @@ -433,7 +433,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) gGL.flush(); // Route the resize-skip black clear through the swap chain so we // don't reach for FBO 0 here either. Only run when no other RT is on - // the stack — snapshots set their own bottom-of-stack target. + // the stack -- snapshots set their own bottom-of-stack target. LLSwapChain& sc = LLAppViewer::instance()->getSwapChain(); if (LLRenderTarget::getCurrentBoundTarget() == nullptr) { @@ -715,9 +715,9 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) // // Bind the OS window's presentation image as the bottom of the render - // target stack for this frame. Everything downstream — dynamic-texture + // target stack for this frame. Everything downstream -- dynamic-texture // updates, pipeline G-buffers, lighting, post-proc ping-pongs, - // renderFinalize's fullscreen triangle, and the UI — sits on top of this + // renderFinalize's fullscreen triangle, and the UI -- sits on top of this // in the LLRenderTarget stack and pops back to it on flush. swap() // flushes the image and calls present(). Snapshot renders go to their // own off-screen target, so we skip the bind in that case. @@ -804,7 +804,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) LLDrawable::incrementVisible(); LLSpatialGroup::sNoDelete = true; - LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName(); + LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName().get(); S32 occlusion = LLPipeline::sUseOcclusion; if (gDepthDirty) diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 0f23596c9a8..27c8d970730 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -2924,7 +2924,9 @@ void LLViewerFetchedTexture::readbackRawImage() LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; // readback the raw image from vram if the current raw image is null or smaller than the texture - if (mGLTexturep.notNull() && mGLTexturep->getTexName() != 0 && + // Split conditions so each lease op (getHasGLTexture, getWidth, getHeight) + // runs sequentially -- avoids nested shared leases on the same mutex. + if (mGLTexturep.notNull() && mGLTexturep->getHasGLTexture() && (mRawImage.isNull() || mRawImage->getWidth() < mGLTexturep->getWidth() || mRawImage->getHeight() < mGLTexturep->getHeight() )) { if (mRawImage.isNull()) diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 4a5ba7fec2e..2d0e32d25f8 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -121,7 +121,7 @@ void LLViewerTextureList::doPreloadImages() // Set the "white" image LLViewerFetchedTexture::sWhiteImagep = LLViewerTextureManager::getFetchedTextureFromFile("white.tga", FTT_LOCAL_FILE, MIPMAP_NO, LLViewerFetchedTexture::BOOST_UI); - LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName(); + LLTexUnit::sWhiteTexture = LLViewerFetchedTexture::sWhiteImagep->getTexName().get(); LLUIImageList* image_list = LLUIImageList::getInstance(); // Set default particle texture @@ -1178,7 +1178,7 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) // Give mDownResMap a parent on the RT stack so its flush below pops // back to the swap chain image instead of falling off the bottom. // This pump can be called from idle() (no parent bound) or from - // display() (swap chain image already bound) — only wrap when the + // display() (swap chain image already bound) -- only wrap when the // stack is empty. LLSwapChain& dr_sc = LLAppViewer::instance()->getSwapChain(); bool dr_sc_bound = false; From 5b4c0df21d53df62e7775e41daac41698ba5c5dc Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 9 Jun 2026 06:52:50 -0400 Subject: [PATCH 3/9] Add LLCompositor, start moving main thread to viewer thread. Multi-context compositor, test layers & sync Add multi-context composition, threading, and test layers. Also add some synchronization here - this still needs a better vsync signal, but that's likely to come as part of a new frame limiter setup. --- indra/llcommon/llcommon.cpp | 2 +- indra/llcommon/llcoros.cpp | 4 +- indra/llcommon/llcoros.h | 6 +- indra/llcommon/llmainthreadtask.h | 4 +- indra/llcommon/llsingleton.h | 6 +- indra/llcommon/llthread.cpp | 78 +- indra/llcommon/llthread.h | 21 +- .../llcommon/tests/llmainthreadtask_test.cpp | 6 +- indra/llprimitive/llmodelloader.cpp | 4 +- indra/llrender/CMakeLists.txt | 5 + indra/llrender/llcompositable.h | 94 ++ indra/llrender/llcompositor.cpp | 281 +++++ indra/llrender/llcompositor.h | 163 +++ indra/llrender/llgl.cpp | 11 +- indra/llrender/llgl.h | 4 +- indra/llrender/llglslshader.cpp | 4 +- indra/llrender/llglslshader.h | 5 +- indra/llrender/llglstates.h | 7 +- indra/llrender/llgpuresource.cpp | 18 +- indra/llrender/llgpuresource.h | 48 +- indra/llrender/llimagegl.cpp | 39 +- indra/llrender/llimagegl.h | 19 +- indra/llrender/llrender.cpp | 12 +- indra/llrender/llrendertarget.cpp | 108 +- indra/llrender/llrendertarget.h | 109 +- indra/llrender/llresourcelease.h | 7 +- indra/llrender/llswapchain.cpp | 16 +- indra/llrender/llswapchain.h | 8 +- indra/llrender/lltestsquarecompositable.cpp | 178 +++ indra/llrender/lltestsquarecompositable.h | 104 ++ indra/llrender/llvertexbuffer.cpp | 6 +- indra/llrender/llvertexbuffer.h | 8 +- indra/llwindow/llwindow.h | 14 + indra/llwindow/llwindowwin32.cpp | 186 ++- indra/llwindow/llwindowwin32.h | 16 +- indra/newview/CMakeLists.txt | 2 + indra/newview/app_settings/settings.xml | 26 +- .../class1/interface/compositorblitF.glsl | 37 + .../class1/interface/compositorblitV.glsl | 39 + indra/newview/llappviewer.cpp | 1043 +++++++++++------ indra/newview/llappviewer.h | 116 +- indra/newview/lldrawpoolbump.cpp | 2 +- indra/newview/llfloatermodelpreview.cpp | 12 +- indra/newview/llfloaterperformance.cpp | 18 +- indra/newview/llfloaterperformance.h | 1 + indra/newview/llfloaterpreference.cpp | 2 +- .../llfloaterpreferencesgraphicsadvanced.cpp | 31 + .../llfloaterpreferencesgraphicsadvanced.h | 5 + indra/newview/llmodelpreview.cpp | 16 +- indra/newview/llpbrterrainfeatures.cpp | 4 +- indra/newview/llperfstats.cpp | 13 +- indra/newview/llperfstats.h | 1 + indra/newview/llscenemonitor.cpp | 8 +- indra/newview/llstartup.cpp | 8 +- indra/newview/llviewercontrol.cpp | 22 +- indra/newview/llviewerdisplay.cpp | 100 +- indra/newview/llviewerdisplay.h | 5 + indra/newview/llviewershadermgr.cpp | 11 + indra/newview/llviewershadermgr.h | 1 + indra/newview/llviewertexture.cpp | 6 +- indra/newview/llviewertexturelist.cpp | 27 +- indra/newview/llviewerthread.cpp | 107 ++ indra/newview/llviewerthread.h | 61 + indra/newview/llviewerwindow.cpp | 66 +- indra/newview/pipeline.cpp | 1 - .../default/xui/en/floater_performance.xml | 2 +- .../floater_preferences_graphics_advanced.xml | 54 +- .../skins/default/xui/en/menu_viewer.xml | 10 + .../en/panel_performance_autoadjustments.xml | 50 +- 69 files changed, 2794 insertions(+), 714 deletions(-) create mode 100644 indra/llrender/llcompositable.h create mode 100644 indra/llrender/llcompositor.cpp create mode 100644 indra/llrender/llcompositor.h create mode 100644 indra/llrender/lltestsquarecompositable.cpp create mode 100644 indra/llrender/lltestsquarecompositable.h create mode 100644 indra/newview/app_settings/shaders/class1/interface/compositorblitF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/compositorblitV.glsl create mode 100644 indra/newview/llviewerthread.cpp create mode 100644 indra/newview/llviewerthread.h diff --git a/indra/llcommon/llcommon.cpp b/indra/llcommon/llcommon.cpp index 7a22eaf2032..a3d78568204 100644 --- a/indra/llcommon/llcommon.cpp +++ b/indra/llcommon/llcommon.cpp @@ -126,7 +126,7 @@ void LLCommon::initClass() sAprInitialized = true; } LLTimer::initClass(); - assert_main_thread(); // Make sure we record the main thread + assert_viewer_thread(); // Make sure we record the main thread if (!sMasterThreadRecorder) { sMasterThreadRecorder = new LLTrace::ThreadRecorder(); diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index 9e95d9c85f1..a2251dc5b5c 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -73,9 +73,9 @@ bool LLCoros::on_main_coro() } // static -bool LLCoros::on_main_thread_main_coro() +bool LLCoros::on_viewer_thread_main_coro() { - return on_main_coro() && on_main_thread(); + return on_main_coro() && on_viewer_thread(); } // static diff --git a/indra/llcommon/llcoros.h b/indra/llcommon/llcoros.h index 9df52b6ed5a..7db0b37dbd8 100644 --- a/indra/llcommon/llcoros.h +++ b/indra/llcommon/llcoros.h @@ -100,10 +100,10 @@ class LL_COMMON_API LLCoros: public LLSingleton // llassert(LLCoros::on_main_coro()) static bool on_main_coro(); - // For debugging, return true if on the main thread and not in a coroutine + // For debugging, return true if on the viewer thread and not in a coroutine. // Non-thread-safe code in the main loop should be protected by - // llassert(LLCoros::on_main_thread_main_coro()) - static bool on_main_thread_main_coro(); + // llassert(LLCoros::on_viewer_thread_main_coro()) + static bool on_viewer_thread_main_coro(); /// The viewer's use of the term "coroutine" became deeply embedded before /// the industry term "fiber" emerged to distinguish userland threads from diff --git a/indra/llcommon/llmainthreadtask.h b/indra/llcommon/llmainthreadtask.h index c3ed7fef52d..18ed69d63c7 100644 --- a/indra/llcommon/llmainthreadtask.h +++ b/indra/llcommon/llmainthreadtask.h @@ -31,7 +31,7 @@ * exception. See std::packaged_task.) * * When you call dispatch() on the main thread (as determined by - * on_main_thread() in llthread.h), it simply calls your task and returns the + * on_viewer_thread() in llthread.h), it simply calls your task and returns the * result. * * When you call dispatch() on a secondary thread, it instantiates an @@ -52,7 +52,7 @@ class LLMainThreadTask template static auto dispatch(CALLABLE&& callable) -> decltype(callable()) { - if (on_main_thread()) + if (on_viewer_thread()) { // we're already running on the main thread, perfect return callable(); diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index e6989211ae9..640849e7604 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -32,7 +32,7 @@ #include #include "mutex.h" #include "lockstatic.h" -#include "llthread.h" // on_main_thread() +#include "llthread.h" // on_viewer_thread() #include "llmainthreadtask.h" #include "llprofiler.h" #include "llerror.h" @@ -548,7 +548,7 @@ class LLSingleton : public LLSingletonBase } // Here we need to construct a new instance. - if (on_main_thread()) + if (on_viewer_thread()) { // On the main thread, directly construct the instance while // holding the lock. @@ -657,7 +657,7 @@ class LLParamSingleton : public LLSingleton " twice!"}); return nullptr; } - else if (on_main_thread()) + else if (on_viewer_thread()) { // on the main thread, simply construct instance while holding lock super::logdebugs({super::template classname(), diff --git a/indra/llcommon/llthread.cpp b/indra/llcommon/llthread.cpp index 8c12ee7f12d..fb6b4f888a8 100644 --- a/indra/llcommon/llthread.cpp +++ b/indra/llcommon/llthread.cpp @@ -98,15 +98,18 @@ void set_thread_name( DWORD dwThreadID, const char* threadName) namespace { - LLThread::id_t main_thread() - { - // Using a function-static variable to identify the main thread - // requires that control reach here from the main thread before it - // reaches here from any other thread. We simply trust that whichever - // thread gets here first is the main thread. - static LLThread::id_t s_thread_id = LLThread::currentID(); - return s_thread_id; - } + // Thread identity anchors. A default-constructed thread id means + // "not set yet", so we don't need a separate flag. + // + // The viewer thread anchors itself at run() entry while other + // threads are already running and reading - atomic makes that + // intentional race well-defined. + std::atomic g_viewer_thread_id{}; + + // Set once on the OS main thread at init, before any thread that + // reads it exists - no atomic needed. Set explicitly rather than + // first-caller-wins since the viewer thread might get here first. + LLThread::id_t g_os_main_thread_id{}; #if LL_WINDOWS @@ -131,20 +134,61 @@ namespace #endif // LL_WINDOWS } // anonymous namespace -LL_COMMON_API bool on_main_thread() +LL_COMMON_API void set_viewer_thread() +{ + g_viewer_thread_id.store(LLThread::currentID(), std::memory_order_release); +} + +LL_COMMON_API void clear_viewer_thread() +{ + g_viewer_thread_id.store(LLThread::id_t(), std::memory_order_release); +} + +LL_COMMON_API bool on_viewer_thread() +{ + const LLThread::id_t id = g_viewer_thread_id.load(std::memory_order_acquire); + if (id == LLThread::id_t()) + { + // The viewer thread hasn't spawned yet, so the OS main thread + // is still doing its job. Defer to that check. + return on_os_main_thread(); + } + return (LLThread::currentID() == id); +} + +LL_COMMON_API bool assert_viewer_thread() { - return (LLThread::currentID() == main_thread()); + if (on_viewer_thread()) + return true; + + LL_WARNS() << "Illegal execution from thread id " << LLThread::currentID() + << " outside viewer thread " + << g_viewer_thread_id.load(std::memory_order_acquire) << LL_ENDL; + return false; +} + +LL_COMMON_API void set_os_main_thread() +{ + g_os_main_thread_id = LLThread::currentID(); +} + +LL_COMMON_API bool on_os_main_thread() +{ + if (g_os_main_thread_id == LLThread::id_t()) + { + // Not registered yet - give early init the benefit of the doubt. + return true; + } + return (LLThread::currentID() == g_os_main_thread_id); } -LL_COMMON_API bool assert_main_thread() +LL_COMMON_API bool assert_os_main_thread() { - auto curr = LLThread::currentID(); - auto main = main_thread(); - if (curr == main) + if (on_os_main_thread()) return true; - LL_WARNS() << "Illegal execution from thread id " << curr - << " outside main thread " << main << LL_ENDL; + LL_WARNS() << "Illegal execution from thread id " << LLThread::currentID() + << " outside OS main thread " << g_os_main_thread_id << LL_ENDL; return false; } diff --git a/indra/llcommon/llthread.h b/indra/llcommon/llthread.h index 8794ac93aac..55f12285a44 100644 --- a/indra/llcommon/llthread.h +++ b/indra/llcommon/llthread.h @@ -157,7 +157,24 @@ class LL_COMMON_API LLResponder : public LLThreadSafeRefCount //============================================================================ -extern LL_COMMON_API bool assert_main_thread(); -extern LL_COMMON_API bool on_main_thread(); +// The viewer thread is where we render frames and run most of what +// used to be the main loop. It used to be the OS main thread; now it +// runs on its own while the OS main thread handles the compositor. +// set_viewer_thread() anchors it when the thread starts; +// clear_viewer_thread() un-anchors it after the thread is joined at +// shutdown. While unset, on_viewer_thread() falls back to the OS main +// thread check, so early init and post-join cleanup both run as the +// acting viewer thread without tripping asserts. +extern LL_COMMON_API void set_viewer_thread(); +extern LL_COMMON_API void clear_viewer_thread(); +extern LL_COMMON_API bool assert_viewer_thread(); +extern LL_COMMON_API bool on_viewer_thread(); + +// The OS main thread owns the window message pump and the compositor. +// We record it explicitly at window init so it sticks to the real OS +// main thread rather than whoever happens to ask first. +extern LL_COMMON_API void set_os_main_thread(); +extern LL_COMMON_API bool assert_os_main_thread(); +extern LL_COMMON_API bool on_os_main_thread(); #endif // LL_LLTHREAD_H diff --git a/indra/llcommon/tests/llmainthreadtask_test.cpp b/indra/llcommon/tests/llmainthreadtask_test.cpp index 9ccf391327e..106b3f8c758 100644 --- a/indra/llcommon/tests/llmainthreadtask_test.cpp +++ b/indra/llcommon/tests/llmainthreadtask_test.cpp @@ -20,7 +20,7 @@ // other Linden headers #include "../test/lltut.h" #include "../test/sync.h" -#include "llthread.h" // on_main_thread() +#include "llthread.h" // on_viewer_thread() #include "lleventtimer.h" #include "lockstatic.h" @@ -38,7 +38,7 @@ namespace tut { // we're not testing the result; this is just to cache the // initial thread as the main thread. - on_main_thread(); + on_viewer_thread(); } }; typedef test_group llmainthreadtask_group; @@ -85,7 +85,7 @@ namespace tut // have to lock static mutex to set static data LockStatic()->ran = true; // indicate whether task was run on the main thread - return on_main_thread(); + return on_viewer_thread(); })); // wait for test<2>() to unblock us again mSync.yield_until(3); diff --git a/indra/llprimitive/llmodelloader.cpp b/indra/llprimitive/llmodelloader.cpp index 0383659f624..d26c6c9c1fd 100644 --- a/indra/llprimitive/llmodelloader.cpp +++ b/indra/llprimitive/llmodelloader.cpp @@ -140,14 +140,14 @@ LLModelLoader::LLModelLoader( , mDebugMode(debugMode) , mJointMap(legalJointNamesMap) { - assert_main_thread(); + assert_viewer_thread(); sActiveLoaderList.push_back(this) ; mWarningsArray = LLSD::emptyArray(); } LLModelLoader::~LLModelLoader() { - assert_main_thread(); + assert_viewer_thread(); sActiveLoaderList.remove(this); } diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index 3f3c969f86b..c4a6ac3634b 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -29,6 +29,8 @@ set(llrender_SOURCE_FILES llrendersphere.cpp llrendertarget.cpp llswapchain.cpp + llcompositor.cpp + lltestsquarecompositable.cpp llgpuresource.cpp llshadermgr.cpp lltexture.cpp @@ -62,6 +64,9 @@ set(llrender_HEADER_FILES llrendernavprim.h llrendersphere.h llswapchain.h + llcompositor.h + llcompositable.h + lltestsquarecompositable.h llgpuresource.h llresourcelease.h llshadermgr.h diff --git a/indra/llrender/llcompositable.h b/indra/llrender/llcompositable.h new file mode 100644 index 00000000000..6c7ec02f6d3 --- /dev/null +++ b/indra/llrender/llcompositable.h @@ -0,0 +1,94 @@ +/** + * @file llcompositable.h + * @brief Pure-virtual interface for compositor layer producers. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLCOMPOSITABLE_H +#define LL_LLCOMPOSITABLE_H + +#include "stdtypes.h" +#include "lltimer.h" + +#include +#include + +class LLRenderTarget; + +// One layer in the compositor's stack. A compositable owns its front buffer +// (an LLRenderTarget); the compositor takes a shared lease on it and samples +// it during present. +// +// Neither side blocks the other: frames hand over via tryAcquireNewFront, +// and the per-RT fence pair orders the GPU reads and writes. +class LLCompositable +{ +public: + virtual ~LLCompositable() = default; + + // The front buffer the compositor will draw from. Doesn't take a + // lease - the compositor handles that. + virtual LLRenderTarget& frontBuffer() = 0; + + // Destination offset (pixels, GL origin = bottom-left) where this + // layer's front buffer lands in the swap chain image. Fullscreen + // layers keep the 0,0 default. + virtual void compositeOffset(S32& x, S32& y) const { x = 0; y = 0; } + + // Display name for debug overlays (Show Render Info). + virtual std::string compositableName() const { return "Layer"; } + + // Called by the compositor before reading frontBuffer(). Mailbox-paced + // layers claim the latest published buffer here and return true when + // they got a fresh one; single-buffer layers keep the no-op default. + virtual bool tryAcquireNewFront() { return false; } + + // Producer-side frame metrics: wall-clock time between one publish and + // the next, written by produceFrame() and read by the stats overlay. + F32 lastFrameMs() const { return mFrameMs.load(std::memory_order_relaxed); } + + // Monotonic count of published frames. Rate = produced FPS. + U32 framesProduced() const { return mFramesProduced.load(std::memory_order_relaxed); } + +protected: + // Producers call this when a frame is complete - stamps the cycle + // metrics. + void produceFrame() + { + const F64 now = LLTimer::getTotalSeconds(); + if (mLastProduceTime > 0.0) + { + mFrameMs.store((F32)((now - mLastProduceTime) * 1000.0), + std::memory_order_relaxed); + } + mLastProduceTime = now; // producer-thread-only + mFramesProduced.fetch_add(1, std::memory_order_relaxed); + } + +private: + std::atomic mFrameMs{0.f}; + std::atomic mFramesProduced{0}; + F64 mLastProduceTime = 0.0; +}; + +#endif // LL_LLCOMPOSITABLE_H diff --git a/indra/llrender/llcompositor.cpp b/indra/llrender/llcompositor.cpp new file mode 100644 index 00000000000..720a39bd6f8 --- /dev/null +++ b/indra/llrender/llcompositor.cpp @@ -0,0 +1,281 @@ +/** + * @file llcompositor.cpp + * @brief LLCompositor implementation. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llcompositor.h" + +#include "lltestsquarecompositable.h" +#include "lltimer.h" +#include "llrendertarget.h" +#include "llgl.h" +#include "llglslshader.h" +#include "llwindow.h" + +#include + +LLCompositor::LLCompositor() = default; + +LLCompositor::~LLCompositor() +{ + release(); +} + +void LLCompositor::attachToWindow(LLWindow* window, U32 width, U32 height) +{ + mWindow = window; + mSwapChain.attachToWindow(window, width, height); +} + +void LLCompositor::createRefreshOverlay() +{ + // Runs on our own thread - the squares each spin up a producer + // thread with its own shared GL context, which needs our context + // current (it is, inside presentFrame). + if (mRefreshShown || !mWindow) + { + return; + } + mTestSquares.emplace_back(std::make_unique( + "Blue Square", 0, 64, 255, 1u, 40, 40, mWindow)); + mTestSquares.emplace_back(std::make_unique( + "Orange Square", 255, 128, 0, 2u, 180, 40, mWindow)); + mTestSquares.emplace_back(std::make_unique( + "Red Square", 255, 0, 0, 3u, 320, 40, mWindow)); + mTestSquares.emplace_back(std::make_unique( + "Purple Square", 160, 0, 255, 4u, 460, 40, mWindow)); + for (auto& square : mTestSquares) + { + square->connect(*this); + addCompositable(square.get()); + } + mRefreshShown = true; +} + +void LLCompositor::destroyRefreshOverlay() +{ + if (!mRefreshShown) + { + return; + } + // Drop them from the draw list before tearing down so the present + // loop never touches a destroyed square. + for (auto& square : mTestSquares) + { + removeCompositable(square.get()); + square->disconnect(); + } + mTestSquares.clear(); + mRefreshShown = false; +} + +void LLCompositor::resize(U32 width, U32 height) +{ + mSwapChain.resize(width, height); +} + +void LLCompositor::release() +{ + destroyRefreshOverlay(); + mCompositables.clear(); + mBlitShader = nullptr; // owned by LLViewerShaderMgr + mSwapChain.release(); + // The window is destroyed right after us; drop our pointer and any + // queued work so a late present/toggle can't touch it. + mWindow = nullptr; + mPendingSwapInterval.store(-1, std::memory_order_relaxed); + mPendingShowRefresh.store(-1, std::memory_order_relaxed); +} + +void LLCompositor::addCompositable(LLCompositable* c) +{ + llassert(c != nullptr); + // The stats overlay iterates this list from the viewer thread. + std::lock_guard lock(mCompositablesMutex); + mCompositables.push_back(c); +} + +void LLCompositor::removeCompositable(LLCompositable* c) +{ + std::lock_guard lock(mCompositablesMutex); + mCompositables.erase( + std::remove(mCompositables.begin(), mCompositables.end(), c), + mCompositables.end()); +} + +void LLCompositor::getLayerStats(std::vector& out) const +{ + out.clear(); + std::lock_guard lock(mCompositablesMutex); + for (LLCompositable* c : mCompositables) + { + const F32 frame_ms = c->lastFrameMs(); + out.push_back({c->compositableName(), + frame_ms, + frame_ms > 0.f ? 1000.f / frame_ms : 0.f}); + } +} + + +void LLCompositor::presentFrame() +{ + LL_PROFILE_ZONE_SCOPED; + + // The RT stack should be empty when we get here. + llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); + + // Apply a pending swap interval on our own context. + const S32 pending_interval = mPendingSwapInterval.exchange(-1, std::memory_order_relaxed); + if (pending_interval >= 0 && mWindow) + { + mWindow->setSwapInterval(pending_interval); + } + + // Apply a pending refresh-overlay toggle here, where our GL context + // is current for the squares' context creation/teardown. + const S32 pending_show = mPendingShowRefresh.exchange(-1, std::memory_order_relaxed); + if (pending_show == 1) + { + createRefreshOverlay(); + } + else if (pending_show == 0) + { + destroyRefreshOverlay(); + } + + // Draw every layer's front texture as a quad straight into the default + // framebuffer, bottom first. Binding the texture for sampling is also + // what pulls the producer context's writes into this one. + // + // The blit shader lives in LLViewerShaderMgr (compositorblitV/F.glsl). + // Until it's handed to us and compiled, just present - nothing to + // draw with yet. + const bool shader_ready = mBlitShader && mBlitShader->mProgramObject; + + const GLint dst_w = (GLint)mSwapChain.getWidth(); + const GLint dst_h = (GLint)mSwapChain.getHeight(); + + const F64 present_start = LLTimer::getTotalSeconds(); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + LLRenderTarget::sCurFBO = 0; + glViewport(0, 0, dst_w, dst_h); + glDrawBuffer(GL_BACK); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + if (shader_ready) + { + mBlitShader->bind(); + } + + for (LLCompositable* c : mCompositables) + { + if (!shader_ready) + { + break; + } + // Mailbox claim for queue-paced layers. + c->tryAcquireNewFront(); + + LLRenderTarget& front = c->frontBuffer(); + + // Don't hold an outer lease while calling accessors that take + // their own - leases don't nest. Each call below takes its own; + // only the texture guard's lease spans the draw. + if (!front.isComplete()) + { + continue; // layer not allocated yet (early init) + } + + S32 dst_x = 0, dst_y = 0; + c->compositeOffset(dst_x, dst_y); + const GLint w = (GLint)front.getWidth(); + const GLint h = (GLint)front.getHeight(); + + // Textures are shared between contexts, FBOs aren't. The guard + // holds the RT's shared lease across the draw and fences. + LLScopedTexName src_tex_guard = front.getTexture(0); + const U32 src_tex = src_tex_guard.get(); + llassert(src_tex != 0); + + // Wait on the producer's fence so we only sample finished pixels. + // No-op if the RT didn't opt in to cross-context sync. + front.waitFrameCompleteFence(); + + // Layer rect in NDC; GL origin bottom-left matches the + // compositeOffset convention. + const F32 x0 = 2.f * (F32)dst_x / (F32)dst_w - 1.f; + const F32 y0 = 2.f * (F32)dst_y / (F32)dst_h - 1.f; + const F32 x1 = 2.f * (F32)(dst_x + w) / (F32)dst_w - 1.f; + const F32 y1 = 2.f * (F32)(dst_y + h) / (F32)dst_h - 1.f; + + // Bind and draw the layer quad. + static const LLStaticHashedString sBlitRect("blit_rect"); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, src_tex); + mBlitShader->uniform4f(sBlitRect, x0, y0, x1, y1); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); + + // Reverse fence: the producer waits on this before writing into + // the buffer again. + front.placeReadCompleteFence(); + } + + if (shader_ready) + { + mBlitShader->unbind(); + } + + // The layers were drawn straight into FBO 0; just swap. + mSwapChain.presentDirect(); + + const F64 present_end = LLTimer::getTotalSeconds(); + mLastPresentMs.store( + (F32)((present_end - present_start) * 1000.0), + std::memory_order_relaxed); + + // Present rate over a 1s window. + ++mPresentCount; + if (mPresentWindowStart == 0.0) + { + mPresentWindowStart = present_end; + mPresentCount = 0; + } + else if (present_end - mPresentWindowStart >= 1.0) + { + mPresentFps.store((F32)(mPresentCount / (present_end - mPresentWindowStart)), + std::memory_order_relaxed); + mPresentCount = 0; + mPresentWindowStart = present_end; + } + + // Let subscribers pace themselves to our clock. + ++mFrameIndex; + mOnSync(mFrameIndex); +} diff --git a/indra/llrender/llcompositor.h b/indra/llrender/llcompositor.h new file mode 100644 index 00000000000..c1c4d904cd9 --- /dev/null +++ b/indra/llrender/llcompositor.h @@ -0,0 +1,163 @@ +/** + * @file llcompositor.h + * @brief Compositor that owns the swap chain and presents an ordered list + * of LLCompositables. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLCOMPOSITOR_H +#define LL_LLCOMPOSITOR_H + +#include "llswapchain.h" +#include "llcompositable.h" + +#include + +#include +#include +#include +#include +#include + +class LLWindow; +class LLTestSquareCompositable; +class LLGLSLShader; + +// Owns the swap chain and an ordered list of LLCompositables. Each frame we +// draw every layer's front buffer into the window and present. +// +// Runs on the OS main thread (driven from LLAppViewer::doFrame) while the +// viewer renders on its own thread. Compositables never block our presents. +class LLCompositor +{ +public: + // Defined out of line - the unique_ptr member needs the complete type. + LLCompositor(); + ~LLCompositor(); + + LLCompositor(const LLCompositor&) = delete; + LLCompositor& operator=(const LLCompositor&) = delete; + + // Stand up the swap chain. Called once GL is up. + void attachToWindow(LLWindow* window, U32 width, U32 height); + + // Propagate window resize to the swap chain. + void resize(U32 width, U32 height); + + // Tear everything down. Called before window destruction. + void release(); + + bool isInitialized() const { return mSwapChain.getImageCount() > 0; } + + // Add a compositable. Lower-index layers draw first (bottom). Order is + // fixed at registration; no re-ordering yet. + void addCompositable(LLCompositable* c); + + // Remove a previously added compositable. + void removeCompositable(LLCompositable* c); + + // Show/hide the bring-up refresh overlay (the colored sync-rate + // squares). Callable from any thread - the squares own GL contexts, + // so they're created/destroyed on our own thread at the next + // present. + void setShowRefreshOverlay(bool show) { mPendingShowRefresh.store(show ? 1 : 0, std::memory_order_relaxed); } + + // Composite and present one frame: draw every layer's front buffer in + // registration order (bottom first), present, fire the sync signal. + // We never wait on producers - a layer with no new content just gets + // its last front re-drawn. + void presentFrame(); + + // Present every Nth vblank (1 = every vblank). The driver does the + // pacing and the sync signal inherits the divided cadence. Callable + // from any thread - applied on our own context at the next present. + void setSwapInterval(S32 interval) { mPendingSwapInterval.store(interval, std::memory_order_relaxed); } + + // Fired at the end of presentFrame() with the frame index. Compositables + // subscribe to this to pace themselves to our clock. + typedef boost::signals2::signal sync_signal_t; + boost::signals2::connection onSync(const sync_signal_t::slot_type& cb) + { + return mOnSync.connect(cb); + } + + // Read access for resize handling and similar plumbing. Presenting + // stays in here. + const LLSwapChain& getSwapChain() const { return mSwapChain; } + + // The blit shader we composite with (compositorblitV/F.glsl, owned + // and compiled by LLViewerShaderMgr). Until it's set and compiled + // we present without drawing any layers. + void setBlitShader(LLGLSLShader* shader) { mBlitShader = shader; } + + // Debug overlay stats (Show Render Info). Per-layer numbers come + // straight from the producer's own metrics - we do no bookkeeping + // on this side. + struct LayerStatsSnapshot + { + std::string name; + F32 frameMs; // producer's last full frame cycle, ms + F32 fps; // produced frames per second (1000/frameMs) + }; + + // Wall-clock CPU time of the most recent presentFrame, ms. + F32 getLastPresentMs() const { return mLastPresentMs.load(std::memory_order_relaxed); } + + // Presents per second over a 1s window. + F32 getPresentFps() const { return mPresentFps.load(std::memory_order_relaxed); } + + // Snapshot of per-layer stats in composition order. + void getLayerStats(std::vector& out) const; + +private: + LLSwapChain mSwapChain; + LLWindow* mWindow = nullptr; + std::vector mCompositables; + sync_signal_t mOnSync; + U64 mFrameIndex = 0; + std::atomic mPendingSwapInterval{-1}; // applied at next present; -1 = none + + // Bring-up refresh overlay: colored squares that repaint at + // different sync intervals, behind RenderCompositorShowRefresh. + // Created/torn down on our own thread (they own GL contexts) when + // the pending flag flips. + void createRefreshOverlay(); + void destroyRefreshOverlay(); + std::vector> mTestSquares; + std::atomic mPendingShowRefresh{-1}; // applied at next present; -1 = no change + bool mRefreshShown = false; + + // Guards the list structure only - registration vs the stats overlay's + // iteration on the viewer thread. The present loop runs on the same + // thread as all mutations, so it reads lock-free. + mutable std::mutex mCompositablesMutex; + std::atomic mLastPresentMs{0.f}; + std::atomic mPresentFps{0.f}; + U32 mPresentCount = 0; // compositor-thread-only + F64 mPresentWindowStart = 0.0; // compositor-thread-only + + // Set by the viewer once LLViewerShaderMgr has compiled it. + LLGLSLShader* mBlitShader = nullptr; +}; + +#endif // LL_LLCOMPOSITOR_H diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 4584ed1d865..d5772840cec 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2375,12 +2375,13 @@ void clear_glerror() // LLGLState // -// Static members -std::unordered_map LLGLState::sStateMap; +// Static members. These mirror per-context GL state, and each of our +// rendering threads owns its own context, so they're thread_local. +thread_local std::unordered_map LLGLState::sStateMap; -GLboolean LLGLDepthTest::sDepthEnabled = GL_FALSE; // OpenGL default -GLenum LLGLDepthTest::sDepthFunc = GL_LESS; // OpenGL default -GLboolean LLGLDepthTest::sWriteEnabled = GL_TRUE; // OpenGL default +thread_local GLboolean LLGLDepthTest::sDepthEnabled = GL_FALSE; // OpenGL default +thread_local GLenum LLGLDepthTest::sDepthFunc = GL_LESS; // OpenGL default +thread_local GLboolean LLGLDepthTest::sWriteEnabled = GL_TRUE; // OpenGL default //static void LLGLState::initClass() diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index e1ab2a49e6d..fbed31db6d3 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -246,7 +246,9 @@ class LLGLState static void checkStates(GLboolean writeAlpha = GL_TRUE); protected: - static std::unordered_map sStateMap; + // CPU-side mirror of glEnable/glDisable state. GL state is per-context + // and each thread owns its own context, so this is thread_local. + static thread_local std::unordered_map sStateMap; public: enum { CURRENT_STATE = -2, DISABLED_STATE = 0, ENABLED_STATE = 1 }; diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index 0f2c2959fae..9f9b8832f3e 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -51,8 +51,8 @@ using std::pair; using std::make_pair; using std::string; -GLuint LLGLSLShader::sCurBoundShader = 0; -LLGLSLShader* LLGLSLShader::sCurBoundShaderPtr = NULL; +thread_local GLuint LLGLSLShader::sCurBoundShader = 0; +thread_local LLGLSLShader* LLGLSLShader::sCurBoundShaderPtr = NULL; S32 LLGLSLShader::sIndexedTextureChannels = 0; U32 LLGLSLShader::sMaxGLTFMaterials = 0; U32 LLGLSLShader::sMaxGLTFNodes = 0; diff --git a/indra/llrender/llglslshader.h b/indra/llrender/llglslshader.h index 272a99aaa58..d8c46f2ec3a 100644 --- a/indra/llrender/llglslshader.h +++ b/indra/llrender/llglslshader.h @@ -165,8 +165,9 @@ class LLGLSLShader LLGLSLShader(); ~LLGLSLShader(); - static GLuint sCurBoundShader; - static LLGLSLShader* sCurBoundShaderPtr; + // Current-program mirror - per-GL-context, so thread_local. + static thread_local GLuint sCurBoundShader; + static thread_local LLGLSLShader* sCurBoundShaderPtr; static S32 sIndexedTextureChannels; static U32 sMaxGLTFMaterials; diff --git a/indra/llrender/llglstates.h b/indra/llrender/llglstates.h index 9bb980d7ad3..c5ff7f40bbf 100644 --- a/indra/llrender/llglstates.h +++ b/indra/llrender/llglstates.h @@ -46,9 +46,10 @@ class LLGLDepthTest GLenum mPrevDepthFunc; GLboolean mPrevWriteEnabled; private: - static GLboolean sDepthEnabled; // defaults to GL_FALSE - static GLenum sDepthFunc; // defaults to GL_LESS - static GLboolean sWriteEnabled; // defaults to GL_TRUE + // Per-context GL state mirrors, so thread_local. + static thread_local GLboolean sDepthEnabled; // defaults to GL_FALSE + static thread_local GLenum sDepthFunc; // defaults to GL_LESS + static thread_local GLboolean sWriteEnabled; // defaults to GL_TRUE }; //---------------------------------------------------------------------------- diff --git a/indra/llrender/llgpuresource.cpp b/indra/llrender/llgpuresource.cpp index b69a089d728..3b8f9567daa 100644 --- a/indra/llrender/llgpuresource.cpp +++ b/indra/llrender/llgpuresource.cpp @@ -35,14 +35,18 @@ LLGPUResource::~LLGPUResource() { return; } - // Verify no leases are live. If a holder outlives the resource, the - // destructor proceeds through dangling state -- UB. - // try_lock returns true iff no shared or exclusive lock is held. - llassert(mLeaseMutex->try_lock()); - mLeaseMutex->unlock(); + // Verify no leases are live. + if (mLeaseMutex->try_lock()) + { + mLeaseMutex->unlock(); + } + else + { + llassert(false); // a lease holder outlived this resource + } } -// -- LLSharedLease ---------------------------------------------------------- +// - LLSharedLease ---------------------------------------------------------- LLSharedLease::LLSharedLease(LLGPUResource* res) : mResource(res) @@ -86,7 +90,7 @@ void LLSharedLease::release() } } -// -- LLUniqueLease ---------------------------------------------------------- +// - LLUniqueLease ---------------------------------------------------------- LLUniqueLease::LLUniqueLease(LLGPUResource* res) : mResource(res) diff --git a/indra/llrender/llgpuresource.h b/indra/llrender/llgpuresource.h index 5f717c28cb0..31b7ae64252 100644 --- a/indra/llrender/llgpuresource.h +++ b/indra/llrender/llgpuresource.h @@ -35,19 +35,13 @@ // Base class for GPU resources that need cross-thread synchronization. // -// Provides two kinds of RAII lease: -// - shared (many concurrent holders, read-only access) -// - unique (exclusive, for mutation) +// Provides two kinds of RAII lease: shared (many readers) and unique (one +// writer). Subclasses can override the acquire/release hooks to place GL +// fences at the lease boundary; the defaults do nothing, so resources that +// don't need cross-context fences pay nothing. // -// Backed by std::shared_mutex. Subclasses override the on*Acquire/Release -// hooks to inject GL fences at the lease boundary when GPU-side ordering -// needs to follow CPU-side serialization -- e.g. a worker context's writes -// must be GPU-visible before a different context reads. Defaults are no-op, -// so resources that don't need cross-context fences pay nothing. -// -// Leases follow std::shared_mutex semantics: non-recursive, same-thread -// release. A thread that holds a lease must not acquire another lease on -// the same resource (deadlock). +// Leases follow std::shared_mutex rules: non-recursive, so don't take a +// second lease on the same resource while you already hold one. class LLGPUResource { public: @@ -57,11 +51,9 @@ class LLGPUResource LLGPUResource(const LLGPUResource&) = delete; LLGPUResource& operator=(const LLGPUResource&) = delete; - // Movable: the mutex hides behind a unique_ptr so the default move - // ctor works. After a move the source's mLeaseMutex is null -- further - // lease ops on it are UB. Only safe to move when no leases are live; - // in practice that's container setup time, never mid-render. Needed so - // vector et al. can reallocate. + // Movable so containers of these can reallocate. Only move when no + // leases are live; a moved-from resource has a null mutex and can't + // be leased again. LLGPUResource(LLGPUResource&& other) noexcept = default; LLGPUResource& operator=(LLGPUResource&&) = delete; @@ -69,9 +61,8 @@ class LLGPUResource LLUniqueLease getUniqueLease() { return LLUniqueLease(this); } protected: - // Hooks called by the lease lifecycle. Defaults are no-op. Subclasses - // override the ones they care about. Called with the appropriate lock - // held (shared lock for shared hooks, exclusive lock for unique hooks). + // Lease lifecycle hooks, called with the lock held. Override the ones + // you care about; the defaults do nothing. virtual void onSharedAcquire() {} virtual void onSharedRelease() {} virtual void onUniqueAcquire() {} @@ -83,18 +74,15 @@ class LLGPUResource mutable std::unique_ptr mLeaseMutex; }; -// Guarded GL texture name. Returned by accessors that would otherwise be -// snapshot-return -- the lease lives in this guard, keyed to the caller's -// scope. Use .get() to read the value; the guard's lifetime is what holds -// the lock. +// Guarded GL texture name - the guard holds a shared lease for as long as +// it lives. Use .get() to read the value. // -// Common safe patterns: -// glBindTexture(target, img->getTexName().get()); // expression scope -// auto name = img->getTexName(); // statement scope -// ... use name.get() ... +// Safe: +// glBindTexture(target, img->getTexName().get()); +// auto name = img->getTexName(); ... name.get() ... // -// Unsafe (don't): assigning .get() into an LLGLuint local -- the guard temp -// destructs at the ;, leaving you using the value without the lock. +// Unsafe: stashing .get() into a plain LLGLuint - the guard temp dies at +// the semicolon and you're using the value without the lock. class LLScopedTexName { public: diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index ecd4f1d30f5..d8455dc89da 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -43,7 +43,7 @@ #include "llframetimer.h" #include -extern LL_COMMON_API bool on_main_thread(); +extern LL_COMMON_API bool on_viewer_thread(); #if !LL_IMAGEGL_THREAD_CHECK #define checkActiveThread() @@ -579,6 +579,8 @@ void LLImageGL::init(bool usemipmaps, bool allow_compression) mCategory = -1; // Sometimes we have to post work for the main thread. + // The contexts share textures, so it doesn't matter for correctness + // which thread the work actually lands on. mMainQueue = LL::WorkQueue::getInstance("mainloop"); } @@ -1058,11 +1060,11 @@ U32 type_width_from_pixtype(U32 pixtype) bool should_stagger_image_set(bool compressed) { #if LL_DARWIN - return !compressed && on_main_thread() && gGLManager.mIsAMD; + return !compressed && on_viewer_thread() && gGLManager.mIsAMD; #else // glTexSubImage2D doesn't work with compressed textures on select tested Nvidia GPUs on Windows 10 -Cosmic,2023-03-08 // Setting media textures off-thread seems faster when not using sub_image_lines (Nvidia/Windows 10) -Cosmic,2023-03-31 - return !compressed && on_main_thread() && !gGLManager.mIsIntel; + return !compressed && on_viewer_thread() && !gGLManager.mIsIntel; #endif } @@ -1635,7 +1637,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ LL_PROFILE_GPU_ZONE("createGLTexture"); checkActiveThread(); - bool main_thread = on_main_thread(); + bool main_thread = on_viewer_thread(); if (defer_copy) { @@ -1723,8 +1725,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ } else { - // syncTexName takes the unique lease, deletes the prior name - // if different, and installs new_texname. + // syncTexName deletes the old name and installs the new one. syncTexName(new_texname); } } @@ -1742,10 +1743,10 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) { LL_PROFILE_ZONE_SCOPED; - llassert(!on_main_thread()); + llassert(!on_viewer_thread()); - // Cross-context handoff via lease: the release fires onUniqueRelease - // which places the fence the main-thread consumer will wait on. + // Take and drop a unique lease - the release hook places the fence + // the consumer will wait on. { LL_PROFILE_ZONE_NAMED("cglt - sync"); LLUniqueLease lease = getUniqueLease(); @@ -1758,8 +1759,8 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) [=, this]() { LL_PROFILE_ZONE_NAMED("cglt - delete callback"); - // syncTexName takes the unique lease; its acquire-side hook - // server-side-waits on the fence the worker placed above. + // syncTexName takes the unique lease, which waits on the + // fence we placed above. syncTexName(new_tex_name); unref(); }); @@ -1771,14 +1772,14 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) void LLImageGL::onUniqueRelease() { - if (on_main_thread()) + if (on_viewer_thread()) { // No cross-context handoff on main thread. return; } - // Replace any prior fence. Safe to delete under unique lock -- shared - // and unique are mutually exclusive, so no waiter is mid-wait. + // Replace any prior fence. We hold the unique lock, so nothing can + // be mid-wait on the old one. if (mPendingFence != nullptr) { glDeleteSync(mPendingFence); @@ -1793,8 +1794,8 @@ void LLImageGL::onUniqueRelease() void LLImageGL::onSharedAcquire() { - // Concurrent shared holders all wait on the same fence -- fine, glWaitSync - // is idempotent. The next unique holder deletes (see mPendingFence doc). + // Multiple shared holders can wait on the same fence - that's fine. + // The next unique holder cleans it up. if (mPendingFence != nullptr && mPendingFenceThread.load(std::memory_order_acquire) != std::this_thread::get_id()) { @@ -1809,7 +1810,7 @@ void LLImageGL::onUniqueAcquire() { glWaitSync(mPendingFence, 0, GL_TIMEOUT_IGNORED); } - // Exclusive -- drop the fence so the next release can place a fresh one. + // Exclusive - drop the fence so the next release can place a fresh one. if (mPendingFence != nullptr) { glDeleteSync(mPendingFence); @@ -1820,8 +1821,8 @@ void LLImageGL::onUniqueAcquire() void LLImageGL::syncTexName(LLGLuint texname) { - // Mutates mTexName -- unique lease. Acquire waits on any pending - // cross-thread fence before we touch it. + // We're changing mTexName, so take the unique lease. Acquiring also + // waits on any pending fence. LLUniqueLease lease = getUniqueLease(); if (texname != 0) { diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index c428da2c55b..2c5b4c4f66f 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -117,10 +117,9 @@ class LLImageGL : public LLRefCount, public LLGPUResource void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); - // LLGPUResource hooks: place a GL fence when an off-main thread - // releases a unique lease, and server-side-wait on that fence when a - // different thread next acquires either kind of lease. Same-thread - // acquires after a same-thread release pay nothing. + // LLGPUResource hooks. Releasing a unique lease off-thread places a GL + // fence; the next acquire from a different thread waits on it. + // Same-thread acquires pay nothing. void onUniqueRelease() override; void onSharedAcquire() override; void onUniqueAcquire() override; @@ -180,8 +179,8 @@ class LLImageGL : public LLRefCount, public LLGPUResource LLGLenum getFormatType() const { return mFormatType; } bool getHasGLTexture() const; - // Returns a guard that owns a shared lease for the caller's scope. See - // LLScopedTexName doc for the safe / unsafe usage patterns. + // Returns a guard that holds a shared lease for the caller's scope. + // See LLScopedTexName for usage. LLScopedTexName getTexName() const; bool getIsAlphaMask() const; @@ -264,11 +263,9 @@ class LLImageGL : public LLRefCount, public LLGPUResource U16 mHeight; S8 mCurrentDiscardLevel; - // Cross-context fence from the last off-thread unique release. Next - // cross-thread acquire glWaitSyncs on it. Only the unique lock writes - // or deletes the GLsync -- shared waiters just read. The mutex orders - // shared waiters and the unique deleter so they never overlap. - // - Geenz 2026-06-09 + // Fence from the last off-thread unique release; the next cross-thread + // acquire waits on it. Only the unique lock writes or deletes it - + // shared waiters just read. GLsync mPendingFence = nullptr; std::atomic mPendingFenceThread; diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 9058a817e31..2dbef9b1956 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -221,9 +221,7 @@ void LLTexUnit::bindFast(LLTexture* texture) { LL_PROFILE_ZONE_NAMED("MISSING TEXTURE"); //if deleted, will re-generate it immediately - // No lease here -- re-entrant calls take their own unique. Trailing - // glBindTexture uses whatever mCurrTexture bindDefaultImage left, - // same as pre-lease. + // No lease here - the calls below take their own. texture->forceImmediateUpdate(); gl_tex->forceUpdateBindStats(); texture->bindDefaultImage(mIndex); @@ -253,8 +251,8 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind) bool needs_default = false; bool did_bind = false; { - // Guard scoped to texname read + glBindTexture only. - // Drop before updateBindStats (it takes its own shared lease). + // Scope the guard to the bind only - updateBindStats takes + // its own lease, and leases don't nest. auto guard = gl_tex->getTexName(); U32 texname = guard.get(); if (texname) @@ -326,8 +324,8 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 U32 texname = 0; bool did_bind = false; { - // Guard scoped to texname read + glBindTexture only. Internal - // updateBindStats below takes its own shared lease -- can't nest. + // Scope the guard to the bind only - updateBindStats below takes + // its own lease, and leases don't nest. auto guard = texture->getTexName(); texname = usename ? usename : guard.get(); diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 65dd8fb72db..1db3bae7043 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -30,7 +30,7 @@ #include "llrender.h" #include "llgl.h" -LLRenderTarget* LLRenderTarget::sBoundTarget = NULL; +thread_local LLRenderTarget* LLRenderTarget::sBoundTarget = NULL; U32 LLRenderTarget::sBytesAllocated = 0; void check_framebuffer_status() @@ -51,14 +51,14 @@ void check_framebuffer_status() } bool LLRenderTarget::sUseFBO = false; -U32 LLRenderTarget::sCurFBO = 0; -bool LLRenderTarget::sFlushRequiresParent = false; +thread_local U32 LLRenderTarget::sCurFBO = 0; +thread_local bool LLRenderTarget::sFlushRequiresParent = false; extern S32 gGLViewport[4]; -U32 LLRenderTarget::sCurResX = 0; -U32 LLRenderTarget::sCurResY = 0; +thread_local U32 LLRenderTarget::sCurResX = 0; +thread_local U32 LLRenderTarget::sCurResY = 0; LLRenderTarget::LLRenderTarget() : mResX(0), @@ -76,7 +76,7 @@ LLRenderTarget::~LLRenderTarget() } // --------------------------------------------------------------------------- -// Public mutators -- each takes a unique lease and delegates to a _locked +// Public mutators - each takes a unique lease and delegates to a _locked // helper if it needs to call into another mutator. // --------------------------------------------------------------------------- @@ -135,7 +135,7 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT if (depth) { - // Inlined allocateDepth body -- still under our unique lease. + // Inlined allocateDepth body - still under our unique lease. LLImageGL::generateTextures(1, &mDepth); gGL.getTexUnit(0)->bindManual(mUsage, mDepth); U32 internal_type = LLTexUnit::getInternalType(mUsage); @@ -185,7 +185,7 @@ void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) mResY = img->getHeight(); mUsage = img->getTarget(); - // Guarded read on img (different LLGPUResource, different mutex -- safe). + // Guarded read on img (different LLGPUResource, different mutex - safe). LLScopedTexName guard; if (use_name == 0) { @@ -346,8 +346,8 @@ void LLRenderTarget::shareDepthBuffer(LLRenderTarget& target) { llassert(!isBoundInStack()); - // Two-resource lock: acquire this then target -- consistent order across - // all callers prevents lock-order inversion. (Only one such method.) + // Lock this first, then target - keep the order consistent so we + // can't deadlock. LLUniqueLease lease_this = getUniqueLease(); LLUniqueLease lease_other = target.getUniqueLease(); @@ -389,10 +389,75 @@ void LLRenderTarget::release() release_locked(); } +namespace +{ + // Wrap a fresh fence in a shared_ptr that deletes it when the last + // reference drops. Sync objects belong to the share group, so either + // context can do the delete. + std::shared_ptr make_fence() + { + return std::shared_ptr( + glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), + [](void* sync) { if (sync) glDeleteSync((GLsync)sync); }); + } +} + +void LLRenderTarget::placeFrameCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence = make_fence(); + std::lock_guard lock(mCrossSync->fenceMutex); + mCrossSync->frameCompleteFence = std::move(fence); +} + +void LLRenderTarget::waitFrameCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence; + { + std::lock_guard lock(mCrossSync->fenceMutex); + fence = mCrossSync->frameCompleteFence; + } + if (fence) + { + // Server-side wait so we don't stall the CPU. Holding the + // shared_ptr keeps the sync alive even if the producer swaps in + // a new one mid-wait. + glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); + } +} + +void LLRenderTarget::placeReadCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence = make_fence(); + // Flush so the other context can actually see the fence. + glFlush(); + std::lock_guard lock(mCrossSync->fenceMutex); + mCrossSync->readCompleteFence = std::move(fence); +} + +void LLRenderTarget::waitReadCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence; + { + std::lock_guard lock(mCrossSync->fenceMutex); + fence = mCrossSync->readCompleteFence; + } + if (fence) + { + glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); + } +} + void LLRenderTarget::release_locked() { mIsSwapChainImage = false; + // Drops the fences; glDeleteSync runs when the last holder lets go. + mCrossSync.reset(); + if (mDepth) { LLImageGL::deleteTextures(1, &mDepth); @@ -561,9 +626,8 @@ void LLRenderTarget::bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilt void LLRenderTarget::bindTexture_locked(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options) { - // Read mTex[index] directly under the caller's lease -- avoids the - // same-thread re-entry that calling getTexture() (also takes shared) - // would cause. + // Read mTex directly - calling getTexture() here would re-enter the + // lease we already hold. LLGLuint name = (index < mTex.size()) ? mTex[index] : 0; gGL.getTexUnit(channel)->bindManual(mUsage, name, filter_options == LLTexUnit::TFO_TRILINEAR || filter_options == LLTexUnit::TFO_ANISOTROPIC); gGL.getTexUnit(channel)->setTextureFilteringOption(filter_options); @@ -602,7 +666,7 @@ void LLRenderTarget::flush_locked() if (mIsSwapChainImage) { // Bottom of the stack for a render batch. Just pop the bookkeeping - // and leave the FBO bound -- LLSwapChain::present() will rebind FBO 0 + // and leave the FBO bound - LLSwapChain::present() will rebind FBO 0 // for the blit, and the next acquireNextImage() will set up the next // image's bind. sBoundTarget = nullptr; @@ -610,7 +674,7 @@ void LLRenderTarget::flush_locked() } // No parent on the stack and not a swap chain image. While the swap chain - // is attached (steady-state rendering) this is a missed bind site -- + // is attached (steady-state rendering) this is a missed bind site - // assert so the call site can be wrapped. Before the chain attaches or // after release (feature-manager GPU benchmark, late shutdown cleanup) // fall back to the OS default framebuffer the way the old code did. @@ -637,7 +701,7 @@ void LLRenderTarget::bindForRead() void LLRenderTarget::unbindRead() { - // Doesn't read instance state -- no lease needed. + // Doesn't read instance state - no lease needed. glBindFramebuffer(GL_READ_FRAMEBUFFER, sCurFBO); glReadBuffer(sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); } @@ -659,10 +723,8 @@ void LLRenderTarget::getViewport(S32* viewport) bool LLRenderTarget::isBoundInStack() const { - // No lease: walks the static sBoundTarget chain through other RTs' - // mPreviousRT fields. Locking other RTs from here invites deadlock and - // protects nothing useful; this is a debug-style probe whose racy reads - // are acceptable. Same risk as before the lease refactor. + // No lease here - this walks other RTs' fields, and locking them from + // here just invites deadlock. It's a debug probe; racy reads are fine. LLRenderTarget* cur = sBoundTarget; while (cur && cur != this) { @@ -702,7 +764,7 @@ void LLRenderTarget::swapFBORefs(LLRenderTarget& other) } // --------------------------------------------------------------------------- -// Self-contained scalar readers -- internal SHARED leases. +// Simple scalar readers - each takes its own shared lease. // --------------------------------------------------------------------------- U32 LLRenderTarget::getWidth() const @@ -731,8 +793,8 @@ U32 LLRenderTarget::getDepth() const U32 LLRenderTarget::getFBO() const { - // Snapshot-return -- caller is responsible for holding a SHARED lease - // externally if the value's use spans potential mutations on this RT. + // Just a snapshot - callers that need the value to stay valid should + // hold a shared lease themselves. return mFBO; } diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index 2802531645e..afded7ea7a9 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -33,6 +33,9 @@ #include "llrender.h" #include "llgpuresource.h" +#include +#include + /* Wrapper around OpenGL framebuffer objects for use in render-to-texture @@ -59,36 +62,38 @@ */ -// Inherits LLGPUResource: every public mutator takes an internal UNIQUE lease, -// every self-contained reader takes an internal SHARED lease. Snapshot-return -// accessors (getTexture, getFBO) -- see their docs. +// Inherits LLGPUResource: mutators take a unique lease internally, readers +// a shared one. getTexture and getFBO are a bit different - see their docs. class LLRenderTarget : public LLGPUResource { public: // Whether or not to use FBO implementation static bool sUseFBO; static U32 sBytesAllocated; - static U32 sCurFBO; - static U32 sCurResX; - static U32 sCurResY; - - // When true, flush() requires a parent on the RT stack -- i.e. the viewer - // has reached steady-state rendering with the swap chain attached. When - // false (early init before swap chain attach, or after shutdown release), - // flush() falls back to the OS default framebuffer so pre-attach code - // paths (feature-manager GPU benchmark, etc.) keep working. Toggled by - // LLSwapChain attach/release. + // Per-context GL state - each thread owns its own context, so the + // FBO binding stack can't be shared across threads. + static thread_local U32 sCurFBO; + static thread_local U32 sCurResX; + static thread_local U32 sCurResY; + + // When true, flush() requires a parent on the RT stack - i.e. this + // context is mid-world-render and a top-level parentless flush is a + // missed bind site. When false (early init, standalone GPU passes + // like avatar profiling that run outside the world render, or after + // shutdown release), flush() falls back to the OS default + // framebuffer. Per-context (thread_local): the viewer thread scopes + // it to renderViewerFrame, the compositor's swap chain latches it on + // its own context. A plain global would let one context's render + // scope wrongly gate the other's flushes. // - Geenz 2026-06-08 - static bool sFlushRequiresParent; + static thread_local bool sFlushRequiresParent; LLRenderTarget(); ~LLRenderTarget(); - // Movable so std::vector can reallocate. The - // user-declared destructor would otherwise suppress implicit move - // generation, falling back to copy (deleted via LLGPUResource). - // Only safe to move when no leases are held; see LLGPUResource doc. + // Movable so std::vector can reallocate. Only safe to + // move when no leases are held - see LLGPUResource. LLRenderTarget(LLRenderTarget&&) noexcept = default; LLRenderTarget(const LLRenderTarget&) = delete; LLRenderTarget& operator=(const LLRenderTarget&) = delete; @@ -192,15 +197,43 @@ class LLRenderTarget : public LLGPUResource U32 getDepth() const; - // Underlying GL framebuffer object name. Exposed for LLSwapChain's - // present-time blit; viewer code should bind through bindTarget / - // bindForRead instead of touching this directly. Snapshot-return -- - // caller is responsible for holding a SHARED lease on this RT for the - // value's use scope. + // Underlying GL framebuffer object name. FBOs aren't shared between + // contexts (the attached textures are), so this is only meaningful on + // the thread that allocated it. Just a snapshot - hold a shared lease + // yourself while you use the value. U32 getFBO() const; void bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options = LLTexUnit::TFO_BILINEAR); + // Cross-context GPU sync. Opt in for RTs that another GL context reads + // from; everything else carries no sync state at all. + void setNeedsCrossContextSync(bool b) + { + if (b && !mCrossSync) + { + mCrossSync = std::make_shared(); + } + else if (!b) + { + mCrossSync.reset(); + } + } + bool needsCrossContextSync() const { return (bool)mCrossSync; } + + // The writer places this fence when it finishes a frame; the reader + // waits on it before sampling so it only sees complete pixels. + void placeFrameCompleteFence(); + + // Server-side wait on the writer's fence. No-op if there's no fence + // or sync is disabled. + void waitFrameCompleteFence(); + + // Reverse direction: the reader places this after sampling, and the + // writer waits on it before rendering into the RT again so the two + // don't race on the GPU. + void placeReadCompleteFence(); + void waitReadCompleteFence(); + //flush rendering operations //must be called when rendering is complete //should be used 1:1 with bindTarget @@ -222,7 +255,9 @@ class LLRenderTarget : public LLGPUResource // *HACK void swapFBORefs(LLRenderTarget& other); - static LLRenderTarget* sBoundTarget; + // Per-GL-context state - thread_local for the same reasons as + // sCurFBO above. + static thread_local LLRenderTarget* sBoundTarget; protected: U32 mResX; @@ -247,16 +282,30 @@ class LLRenderTarget : public LLGPUResource // into its images. Generalizes sub-rect viewports for any RT and maps // naturally to Vk/XR state. Held off today because gGLViewport changes // more often than window resize (per-frame setup3DViewport, UI scale, - // sidebar) -- syncing it everywhere is intrusive. Worth taking on as + // sidebar) - syncing it everywhere is intrusive. Worth taking on as // part of the broader render-state cleanup for Vulkan port prep. bool mIsSwapChainImage = false; + // Cross-context GPU sync state. Only allocated for RTs that opt in - + // a null mCrossSync means no sync. frameComplete is placed by the + // producer and waited on by the compositor; readComplete goes the + // other way. + // + // Fences are shared_ptr with glDeleteSync as the deleter, so a + // fence can't be deleted out from under a waiter that still holds a + // reference. The mutex only guards the pointer swap, never a GL call. + struct CrossContextSync + { + std::mutex fenceMutex; + std::shared_ptr frameCompleteFence; + std::shared_ptr readCompleteFence; + }; + std::shared_ptr mCrossSync; + private: - // Lockless helpers: do not take a lease. Public wrappers above own the - // lease and call these; internal callers (allocate -> release_locked, - // addColorAttachment debug -> bindTarget_locked / flush_locked, flush mip - // gen -> bindTexture_locked) use them to avoid same-thread re-entry on - // the shared_mutex. + // The _locked helpers don't take a lease - the public wrappers own it. + // Internal callers use these to avoid re-entering the shared_mutex on + // the same thread. void release_locked(); bool addColorAttachment_locked(U32 color_fmt); void bindTarget_locked(); diff --git a/indra/llrender/llresourcelease.h b/indra/llrender/llresourcelease.h index 861fe10be47..b450a8626a8 100644 --- a/indra/llrender/llresourcelease.h +++ b/indra/llrender/llresourcelease.h @@ -29,10 +29,9 @@ class LLGPUResource; -// RAII shared lease: many concurrent holders allowed, blocks while a unique -// lease is held. Non-copyable, movable. Same-thread release (std::shared_mutex -// contract). Non-recursive -- a thread already holding a lease on the same -// resource must not acquire another, including via subroutines. +// RAII shared lease: many holders at once, blocks while a unique lease is +// held. Non-recursive - don't take another lease on the same resource +// while you hold one, including via subroutines. class LLSharedLease { public: diff --git a/indra/llrender/llswapchain.cpp b/indra/llrender/llswapchain.cpp index 0a6de9b7a30..6081d136faf 100644 --- a/indra/llrender/llswapchain.cpp +++ b/indra/llrender/llswapchain.cpp @@ -31,6 +31,7 @@ #include "llgl.h" #include "llwindow.h" + LLSwapChain::~LLSwapChain() { release(); @@ -72,7 +73,7 @@ void LLSwapChain::resize(U32 width, U32 height) { if (width == 0 || height == 0) { - return; // minimized / iconified -- keep existing storage + return; // minimized / iconified - keep existing storage } mWidth = width; @@ -88,8 +89,8 @@ LLRenderTarget& LLSwapChain::acquireNextImage() { llassert(!mImages.empty()); - // Rotate to the next image. GL has no real "acquire" -- the driver owns - // the back buffer rotation under SwapBuffers -- so this is just structural + // Rotate to the next image. GL has no real "acquire" - the driver owns + // the back buffer rotation under SwapBuffers - so this is just structural // cycling. Vk/XR backends will do the real WSI acquire here. mCurrentIndex = (mCurrentIndex + 1) % (U32)mImages.size(); return mImages[mCurrentIndex]; @@ -121,8 +122,7 @@ void LLSwapChain::present() { LL_PROFILE_GPU_ZONE("swapchain present blit"); - // Hold a shared lease on the image across the blit -- getFBO is - // snapshot-return, the value must stay valid for the call. + // Hold a shared lease on the image while we blit from it. LLSharedLease img_lease = img.getSharedLease(); // Save current read FB so we don't disturb anyone else's state. @@ -145,6 +145,12 @@ void LLSwapChain::present() mWindow->swapBuffers(); } +void LLSwapChain::presentDirect() +{ + llassert(mWindow != nullptr); + mWindow->swapBuffers(); +} + void LLSwapChain::release() { // Drop the strict-flush requirement so any late-shutdown top-level diff --git a/indra/llrender/llswapchain.h b/indra/llrender/llswapchain.h index 9f904d4285d..d4e63acc16d 100644 --- a/indra/llrender/llswapchain.h +++ b/indra/llrender/llswapchain.h @@ -41,7 +41,7 @@ class LLWindow; // GL backend (today): the chain holds N off-screen color+depth FBOs sized to // the window. acquireNextImage() rotates the index. present() blits the // current image's color to FBO 0 and calls swapBuffers(). The literal 0 -// only appears inside present() -- viewer code never names it. +// only appears inside present() - viewer code never names it. // // Vulkan / OpenXR backends (future): mImages map onto the runtime's actual // swap chain images; acquireNextImage / present become the real WSI calls @@ -77,7 +77,7 @@ class LLSwapChain // readback consumers that want the in-progress frame. LLRenderTarget& getCurrentImage(); - // Most recently presented image -- i.e. the one whose contents are on + // Most recently presented image - i.e. the one whose contents are on // screen right now. Useful for consumers (scene monitor) that want the // last visible frame rather than the half-rendered current one. LLRenderTarget& getPreviousImage(); @@ -85,6 +85,10 @@ class LLSwapChain // Blit the current image's color to FBO 0 and call window->swapBuffers(). void present(); + // SwapBuffers only - for callers (LLCompositor's shader composite) + // that have already drawn the frame directly into FBO 0. + void presentDirect(); + // Drop image resources and detach. Safe to call redundantly. void release(); diff --git a/indra/llrender/lltestsquarecompositable.cpp b/indra/llrender/lltestsquarecompositable.cpp new file mode 100644 index 00000000000..7f0380adf04 --- /dev/null +++ b/indra/llrender/lltestsquarecompositable.cpp @@ -0,0 +1,178 @@ +/** + * @file lltestsquarecompositable.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "lltestsquarecompositable.h" + +#include "llcompositor.h" +#include "llrender.h" +#include "llwindow.h" + +#include + +LLTestSquareCompositable::LLTestSquareCompositable(const std::string& name, + U8 r, U8 g, U8 b, + U32 interval, S32 x, S32 y, + LLWindow* window, + U32 size) +: LLThread("TestSquare:" + name), + mName(name), + mColor{r, g, b}, + mInterval(interval ? interval : 1), + mX(x), + mY(y), + mSize(size), + mWindow(window), + // Created on the calling thread (OS main, GL context current); + // made current on the producer thread in run(). + mContext(window->createSharedContext()) +{ +} + +LLTestSquareCompositable::~LLTestSquareCompositable() +{ + disconnect(); +} + +void LLTestSquareCompositable::connect(LLCompositor& compositor) +{ + // Runs on the compositor's thread at the end of every present. tryPush + // means a full queue just drops the tick - we never block the + // compositor. + mSyncConnection = compositor.onSync( + [this](U64 frame_index) { mTicks.tryPush(frame_index); }); + + start(); +} + +void LLTestSquareCompositable::disconnect() +{ + mSyncConnection.disconnect(); + if (!isStopped()) + { + setQuitting(); + mTicks.tryPush(kQuitTick); + shutdown(); // joins + } +} + +void LLTestSquareCompositable::run() +{ + // Producer-thread setup: make our context current, bring up gGL, then + // allocate our GL resources. The RT's FBO lives in this context - the + // compositor only ever touches the shared color texture. + mWindow->makeContextCurrent(mContext); + gGL.init(false); + + mRT.allocate(mSize, mSize, GL_RGBA, /*depth=*/false); + mRT.setNeedsCrossContextSync(true); + + // First frame so the layer shows up before the first tick. + paint(); + mRT.placeFrameCompleteFence(); + glFlush(); + produceFrame(); + + while (!isQuitting()) + { + U64 tick = 0; + try + { + tick = mTicks.pop(); // blocks until the next sync tick + } + catch (const LLThreadSafeQueueInterrupt&) + { + break; // queue closed + } + + if (tick == kQuitTick || isQuitting()) + { + break; + } + + if (tick % mInterval == 0) + { + // Don't overwrite pixels the compositor may still be + // copying - GPU-side wait on its read-complete fence. + mRT.waitReadCompleteFence(); + + mStep = (mStep + 3) % mSize; + paint(); + + // Publish: the compositor waits on this fence before sampling. + // Flush so the other context can actually see it. + mRT.placeFrameCompleteFence(); + glFlush(); + produceFrame(); + } + } + + // Tear down on this thread - the context has to be destroyed where + // it's current. + mRT.release(); + gGL.shutdown(); + mWindow->destroySharedContext(mContext); + mContext = nullptr; +} + +void LLTestSquareCompositable::paint() +{ + // Fill the RT's color texture: solid base color with a bright + // vertical bar at the current sweep position (wraps around). + const U8 r = mColor[0]; + const U8 g = mColor[1]; + const U8 b = mColor[2]; + + // Bar pixels lighten toward white. + auto lighten = [](U8 c) { return (U8)(c + ((255 - c) * 3) / 4); }; + const U8 br = lighten(r); + const U8 bg = lighten(g); + const U8 bb = lighten(b); + + const U32 kBarWidth = 12; + + std::vector pixels(mSize * mSize * 4); + for (U32 y = 0; y < mSize; ++y) + { + for (U32 x = 0; x < mSize; ++x) + { + // Distance from the bar start, wrapping at the edge. + const U32 dist = (x + mSize - mStep) % mSize; + const bool in_bar = dist < kBarWidth; + const size_t i = (y * mSize + x) * 4; + pixels[i + 0] = in_bar ? br : r; + pixels[i + 1] = in_bar ? bg : g; + pixels[i + 2] = in_bar ? bb : b; + pixels[i + 3] = 255; + } + } + + LLScopedTexName tex = mRT.getTexture(0); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, tex.get()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)mSize, (GLsizei)mSize, + GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); +} diff --git a/indra/llrender/lltestsquarecompositable.h b/indra/llrender/lltestsquarecompositable.h new file mode 100644 index 00000000000..8c29ddb0df6 --- /dev/null +++ b/indra/llrender/lltestsquarecompositable.h @@ -0,0 +1,104 @@ +/** + * @file lltestsquarecompositable.h + * @brief Threaded solid-color test layer for compositor bring-up. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLTESTSQUARECOMPOSITABLE_H +#define LL_LLTESTSQUARECOMPOSITABLE_H + +#include "llcompositable.h" +#include "llrendertarget.h" +#include "llthread.h" +#include "llthreadsafequeue.h" + +#include + +class LLCompositor; +class LLWindow; + +// Compositor bring-up test layer, and a miniature model of the real +// producer architecture: a dedicated thread with its own shared GL context +// paints a sweeping bar into its own RT and publishes it through the +// cross-context fence pair, paced by the compositor's sync signal +// delivered over a queue. +class LLTestSquareCompositable : public LLCompositable, public LLThread +{ +public: + // color: RGB. interval: repaint every Nth sync. (x, y): destination + // offset in the swap chain image, GL origin. window: source of the + // shared GL context (created in the ctor on the calling thread, + // which must have a current GL context). + LLTestSquareCompositable(const std::string& name, + U8 r, U8 g, U8 b, + U32 interval, S32 x, S32 y, + LLWindow* window, + U32 size = 100); + ~LLTestSquareCompositable() override; + + // Subscribe to the compositor's sync signal and start the producer + // thread. Call on the compositor's thread. + void connect(LLCompositor& compositor); + + // Unsubscribe, wake + join the thread (it releases its GL resources + // and destroys its context on the way out). + void disconnect(); + + // - LLCompositable ----------------------------------------------- + LLRenderTarget& frontBuffer() override { return mRT; } + void compositeOffset(S32& x, S32& y) const override { x = mX; y = mY; } + std::string compositableName() const override { return mName; } + +protected: + // - LLThread ------------------------------------------------------- + // Producer loop: make the shared context current, allocate the RT, + // then consume sync ticks until the quit sentinel arrives. + void run() override; + +private: + // Fill the RT's color texture: solid color + sweep bar at mStep. + void paint(); + + static constexpr U64 kQuitTick = ~U64(0); + + LLRenderTarget mRT; + std::string mName; + U8 mColor[3]; + U32 mInterval; + S32 mX; + S32 mY; + U32 mSize; + U32 mStep = 0; // sweep-bar position; advances per repaint + + LLWindow* mWindow; + void* mContext; + + // Sync ticks from the compositor. Bounded, and the handler uses + // tryPush, so a slow square never blocks the compositor - extra + // ticks just drop. + LLThreadSafeQueue mTicks{64}; + + boost::signals2::connection mSyncConnection; +}; + +#endif // LL_LLTESTSQUARECOMPOSITABLE_H diff --git a/indra/llrender/llvertexbuffer.cpp b/indra/llrender/llvertexbuffer.cpp index d59ddd0fecb..87bcd33ee4a 100644 --- a/indra/llrender/llvertexbuffer.cpp +++ b/indra/llrender/llvertexbuffer.cpp @@ -677,9 +677,9 @@ U64 LLVertexBuffer::getBytesAllocated() //============================================================================ // //static -U32 LLVertexBuffer::sGLRenderBuffer = 0; -U32 LLVertexBuffer::sGLRenderIndices = 0; -U32 LLVertexBuffer::sLastMask = 0; +thread_local U32 LLVertexBuffer::sGLRenderBuffer = 0; +thread_local U32 LLVertexBuffer::sGLRenderIndices = 0; +thread_local U32 LLVertexBuffer::sLastMask = 0; U32 LLVertexBuffer::sVertexCount = 0; diff --git a/indra/llrender/llvertexbuffer.h b/indra/llrender/llvertexbuffer.h index f24d75e41d0..6c71211bb78 100644 --- a/indra/llrender/llvertexbuffer.h +++ b/indra/llrender/llvertexbuffer.h @@ -326,9 +326,11 @@ class LLVertexBuffer final : public LLRefCount static U64 getBytesAllocated(); static const U32 sTypeSize[TYPE_MAX]; static const U32 sGLMode[LLRender::NUM_MODES]; - static U32 sGLRenderBuffer; - static U32 sGLRenderIndices; - static U32 sLastMask; + // Per-context GL state mirrors (current bindings and enabled-attribute + // mask), so thread_local. + static thread_local U32 sGLRenderBuffer; + static thread_local U32 sGLRenderIndices; + static thread_local U32 sLastMask; static U32 sVertexCount; }; diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h index 185940e32dd..f2e4e7ddacd 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -207,6 +207,20 @@ class LLWindow : public LLInstanceTracker virtual S32 getRefreshRate() { return mRefreshRate; } virtual void initWatchdog() {} // windows runs window as a thread and it needs a watchdog + + // New virtuals go at the END of the class - inserting mid-class + // shifts every later vtable slot, and any stale or out-of-sync + // translation unit then dispatches into the wrong function. + + // Swap every Nth vblank (1 = every vblank). The driver does the + // pacing; callers use this as a frame limiter. Call on the thread + // whose GL context should be paced. + virtual void setSwapInterval(S32 interval) {} + + // True when the display runs a dynamic refresh rate (the OS swings + // it between a base and boost rate). Computed as a side effect of + // getRefreshRate() where supported. + virtual bool isDynamicRefreshRate() { return false; } protected: LLWindow(LLWindowCallbacks* callbacks, bool fullscreen, U32 flags); virtual ~LLWindow(); diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 79cdf8b67a9..41345cb855a 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -34,6 +34,7 @@ #include "llkeyboardwin32.h" #include "lldragdropwin32.h" #include "llpreeditor.h" +#include "llthread.h" // for ASSERT_MAIN_THREAD #include "llwindowcallbacks.h" // Linden library includes @@ -105,13 +106,14 @@ extern bool gDebugWindowProc; static std::thread::id sWindowThreadId; static std::thread::id sMainThreadId; -#if 1 // flip to zero to enable assertions for functions being called from wrong thread -#define ASSERT_MAIN_THREAD() -#define ASSERT_WINDOW_THREAD() -#else -#define ASSERT_MAIN_THREAD() llassert(LLThread::currentID() == sMainThreadId) +// "Main" here means the thread that runs the frame loop, which is the +// viewer thread now. Before the viewer thread spawns this falls back to +// OS main, so it works either way. The window message pump thread is +// its own thing and keeps its own assert. +// assert_viewer_thread() logs the offending thread id before failing, +// which is what you want when hunting a wrong-thread call. +#define ASSERT_MAIN_THREAD() llassert(assert_viewer_thread()) #define ASSERT_WINDOW_THREAD() llassert(LLThread::currentID() == sWindowThreadId) -#endif LPWSTR gIconResource = IDI_APPLICATION; @@ -2011,6 +2013,125 @@ void LLWindowWin32::toggleVSync(bool enable_vsync) } } +#ifndef QDC_VIRTUAL_REFRESH_RATE_AWARE +#define QDC_VIRTUAL_REFRESH_RATE_AWARE 0x00000040 +#endif + +// Signal rate of the active display path feeding the given GDI device, +// via the CCD API. With QDC_VIRTUAL_REFRESH_RATE_AWARE this reports the +// true rate on dynamic-refresh paths; without it, the base rate - the +// two disagreeing is how we detect dynamic refresh. +static F32 query_ccd_refresh_rate(const WCHAR* gdi_device, UINT32 extra_flags) +{ + UINT32 flags = QDC_ONLY_ACTIVE_PATHS | extra_flags; + UINT32 num_paths = 0; + UINT32 num_modes = 0; + if (GetDisplayConfigBufferSizes(flags, &num_paths, &num_modes) != ERROR_SUCCESS + || num_paths == 0) + { + return 0.f; + } + std::vector paths(num_paths); + std::vector modes(num_modes); + if (QueryDisplayConfig(flags, &num_paths, paths.data(), + &num_modes, modes.data(), nullptr) != ERROR_SUCCESS) + { + return 0.f; + } + for (UINT32 i = 0; i < num_paths; ++i) + { + DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {}; + source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source.header.size = sizeof(source); + source.header.adapterId = paths[i].sourceInfo.adapterId; + source.header.id = paths[i].sourceInfo.id; + if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS + && wcscmp(source.viewGdiDeviceName, gdi_device) == 0) + { + const DISPLAYCONFIG_RATIONAL& rr = paths[i].targetInfo.refreshRate; + if (rr.Numerator > 0 && rr.Denominator > 0) + { + return (F32)rr.Numerator / (F32)rr.Denominator; + } + } + } + return 0.f; +} + +S32 LLWindowWin32::getRefreshRate() +{ + // Rate of the monitor the window is on right now, via the CCD API. + // Also notes whether the path runs dynamic refresh (virtual-aware + // and base queries disagree). + HMONITOR monitor = MonitorFromWindow(mWindowHandle, MONITOR_DEFAULTTONEAREST); + if (monitor) + { + MONITORINFOEXW mi; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(monitor, &mi)) + { + F32 aware_rate = query_ccd_refresh_rate(mi.szDevice, QDC_VIRTUAL_REFRESH_RATE_AWARE); + if (aware_rate > 1.f) + { + F32 base_rate = query_ccd_refresh_rate(mi.szDevice, 0); + mIsDynamicRefresh = (base_rate > 1.f) + && (llround(aware_rate) != llround(base_rate)); + mRefreshRate = (S32)llround(aware_rate); + return mRefreshRate; + } + + // CCD unavailable: fall back to the highest mode this + // display supports at its current resolution. + mIsDynamicRefresh = false; + DEVMODEW current = {}; + current.dmSize = sizeof(current); + if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, ¤t)) + { + S32 max_rate = (S32)current.dmDisplayFrequency; + for (DWORD i = 0; ; ++i) + { + // Re-zero per call so a mode that omits a field + // doesn't inherit the previous iteration's value. + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + if (!EnumDisplaySettingsW(mi.szDevice, i, &dm)) + { + break; + } + if (dm.dmPelsWidth == current.dmPelsWidth + && dm.dmPelsHeight == current.dmPelsHeight + && (S32)dm.dmDisplayFrequency > max_rate) + { + max_rate = (S32)dm.dmDisplayFrequency; + } + } + if (max_rate > 1) + { + mRefreshRate = max_rate; + } + } + } + } + return mRefreshRate; +} + +bool LLWindowWin32::isDynamicRefreshRate() +{ + // Computed as a side effect of getRefreshRate(). + return mIsDynamicRefresh; +} + +void LLWindowWin32::setSwapInterval(S32 interval) +{ + if (wglSwapIntervalEXT == nullptr) + { + LL_INFOS("Window") << "setSwapInterval: wglSwapIntervalEXT not initialized" << LL_ENDL; + return; + } + LL_INFOS("Window") << "Swap interval: " << interval << LL_ENDL; + wglSwapIntervalEXT(llmax(interval, 0)); +} + void LLWindowWin32::moveWindow( const LLCoordScreen& position, const LLCoordScreen& size ) { if( mIsMouseClipping ) @@ -2047,8 +2168,10 @@ void LLWindowWin32::setTitle(const std::string title) bool LLWindowWin32::setCursorPosition(const LLCoordWindow position) { - ASSERT_MAIN_THREAD(); - + // Cursor state is OS-thread-owned: the cache write and the OS call + // both run on the window thread. Callers anywhere just route the + // request; the app sees the result through the normal mouse + // pipeline. if (!mWindowHandle) { return false; @@ -2056,19 +2179,18 @@ bool LLWindowWin32::setCursorPosition(const LLCoordWindow position) LLCoordScreen screen_pos(position.convert()); - // instantly set the cursor position from the app's point of view - mCursorPosition = position; - mLastCursorPosition = position; - - // Inform the application of the new mouse position (needed for per-frame - // hover/picking to function). - mCallbacks->handleMouseMove(this, position.convert(), (MASK)0); + // Publish the requested position so a same-frame getCursorPosition + // reflects the warp before the window thread applies it below. The + // cursor cache itself is still written only on the window thread. + mPendingWarp.store(((U64)(U32)position.mX << 32) | (U32)position.mY, + std::memory_order_relaxed); + mHasPendingWarp.store(true, std::memory_order_release); - // actually set the cursor position on the window thread mWindowThread->post([=]() { - // actually set the OS cursor position + mCursorPosition = position; SetCursorPos(screen_pos.mX, screen_pos.mY); + mHasPendingWarp.store(false, std::memory_order_release); }); return true; @@ -2076,13 +2198,23 @@ bool LLWindowWin32::setCursorPosition(const LLCoordWindow position) bool LLWindowWin32::getCursorPosition(LLCoordWindow *position) { - ASSERT_MAIN_THREAD(); + // Reading is fine from any thread. Only the OS (window) thread writes + // the cache; a warp that hasn't been applied yet is served from the + // pending hint so callers don't read the stale pre-warp position. if (!position) { return false; } - *position = mCursorPosition; + if (mHasPendingWarp.load(std::memory_order_acquire)) + { + const U64 packed = mPendingWarp.load(std::memory_order_relaxed); + *position = LLCoordWindow((S32)(U32)(packed >> 32), (S32)(U32)(packed & 0xFFFFFFFFu)); + } + else + { + *position = mCursorPosition; + } return true; } @@ -3007,6 +3139,8 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_LBUTTONDBLCLK: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_LBUTTONDBLCLK"); + // Input state mutates HERE, on the OS thread. + window_imp->mCursorPosition = window_coord; window_imp->postMouseButtonEvent([=]() { //RN: ignore right button double clicks for now @@ -3017,9 +3151,6 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ return; } MASK mask = gKeyboard->currentMask(true); - - // generate move event to update mouse coordinates - window_imp->mCursorPosition = window_coord; window_imp->mCallbacks->handleDoubleClick(window_imp, window_imp->mCursorPosition.convert(), mask); }); @@ -3029,6 +3160,8 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_LBUTTONUP"); { + // Input state mutates HERE, on the OS thread. + window_imp->mCursorPosition = window_coord; window_imp->postMouseButtonEvent([=]() { LL_RECORD_BLOCK_TIME(FTM_MOUSEHANDLER); @@ -3041,8 +3174,6 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ MASK mask = gKeyboard->currentMask(true); - // generate move event to update mouse coordinates - window_imp->mCursorPosition = window_coord; window_imp->mCallbacks->handleMouseUp(window_imp, window_imp->mCursorPosition.convert(), mask); }); } @@ -3252,15 +3383,16 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_MOUSEMOVE: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_MOUSEMOVE"); - // DO NOT use mouse event queue for move events to ensure cursor position is updated - // when button events are handled + // Input state mutates HERE, on the OS thread. + window_imp->mCursorPosition = window_coord; + // The modifier mask comes from app-side keyboard state, so + // it updates through the queue with the other app work. WINDOW_IMP_POST( { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_MOUSEMOVE lambda"); MASK mask = gKeyboard->currentMask(true); window_imp->mMouseMask = mask; - window_imp->mCursorPosition = window_coord; }); return 0; } diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index defa90d1a32..ee16d45fa12 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -38,6 +38,8 @@ #include "llmutex.h" #include "workqueue.h" +#include + // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) // For requesting shutdown on uninstall, @@ -73,6 +75,9 @@ class LLWindowWin32 : public LLWindow void makeContextCurrent(void* context) override; void destroySharedContext(void* context) override; void toggleVSync(bool enable_vsync) override; + void setSwapInterval(S32 interval) override; + S32 getRefreshRate() override; + bool isDynamicRefreshRate() override; bool setCursorPosition(LLCoordWindow position) override; bool getCursorPosition(LLCoordWindow *position) override; bool getCursorDelta(LLCoordCommon* delta) override; @@ -213,9 +218,18 @@ class LLWindowWin32 : public LLWindow WPARAM mLastSizeWParam; F32 mOverrideAspectRatio; F32 mNativeAspectRatio; + bool mIsDynamicRefresh = false; // display runs dynamic refresh; see getRefreshRate() HCURSOR mCursor[ UI_CURSOR_COUNT ]; // Array of all mouse cursors - LLCoordWindow mCursorPosition; // mouse cursor position, should only be mutated on main thread + LLCoordWindow mCursorPosition; // mouse cursor position; written only on the OS (window) thread, readable from anywhere + + // A warp requested via setCursorPosition is applied asynchronously on + // the window thread. Until then getCursorPosition prefers the + // requested position so a same-frame read right after a warp isn't + // stale. The cursor cache itself is still written only on the window + // thread; this is just a read-side hint, packed (x<<32 | y). + std::atomic mHasPendingWarp{false}; + std::atomic mPendingWarp{0}; bool mAbsoluteCursorPosition; // true if last position was received in absolute coordinates. LLMutex mRawMouseMutex; RAWINPUTDEVICE mRawMouse; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 663b932f3b1..cc61811157f 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -709,6 +709,7 @@ set(viewer_SOURCE_FILES llviewertexture.cpp llviewertextureanim.cpp llviewertexturelist.cpp + llviewerthread.cpp llviewerthrottle.cpp llviewerwearable.cpp llviewerwindow.cpp @@ -1382,6 +1383,7 @@ set(viewer_HEADER_FILES llviewertexture.h llviewertextureanim.h llviewertexturelist.h + llviewerthread.h llviewerthrottle.h llviewerwearable.h llviewerwindow.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b1e161182ee..d3869dc72cd 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2194,6 +2194,17 @@ Value 0 + RenderCompositorShowRefresh + + Comment + Show the compositor refresh overlay (colored squares repainting at different sync-rate intervals, bottom-left) + Persist + 1 + Type + Boolean + Value + 0 + DebugShowRenderMatrices Comment @@ -2546,10 +2557,10 @@ Value 0 - RenderVSyncEnable + CoroutinePoolsOnViewerThread Comment - Update frames between display scans (FALSE = Update frames as fast as possible). + Run the coprocedure coroutine pools (AIS, Upload) on the viewer thread, alongside the inventory and UI state they touch. Turning this off runs them on the OS main thread instead - experimental and not yet safe. Persist 1 Type @@ -2557,6 +2568,17 @@ Value 1 + RenderVSyncMode + + Comment + World pacing against the compositor's sync signal. 0 = off (world renders as fast as it can), 1 = every sync (refresh rate), 2 = every other sync (refresh/2), 3 = every 2nd sync (refresh/3), 4 = every 3rd sync (refresh/4). The compositor itself always presents synced. + Persist + 1 + Type + U32 + Value + 1 + EnableGroupChatPopups Comment diff --git a/indra/newview/app_settings/shaders/class1/interface/compositorblitF.glsl b/indra/newview/app_settings/shaders/class1/interface/compositorblitF.glsl new file mode 100644 index 00000000000..086866855f7 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/compositorblitF.glsl @@ -0,0 +1,37 @@ +/** + * @file compositorblitF.glsl + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +/*[EXTRA_CODE_HERE]*/ + +out vec4 frag_color; + +uniform sampler2D diffuseMap; + +in vec2 tc; + +void main() +{ + frag_color = texture(diffuseMap, tc); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/compositorblitV.glsl b/indra/newview/app_settings/shaders/class1/interface/compositorblitV.glsl new file mode 100644 index 00000000000..1a8f5559461 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/compositorblitV.glsl @@ -0,0 +1,39 @@ +/** + * @file compositorblitV.glsl + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +// Attribute-less quad for the compositor's layer blit. gl_VertexID +// expands to the unit square; blit_rect places it in NDC. + +uniform vec4 blit_rect; + +out vec2 tc; + +void main() +{ + vec2 p = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)); + tc = p; + vec2 ndc = mix(blit_rect.xy, blit_rect.zw, p); + gl_Position = vec4(ndc, 0.0, 1.0); +} diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index b6485b41e88..1da7f3ddf9b 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -73,6 +73,7 @@ #include "lltracethreadrecorder.h" #include "llviewerwindow.h" #include "llviewerdisplay.h" +#include "llviewerthread.h" #include "llviewermedia.h" #include "llviewerparcelaskplay.h" #include "llviewerparcelmedia.h" @@ -245,6 +246,8 @@ #include "gltfscenemanager.h" #include "workqueue.h" + +#include using namespace LL; // Include for security api initialization @@ -375,6 +378,15 @@ bool gDoDisconnect = false; // to have to block due to this WorkQueue being full. WorkQueue gMainloopWork("mainloop", 1024*1024); +// Work queue for things that need to run on the viewer thread, like +// texture-sync callbacks that have to land on the thread that owns the +// viewer's GL context. +WorkQueue gViewerWork("viewer", 1024*1024); + +// Work queue for things that need to run on the OS main thread, like +// DestroyWindow calls on windows that thread owns. +WorkQueue gOSMainWork("osmain", 1024*1024); + //////////////////////////////////////////////////////////// // Internal globals... that should be removed. static std::string gArgs; @@ -737,6 +749,524 @@ LLAppViewer::~LLAppViewer() removeMarkerFiles(); } +// - LLCompositable plumbing ------------------------------------------------- + +void LLAppViewer::resetMailbox() +{ + // Starting layout: we hold 0, the free slot holds 1, and the + // compositor displays 2 (black until the first publish). Both + // pacing modes use the same layout. + mBackIdx = 0; + mLatestIdx.store(-1, std::memory_order_release); + mFreeIdx.store(1, std::memory_order_release); + mDisplayIdx = 2; +} + +void LLAppViewer::allocateRenderTargets(U32 width, U32 height) +{ + llassert(width > 0 && height > 0); + + // All three buffers are marked as swap chain images so the viewport + // restores correctly on pop-back, and flagged for cross-context + // sync since the viewer thread writes them while the compositor's + // context reads them. + for (auto& rt : mRTs) + { + rt.allocate(width, height, GL_RGBA, /*depth=*/true); + rt.markAsSwapChainImage(true); + rt.setNeedsCrossContextSync(true); + } + resetMailbox(); + + // Scratch parent for the post-display updaters - somewhere safe for + // cubeSnapshot's raw glClear to land each frame. + mScratchParent.allocate(width, height, GL_RGBA, /*depth=*/true); + mScratchParent.markAsSwapChainImage(true); +} + +void LLAppViewer::resizeRenderTargets(U32 width, U32 height) +{ + if (!mRTs[0].isComplete()) + { + allocateRenderTargets(width, height); + return; + } + for (auto& rt : mRTs) + { + rt.resize(width, height); + } + mScratchParent.resize(width, height); +} + +void LLAppViewer::releaseRenderTargets() +{ + for (auto& rt : mRTs) + { + rt.release(); + } + mScratchParent.release(); + resetMailbox(); +} + +void LLAppViewer::markFrameRendered() +{ + // When vsync is on the world is compositor-paced: wait for the + // compositor to claim our last publish before sending another, so + // we don't run ahead of it. With vsync off the world runs free and + // the latest frame wins. + static LLCachedControl vsync_mode(gSavedSettings, "RenderVSyncMode", 1); + if ((U32)vsync_mode > 0) + { + U32 spins = 0; + while (mLatestIdx.load(std::memory_order_acquire) != -1) + { + if (++spins < 64) + { + std::this_thread::yield(); + } + else + { + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } + } + } + + // Publish into the mailbox. Fence and flush first so the compositor + // can wait on the completed frame GPU-side. + mRTs[mBackIdx].placeFrameCompleteFence(); + glFlush(); + + S32 old_latest = mLatestIdx.exchange((S32)mBackIdx, std::memory_order_acq_rel); + if (old_latest != -1) + { + // The compositor never picked up our last publish - take it back + // and reuse it as our next back buffer. + mBackIdx = (U32)old_latest; + } + else + { + // The compositor took our last publish; grab the buffer it + // released from the free slot. There's a tiny window in + // tryAcquireNewFront where both slots are briefly empty, so + // spin until the free slot fills in. + S32 free_idx = mFreeIdx.exchange(-1, std::memory_order_acq_rel); + U32 spins = 0; + while (free_idx == -1) + { + if (++spins < 64) + { + std::this_thread::yield(); + } + else + { + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } + free_idx = mFreeIdx.exchange(-1, std::memory_order_acq_rel); + } + mBackIdx = (U32)free_idx; + } + produceFrame(); // frame metrics +} + +bool LLAppViewer::tryAcquireNewFront() +{ + // Compositor thread: claim the latest published buffer, if any. + S32 latest = mLatestIdx.exchange(-1, std::memory_order_acq_rel); + if (latest == -1) + { + return false; // nothing new; keep re-presenting mDisplayIdx + } + // Release the previously displayed buffer to the producer's free + // slot and adopt the fresh one. + mFreeIdx.store(mDisplayIdx, std::memory_order_release); + mDisplayIdx = latest; + return true; +} + +void LLAppViewer::viewerThreadTick() +{ + // Vsync pacing (RenderVSyncMode): 0 = off, the world renders as + // fast as it can. Otherwise run one tick per compositor sync - the + // driver already divides the present cadence via the swap interval, + // so waiting for the next sync IS refresh/N pacing. Stale ticks get + // drained first so a slow frame doesn't burst afterward. The pop + // times out so a stalled compositor can't wedge us, and the + // single-thread fallback skips the gate - the sync fires after our + // inline tick, so waiting on it would deadlock. + static LLCachedControl vsync_mode(gSavedSettings, "RenderVSyncMode", 1); + if ((U32)vsync_mode > 0 && mViewerThread) + { + U64 tick = 0; + while (mSyncTicks.tryPop(tick)) {} + mSyncTicks.tryPopFor(std::chrono::milliseconds(100), tick); + } + + // Everything the old main loop did runs here, minus the compositor + // present and the OS-affine work queue, which stay on OS main. + LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_FRAME); + +#ifdef LL_DISCORD + { + LL_PROFILE_ZONE_NAMED("discord_callbacks"); + discordpp::RunCallbacks(); + } +#endif + + if (!LLWorld::instanceExists()) + { + LLWorld::createInstance(); + } + + //memory leaking simulation + if (gSimulateMemLeak) + { + LLFloaterMemLeak* mem_leak_instance = + LLFloaterReg::findTypedInstance("mem_leaking"); + if (mem_leak_instance) + { + mem_leak_instance->idle(); + } + } + + // Apply auto-tune updates decided last frame. + if (LLPerfStats::tunables.userAutoTuneEnabled + && LLPerfStats::tunables.tuningFlag != LLPerfStats::Tunables::Nothing) + { + LLPerfStats::tunables.applyUpdates(); + } + + // LLTrace frame rotation. The recorders are thread_local, so this + // has to happen on the thread that records and reads them - us. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt LLTrace"); + if (LLFloaterReg::instanceVisible("block_timers")) + { + LLTrace::BlockTimer::processTimes(); + } + LLTrace::get_frame_recording().nextPeriod(); + LLTrace::BlockTimer::logStats(); + LLTrace::get_thread_recorder()->pullFromChildren(); + LL_CLEAR_CALLSTACKS(); + } + + // Drain native events. The OS window thread queues them; we drain + // them here. The queues are thread safe. + if (gViewerWindow) + { + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt processMiscNativeEvents"); + gViewerWindow->getWindow()->processMiscNativeEvents(); + } + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt gatherInput"); + // Re-arm the crash/signal handlers - drivers and injected + // DLLs can overwrite them during message handling. + if (!restoreErrorTrap()) + { + LL_WARNS() << " Someone took over my signal/exception handler (post messagehandling)!" << LL_ENDL; + } + gViewerWindow->getWindow()->gatherInput(); + } + } + + // Joystick / keyboard / mouse polls. Same gating as the old + // single-threaded doFrame. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt JoystickKeyboard"); + pingMainloopTimeout("Vt:JoystickKeyboard"); + + if (gViewerWindow + && (gHeadlessClient || gViewerWindow->getWindow()->getVisible()) + && gViewerWindow->getActive() + && !gViewerWindow->getWindow()->getMinimized() + && LLStartUp::getStartupState() == STATE_STARTED + && (gHeadlessClient || !gViewerWindow->getShowProgress()) + && !gFocusMgr.focusLocked()) + { + LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); + joystick->scanJoystick(); + gKeyboard->scanKeyboard(); + gViewerInput.scanMouse(); + } + } + + // Drain the viewer-thread async work queue. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt work"); + static std::chrono::nanoseconds kViewerWorkTime{1'000'000}; // 1ms slice + gViewerWork.runFor(kViewerWorkTime); + } + + // Post the per-frame "mainloop" event and give coroutines a chance + // to run. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt mainloop"); + pingMainloopTimeout("vt mainloop"); + static LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); + mainloop.post(LLSD()); + } + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt suspend"); + pingMainloopTimeout("vt suspend"); + llcoro::suspend(); + LLCoros::instance().rethrow(); + } + + // Full idle pass - network, media, timers, everything the old + // single-threaded doFrame ran. + { + pingMainloopTimeout("vt idle"); + pauseMainloopTimeout(); + } + { + LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt idle"); + idle(); + } + { + resumeMainloopTimeout("vt idle"); + } + + // Disconnect runs here, not in doFrame - saveFinalSnapshot renders + // (rawSnapshot -> display) and disconnectViewer tears down world + // state, both of which belong to us. + if (gDoDisconnect && (LLStartUp::getStartupState() == STATE_STARTED)) + { + pauseMainloopTimeout(); + saveFinalSnapshot(); + + if (LLVoiceClient::instanceExists()) + { + LLVoiceClient::getInstance()->terminate(); + } + + disconnectViewer(); + resumeMainloopTimeout("vt snapshot n disconnect"); + } + + renderViewerFrame(); + + if (LLViewerStatsRecorder::instanceExists()) + { + LLViewerStatsRecorder::instance().idle(); + } + + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt pauseMainloopTimeout"); + pingMainloopTimeout("Main:Sleep"); + pauseMainloopTimeout(); + } + + // Sleep and run background threads + { + LL_PROFILE_ZONE_WARN("Sleep2"); + + // yield some time to the os based on command line option + static LLCachedControl yield_time(gSavedSettings, "YieldTime", -1); + if (yield_time >= 0) + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("Yield"); + LL_PROFILE_ZONE_NUM(yield_time); + ms_sleep(yield_time); + } + + if (gNonInteractive) + { + S32 non_interactive_ms_sleep_time = 100; + LLAppViewer::getTextureCache()->pause(); + ms_sleep(non_interactive_ms_sleep_time); + } + + // yield cooperatively when not running as foreground window + // and when not quiting (causes trouble at mac's cleanup stage) + if (!LLApp::isExiting() + && ((gViewerWindow && !gViewerWindow->getWindow()->getVisible()) + || !gFocusMgr.getAppHasFocus())) + { + // Sleep if we're not rendering, or the window is minimized. + static LLCachedControl s_background_yield_time(gSavedSettings, "BackgroundYieldTime", 40); + S32 milliseconds_to_sleep = llclamp((S32)s_background_yield_time, 0, 1000); + // don't sleep when BackgroundYieldTime set to 0, since this will still yield to other threads + // of equal priority on Windows + if (milliseconds_to_sleep > 0) + { + LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SLEEP ); + ms_sleep(milliseconds_to_sleep); + // also pause worker threads during this wait period + LLAppViewer::getTextureCache()->pause(); + } + } + + if (mRandomizeFramerate) + { + ms_sleep(rand() % 200); + } + + if (mPeriodicSlowFrame + && (gFrameCount % 10 == 0)) + { + LL_INFOS() << "Periodic slow frame - sleeping 500 ms" << LL_ENDL; + ms_sleep(500); + } + + S32 total_work_pending = 0; + S32 total_io_pending = 0; + { + S32 work_pending = 0; + S32 io_pending = 0; + F32 max_time = llmin(gFrameIntervalSeconds.value() *10.f, 1.f); + + work_pending += updateTextureThreads(max_time); + + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("LFS Thread"); + io_pending += LLLFSThread::updateClass(1); + } + + if (io_pending > 1000) + { + ms_sleep(llmin(io_pending/100,100)); // give the lfs some time to catch up + } + + total_work_pending += work_pending ; + total_io_pending += io_pending ; + } + + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt gMeshRepo"); + gMeshRepo.update() ; + } + + if(!total_work_pending) //pause texture fetching threads if nothing to process. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt getTextureCache"); + LLAppViewer::getTextureCache()->pause(); + LLAppViewer::getTextureFetch()->pause(); + } + if(!total_io_pending) //pause file threads if nothing to process. + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt LLVFSThread"); + LLLFSThread::sLocal->pause(); + } + + { + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("vt resumeMainloopTimeout"); + resumeMainloopTimeout(); + } + pingMainloopTimeout("Main:End"); + } + + LLPerfStats::StatsRecorder::endFrame(); +} + +void LLAppViewer::viewerThreadShutdown() +{ + // The tail of the old main loop, run by the viewer thread (or its + // inline stand-in) on the way out, while the world and GL are still + // intact. + + // Save the login-screen snapshot for next time. No-op if the + // disconnect path already saved one. + if (LLStartUp::getStartupState() == STATE_STARTED) + { + saveFinalSnapshot(); + } + + if (LLVoiceClient::instanceExists()) + { + LLVoiceClient::getInstance()->terminate(); + } + + // LLTrace recordings have to deactivate on this thread - they're + // registered with this thread's recorder, which LLThread deletes + // when we exit. Stopped recordings are safe to destroy from any + // thread later, so singletons that outlive us just get their + // recordings stopped here. + if (LLSceneMonitor::instanceExists()) + { + if (!isSecondInstance()) + { + std::string dump_path = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "scene_monitor_results.csv"); + LLSceneMonitor::instance().dumpToFile(dump_path); + } + LLSceneMonitor::deleteSingleton(); + } + LLViewerAssetStatsFF::cleanup(); + if (LLViewerStats::instanceExists()) + { + LLViewerStats::instance().getRecording().stop(); + } + + delete gServicePump; + gServicePump = NULL; + + destroyMainloopTimeout(); +} + +void LLAppViewer::renderViewerFrame() +{ + // Render into the back buffer, then publish it through the mailbox. + // The compositor presents on its own clock; we never wait on it. + + // Only publish if a render path actually drew into the back buffer + // this tick. If nothing drew, the compositor just keeps showing the + // last good frame. + mBackBufferRendered = false; + + // For the world render, a top-level parentless flush is a missed + // bind site (the back buffer should be the bottom of the stack), so + // assert. Outside this scope - standalone GPU passes like avatar + // profiling run from idle() - parentless flushes are legitimate and + // fall back to the default framebuffer. + LLRenderTarget::sFlushRequiresParent = true; + + // Wait GPU-side until the compositor is done reading the buffer we + // just picked up as our new back. + getBackBuffer().waitReadCompleteFence(); + + display(); + + LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Snapshot"); + pingMainloopTimeout("Main:Snapshot"); + + // The updaters need a parent on the RT stack to pop back to, but it + // can't be the back buffer - cubeSnapshot clears whatever's bound, + // which would wipe the frame we just rendered. Give them the scratch + // parent instead. + bool scratch_bound = false; + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + mScratchParent.bindTarget(); + scratch_bound = true; + } + + gPipeline.mReflectionMapManager.update(); + LLFloaterSnapshot::update(); + LLFloaterSimpleSnapshot::update(); + + if (scratch_bound && + LLRenderTarget::getCurrentBoundTarget() == &mScratchParent) + { + mScratchParent.flush(); + } + + llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); + + // The one publish for this tick. Skip it if nothing drew into the + // back buffer, or if the snapshot machinery cleared + // gDisplaySwapBuffers to keep a readback-only frame off the screen. + if (mBackBufferRendered && gDisplaySwapBuffers) + { + markFrameRendered(); + } + gDisplaySwapBuffers = true; + + // Back out of the world-render scope; standalone passes that run + // before the next renderViewerFrame may flush parentless. + LLRenderTarget::sFlushRequiresParent = false; +} + class LLUITranslationBridge : public LLTranslationBridge { public: @@ -751,6 +1281,10 @@ bool LLAppViewer::init() { LL_PROFILE_ZONE_SCOPED; + // Remember which thread is the OS main thread before we spawn any + // others, so OS-main-thread checks have something to compare against. + set_os_main_thread(); + setupErrorHandling(mSecondInstance); // @@ -1208,11 +1742,26 @@ bool LLAppViewer::init() LLAgentLanguage::init(); - /// Tell the Coprocedure manager how to discover and store the pool sizes - // what I wanted - LLCoprocedureManager::getInstance()->setPropertyMethods( - std::bind(&LLControlGroup::getU32, std::ref(gSavedSettings), std::placeholders::_1), - std::bind(&LLControlGroup::declareU32, std::ref(gSavedSettings), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, LLControlVariable::PERSIST_ALWAYS)); + /// Tell the Coprocedure manager how to discover and store the pool + /// sizes. This also launches the coroutine pools, and coroutines + /// stay on the thread that launches them. They touch inventory and + /// UI state that lives on the viewer thread, so by default we defer + /// the launch there via gViewerWork. CoroutinePoolsOnViewerThread + /// set to false keeps the old inline launch for experiments. + auto init_coproc_pools = []() + { + LLCoprocedureManager::getInstance()->setPropertyMethods( + std::bind(&LLControlGroup::getU32, std::ref(gSavedSettings), std::placeholders::_1), + std::bind(&LLControlGroup::declareU32, std::ref(gSavedSettings), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, LLControlVariable::PERSIST_ALWAYS)); + }; + if (gSavedSettings.getBOOL("CoroutinePoolsOnViewerThread")) + { + gViewerWork.post(init_coproc_pools); + } + else + { + init_coproc_pools(); + } // TODO: consider moving proxy initialization here or LLCopocedureManager after proxy initialization, may be implement // some other protection to make sure we don't use network before initializng proxy @@ -1252,6 +1801,20 @@ bool LLAppViewer::init() gDirUtilp->deleteDirAndContents(gDirUtilp->getDumpLogsDirPath()); } #endif + + // Init is done - spawn the viewer thread. From here on the viewer + // runs on its own thread and OS main just composites and pumps + // events. Windows only for now; other platforms stay single + // threaded. +#if LL_WINDOWS + if (gViewerWindow && gViewerWindow->getWindow()) + { + mViewerThread = std::make_unique(gViewerWindow->getWindow()); + mViewerThread->start(); + LL_INFOS("Viewer") << "Viewer thread spawned." << LL_ENDL; + } +#endif + LL_PROFILER_FRAME_END; return true; } @@ -1326,358 +1889,97 @@ bool LLAppViewer::frame() bool LLAppViewer::doFrame() { - resumeMainloopTimeout("Main:doFrameStart"); -#ifdef LL_DISCORD - { - LL_PROFILE_ZONE_NAMED("discord_callbacks"); - discordpp::RunCallbacks(); - } -#endif - + // OS main owns exactly three things: the OS-affine work queue, the + // compositor present, and standing in for the viewer thread when it + // isn't spawned. Everything else from the old main loop lives in + // viewerThreadTick / viewerThreadShutdown. LL_RECORD_BLOCK_TIME(FTM_FRAME); LL_PROFILE_GPU_ZONE("Frame"); - { - // and now adjust the visuals from previous frame. - if(LLPerfStats::tunables.userAutoTuneEnabled && LLPerfStats::tunables.tuningFlag != LLPerfStats::Tunables::Nothing) - { - LLPerfStats::tunables.applyUpdates(); - } - LLPerfStats::RecordSceneTime T (LLPerfStats::StatType_t::RENDER_FRAME); - if (!LLWorld::instanceExists()) + if (!LLApp::isExiting()) { - LLWorld::createInstance(); - } - - LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); - LLSD newFrame; - { - LLPerfStats::RecordSceneTime T (LLPerfStats::StatType_t::RENDER_IDLE); // perf stats + // No viewer thread? Run the whole viewer tick inline here + // instead - same function the viewer thread would run. + if (!mViewerThread) { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df LLTrace"); - if (LLFloaterReg::instanceVisible("block_timers")) - { - LLTrace::BlockTimer::processTimes(); - } - - LLTrace::get_frame_recording().nextPeriod(); - LLTrace::BlockTimer::logStats(); + viewerThreadTick(); } - LLTrace::get_thread_recorder()->pullFromChildren(); - - //clear call stack records - LL_CLEAR_CALLSTACKS(); - } - { + // Drain work that has to run on the OS main thread, like + // splash screen teardown. { - LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); // ensure we have the entire top scope of frame covered (input event and coro) - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df processMiscNativeEvents") - pingMainloopTimeout("Main:MiscNativeWindowEvents"); - - if (gViewerWindow) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("System Messages"); - gViewerWindow->getWindow()->processMiscNativeEvents(); - } - - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df gatherInput") - pingMainloopTimeout("Main:GatherInput"); - } - - if (gViewerWindow) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("System Messages"); - if (!restoreErrorTrap()) - { - LL_WARNS() << " Someone took over my signal/exception handler (post messagehandling)!" << LL_ENDL; - } - - gViewerWindow->getWindow()->gatherInput(); - } - - //memory leaking simulation - if (gSimulateMemLeak) - { - LLFloaterMemLeak* mem_leak_instance = - LLFloaterReg::findTypedInstance("mem_leaking"); - if (mem_leak_instance) - { - mem_leak_instance->idle(); - } - } - - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df mainloop"); - pingMainloopTimeout("df mainloop"); - // canonical per-frame event - mainloop.post(newFrame); - } - - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df suspend"); - pingMainloopTimeout("df suspend"); - // give listeners a chance to run - llcoro::suspend(); - // if one of our coroutines threw an uncaught exception, rethrow it now - LLCoros::instance().rethrow(); - } + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df osmain work"); + static std::chrono::nanoseconds kOSMainWorkTime{1'000'000}; // 1ms slice + gOSMainWork.runFor(kOSMainWorkTime); } - if (!LLApp::isExiting()) + if (!gHeadlessClient && gViewerWindow) { - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df JoystickKeyboard"); - pingMainloopTimeout("Main:JoystickKeyboard"); - - // Scan keyboard for movement keys. Command keys and typing - // are handled by windows callbacks. Don't do this until we're - // done initializing. JC - if (gViewerWindow - && (gHeadlessClient || gViewerWindow->getWindow()->getVisible()) - && gViewerWindow->getActive() - && !gViewerWindow->getWindow()->getMinimized() - && LLStartUp::getStartupState() == STATE_STARTED - && (gHeadlessClient || !gViewerWindow->getShowProgress()) - && !gFocusMgr.focusLocked()) - { - LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); - joystick->scanJoystick(); - gKeyboard->scanKeyboard(); - gViewerInput.scanMouse(); - } - } - - // Update state based on messages, user input, object idle. - { - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df pauseMainloopTimeout"); - pingMainloopTimeout("df idle"); // So that it will be aware of last state. - pauseMainloopTimeout(); // *TODO: Remove. Messages shouldn't be stalling for 20+ seconds! - } - - { - LLPerfStats::RecordSceneTime T (LLPerfStats::StatType_t::RENDER_IDLE); - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df idle"); - idle(); - } - - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df resumeMainloopTimeout"); - resumeMainloopTimeout("df idle"); - } - } - - if (gDoDisconnect && (LLStartUp::getStartupState() == STATE_STARTED)) - { - pauseMainloopTimeout(); - saveFinalSnapshot(); - - if (LLVoiceClient::instanceExists()) - { - LLVoiceClient::getInstance()->terminate(); - } - - disconnectViewer(); - resumeMainloopTimeout("df snapshot n disconnect"); - } - - // Render scene. - // *TODO: Should we run display() even during gHeadlessClient? DK 2011-02-18 - if (!LLApp::isExiting() && !gHeadlessClient && gViewerWindow) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Display"); - pingMainloopTimeout("Main:Display"); - gGLActive = true; - - display(); - - { - LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Snapshot"); - pingMainloopTimeout("Main:Snapshot"); - - // These updaters allocate and bind their own render - // targets while no parent target is on the stack. Bind - // the swap chain image around the calls so they pop back - // to it instead of tripping the missing-parent assert in - // LLRenderTarget::flush(). Only wrap when the stack is - // empty -- these updaters can recurse into display() (e.g. - // snapshot live preview -> rawSnapshot -> display) which - // may bring its own parent. - bool sc_bound = false; - if (LLRenderTarget::getCurrentBoundTarget() == nullptr) - { - mSwapChain.acquireNextImage().bindTarget(); - sc_bound = true; - } - - gPipeline.mReflectionMapManager.update(); - LLFloaterSnapshot::update(); // take snapshots - LLFloaterSimpleSnapshot::update(); - - if (sc_bound && - LLRenderTarget::getCurrentBoundTarget() == &mSwapChain.getCurrentImage()) - { - // Only flush if our bind is still on top. Updaters - // can internally call swap() (e.g. rawSnapshot's - // non-deferred path), which will already have flushed - // the image. - mSwapChain.getCurrentImage().flush(); - } + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Display"); + gGLActive = true; - gGLActive = false; - } + // The viewer renders at its own rate; the compositor + // presents every doFrame, re-showing the last frame when + // nothing new arrived. Neither side waits on the other - + // swapBuffers (vsync) is the only pacing here. + mCompositor.presentFrame(); - if (LLViewerStatsRecorder::instanceExists()) - { - LLViewerStatsRecorder::instance().idle(); - } - } + gGLActive = false; } - + } + else + { + // The threaded viewer runs its shutdown (final snapshot, voice, + // service pump, watchdog) on its way out of LLViewerThread::run + // when cleanup joins it. Without a viewer thread, we're the + // acting viewer thread - do it here. + if (!mViewerThread) { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df pauseMainloopTimeout"); - pingMainloopTimeout("Main:Sleep"); - - pauseMainloopTimeout(); + viewerThreadShutdown(); } + LL_INFOS() << "Exiting main_loop" << LL_ENDL; + } - // Sleep and run background threads - { - //LL_RECORD_BLOCK_TIME(SLEEP2); - LL_PROFILE_ZONE_WARN("Sleep2"); - - // yield some time to the os based on command line option - static LLCachedControl yield_time(gSavedSettings, "YieldTime", -1); - if(yield_time >= 0) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("Yield"); - LL_PROFILE_ZONE_NUM(yield_time); - ms_sleep(yield_time); - } - - if (gNonInteractive) - { - S32 non_interactive_ms_sleep_time = 100; - LLAppViewer::getTextureCache()->pause(); - ms_sleep(non_interactive_ms_sleep_time); - } - - // yield cooperatively when not running as foreground window - // and when not quiting (causes trouble at mac's cleanup stage) - if (!LLApp::isExiting() - && ((gViewerWindow && !gViewerWindow->getWindow()->getVisible()) - || !gFocusMgr.getAppHasFocus())) - { - // Sleep if we're not rendering, or the window is minimized. - static LLCachedControl s_background_yield_time(gSavedSettings, "BackgroundYieldTime", 40); - S32 milliseconds_to_sleep = llclamp((S32)s_background_yield_time, 0, 1000); - // don't sleep when BackgroundYieldTime set to 0, since this will still yield to other threads - // of equal priority on Windows - if (milliseconds_to_sleep > 0) - { - LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SLEEP ); - ms_sleep(milliseconds_to_sleep); - // also pause worker threads during this wait period - LLAppViewer::getTextureCache()->pause(); - } - } - - if (mRandomizeFramerate) - { - ms_sleep(rand() % 200); - } - - if (mPeriodicSlowFrame - && (gFrameCount % 10 == 0)) - { - LL_INFOS() << "Periodic slow frame - sleeping 500 ms" << LL_ENDL; - ms_sleep(500); - } - - S32 total_work_pending = 0; - S32 total_io_pending = 0; - { - S32 work_pending = 0; - S32 io_pending = 0; - F32 max_time = llmin(gFrameIntervalSeconds.value() *10.f, 1.f); - - work_pending += updateTextureThreads(max_time); - - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("LFS Thread"); - io_pending += LLLFSThread::updateClass(1); - } - - if (io_pending > 1000) - { - ms_sleep(llmin(io_pending/100,100)); // give the lfs some time to catch up - } - - total_work_pending += work_pending ; - total_io_pending += io_pending ; + LL_PROFILER_FRAME_END; - } + return ! LLApp::isRunning(); +} - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df gMeshRepo"); - gMeshRepo.update() ; - } +S32 LLAppViewer::getDisplayRefreshRate() const +{ + // Platform-detected rate of the display the window is on. + if (gViewerWindow && gViewerWindow->getWindow()) + { + return gViewerWindow->getWindow()->getRefreshRate(); + } + return 0; +} - if(!total_work_pending) //pause texture fetching threads if nothing to process. - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df getTextureCache"); - LLAppViewer::getTextureCache()->pause(); - LLAppViewer::getTextureFetch()->pause(); - } - if(!total_io_pending) //pause file threads if nothing to process. - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df LLVFSThread"); - LLLFSThread::sLocal->pause(); - } +void LLAppViewer::setVSyncMode(U32 mode) +{ + mode = llclamp(mode, 0u, 4u); - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df resumeMainloopTimeout"); - resumeMainloopTimeout(); - } - pingMainloopTimeout("Main:End"); - } - } + // The driver does the pacing: swap interval N presents every Nth + // vblank, and the sync signal (and the world with it) inherits that + // cadence. Mode 0 still presents every vblank - only the world runs + // free. The compositor applies it on its own context. + mCompositor.setSwapInterval(mode >= 1 ? (S32)mode : 1); - if (LLApp::isExiting()) + LLPerfStats::tunables.vsyncEnabled = (mode > 0); + if (mode > 0) { - pingMainloopTimeout("Main:qSnapshot"); - // Save snapshot for next time, if we made it through initialization - if (STATE_STARTED == LLStartUp::getStartupState()) + // Tell the autotuner the effective frame rate vsync allows + // (refresh / mode). It caps its target at min(this, TargetFPS) + // live, so it won't chase an unreachable FPS and degrade quality + // for nothing. We do NOT rewrite the user's persisted TargetFPS - + // that's their preference, and capping it here would ratchet it + // down permanently. + S32 refresh = getDisplayRefreshRate(); + if (refresh > 0) { - saveFinalSnapshot(); + LLPerfStats::vsync_max_fps = (U32)(refresh / mode); } - - pingMainloopTimeout("Main:TerminateVoice"); - if (LLVoiceClient::instanceExists()) - { - LLVoiceClient::getInstance()->terminate(); - } - - pingMainloopTimeout("Main:TerminatePump"); - delete gServicePump; - gServicePump = NULL; - - destroyMainloopTimeout(); - - LL_INFOS() << "Exiting main_loop" << LL_ENDL; } - }LLPerfStats::StatsRecorder::endFrame(); - - // Not viewer's fault if something outside frame - // pauses viewer (ex: macOS doesn't call oneFrame), - // so stop tracking on exit. - pauseMainloopTimeout(); - LL_PROFILER_FRAME_END; - - return ! LLApp::isRunning(); } S32 LLAppViewer::updateTextureThreads(F32 max_time) @@ -1728,6 +2030,23 @@ bool LLAppViewer::cleanup() velopack_cleanup(); #endif + // Stop the viewer thread FIRST - everything below tears down state + // its tick is actively using (display, idle, network, render + // targets). It finishes its current tick over still-intact state + // and exits; shutdown() joins. It destroys its GL context on its + // own thread on the way out, since WGL requires that. + if (mViewerThread) + { + mViewerThread->requestStop(); + mViewerThread->shutdown(); + mViewerThread.reset(); + + // With the thread gone, OS main is the acting viewer thread for + // the rest of cleanup - window/render calls below would trip + // on_viewer_thread() asserts against a dead thread id otherwise. + clear_viewer_thread(); + } + //ditch LLVOAvatarSelf instance gAgentAvatarp = NULL; @@ -1736,16 +2055,9 @@ bool LLAppViewer::cleanup() // workaround for DEV-35406 crash on shutdown LLEventPumps::instance().reset(true); - //dump scene loading monitor results - if (LLSceneMonitor::instanceExists()) - { - if (!isSecondInstance()) - { - std::string dump_path = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "scene_monitor_results.csv"); - LLSceneMonitor::instance().dumpToFile(dump_path); - } - LLSceneMonitor::deleteSingleton(); - } + // Scene monitor and asset stats were torn down by the viewer thread + // on its way out (viewerThreadShutdown) - their LLTrace recordings + // belong to that thread's recorder. // There used to be an 'if (LLFastTimerView::sAnalyzePerformance)' block // here, completely redundant with the one that occurs later in this same @@ -2061,9 +2373,11 @@ bool LLAppViewer::cleanup() LL_INFOS() << "Shutting down OpenGL" << LL_ENDL; - // Drop the swap chain before window teardown -- its images are bound to + // Drop LLAppViewer's render targets and the compositor (which owns the + // swap chain) before window teardown - their resources are bound to // the OS window and must not outlive it. - mSwapChain.release(); + releaseRenderTargets(); + mCompositor.release(); // Shut down OpenGL if (gViewerWindow) @@ -2155,8 +2469,6 @@ bool LLAppViewer::cleanup() LLWatchdog::getInstance()->cleanup(); - LLViewerAssetStatsFF::cleanup(); - // If we're exiting to launch an URL, do that here so the screen // is at the right resolution before we launch IE. if (!gLaunchFileOnQuit.empty()) @@ -2342,7 +2654,7 @@ void errorHandler(const std::string& title_string, const std::string& message_st } if (!message_string.empty()) { - if (on_main_thread()) + if (on_viewer_thread()) { // Prevent watchdog from killing us while dialog is up. // Can't do pauseMainloopTimeout, since this may be called @@ -3400,12 +3712,31 @@ bool LLAppViewer::initWindow() stop_glerror(); gViewerWindow->initGLDefaults(); - // Stand up the presentation swap chain now that GL is up. From here on the - // viewer acquires an image from this chain each frame instead of touching - // FBO 0 directly. - mSwapChain.attachToWindow(gViewerWindow->getWindow(), - gViewerWindow->getWindowWidthRaw(), - gViewerWindow->getWindowHeightRaw()); + // Stand up the compositor on OS main and register ourselves as the + // compositable. The render targets get allocated later by the viewer + // thread once its own context is current. + { + const U32 w = gViewerWindow->getWindowWidthRaw(); + const U32 h = gViewerWindow->getWindowHeightRaw(); + mCompositor.attachToWindow(gViewerWindow->getWindow(), w, h); + mCompositor.addCompositable(this); + + // Shader objects are shared across the context share group, so + // the compositor can use this program from the OS main thread. + mCompositor.setBlitShader(&gCompositorBlitProgram); + + // Sync ticks feed the vsync pacing gate in viewerThreadTick. + mSyncConnection = mCompositor.onSync( + [this](U64 frame_index) { mSyncTicks.tryPush(frame_index); }); + + // Seed the vsync mode; the compositor applies the interval on + // its own context at the first present. + setVSyncMode(gSavedSettings.getU32("RenderVSyncMode")); + + // Seed the refresh overlay (Develop > Show Info > Show + // Compositor Refresh). + mCompositor.setShowRefreshOverlay(gSavedSettings.getBOOL("RenderCompositorShowRefresh")); + } gSavedSettings.setBOOL("RenderInitError", false); gSavedSettings.saveToFile( gSavedSettings.getString("ClientSettingsFile"), true ); @@ -5171,6 +5502,14 @@ void LLAppViewer::idle() std::chrono::nanoseconds::rep(MainWorkTimeMs.value() * 1000000)}; gMainloopWork.runFor(MainWorkTimeNanoSec); + // Drain gViewerWork here while no viewer thread exists yet - + // texture-sync callbacks start posting to it early in init, and + // somebody has to process them or texture cache init stalls. + if (!mViewerThread) + { + gViewerWork.runFor(MainWorkTimeNanoSec); + } + // Cap out-of-control frame times // Too low because in menus, swapping, debugger, etc. // Too high because idle called with no objects in view, etc. diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index a6b4148b370..4792936b758 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -49,9 +49,15 @@ #include "llsys.h" // for LLOSInfo #include "lltimer.h" #include "llappcorehttp.h" -#include "llswapchain.h" +#include "llcompositor.h" +#include "llcompositable.h" +#include "llrendertarget.h" +#include "llthreadsafequeue.h" #include "threadpool_fwd.h" +#include +#include + #include class LLCommandLineParser; @@ -84,7 +90,9 @@ typedef enum LAST_EXEC_COUNT } eLastExecEvent; -class LLAppViewer : public LLApp +// LLAppViewer renders the world and UI into its own render targets, and +// the compositor picks them up through the LLCompositable interface. +class LLAppViewer : public LLApp, public LLCompositable { public: LLAppViewer(); @@ -150,11 +158,80 @@ class LLAppViewer : public LLApp std::string getSecondLifeTitle() const; // The Second Life title. std::string getWindowTitle() const; // The window display name. - // The OS window's presentation swap chain. Owns the LLRenderTarget(s) that - // back FBO 0 (or the equivalent on future Vk/XR backends). Viewer code - // acquires an image from this chain per frame instead of touching FBO 0 - // directly. - LLSwapChain& getSwapChain() { return mSwapChain; } + // The compositor owns the swap chain and presents us each frame. + LLCompositor& getCompositor() { return mCompositor; } + + // Platform-detected refresh rate of the display the window is on + // (LLWindow::getRefreshRate). The one number everything that shows + // or budgets against "the refresh rate" uses. Returns 0 if there's + // no window yet. + S32 getDisplayRefreshRate() const; + + // THE gate for vsync mode changes (RenderVSyncMode). Updates the + // autotune state, hands the swap interval to the compositor (which + // applies it on its own context), and everyone else adapts to the + // resulting sync cadence. + void setVSyncMode(U32 mode); + LLRenderTarget& getScratchParent() { return mScratchParent; } + + // Triple-buffer mailbox between the viewer thread and the compositor. + // Each buffer lives in exactly one slot at a time: + // producer's mBackIdx | mLatestIdx slot | mFreeIdx slot | + // compositor's mDisplayIdx + // We render into the back buffer and publish it to mLatestIdx; the + // compositor claims it in tryAcquireNewFront and hands its old display + // buffer back through mFreeIdx. Neither side waits on the other, and + // the latest published frame always wins. + LLRenderTarget& getBackBuffer() { return mRTs[mBackIdx]; } + + // True when the back buffer is allocated and ready to render into. + bool isBackBufferReady() const { return mRTs[mBackIdx].isComplete(); } + + // Sizing. + void allocateRenderTargets(U32 width, U32 height); + void resizeRenderTargets(U32 width, U32 height); + void releaseRenderTargets(); + + // Called at the end of renderViewerFrame(). Publishes the back buffer + // into the mailbox and picks the next one. Call this at most once per + // frame, otherwise we publish a buffer we never rendered into. + void markFrameRendered(); + + // Render paths call this once they've actually drawn into the back + // buffer; renderViewerFrame only publishes when set. The tag tells us + // which path drew, for diagnostics. Viewer thread only. + void setBackBufferRendered(const char* who) + { + mBackBufferRendered = true; + mBackBufferRenderedBy = who; + } + + // Runs display() plus the post-display updaters (reflection probes, + // snapshot floaters), then publishes the frame for the compositor. + // Viewer thread only. + void renderViewerFrame(); + + // One full viewer tick: idle() followed by renderViewerFrame(). The + // viewer thread runs this in a loop; without a viewer thread, doFrame + // calls it inline. + void viewerThreadTick(); + + // The tail of the old main loop: final snapshot, voice and service + // pump teardown, watchdog destruction. The viewer thread runs this + // on its way out of run(); without a viewer thread, doFrame calls + // it when exiting. + void viewerThreadShutdown(); + + // - LLCompositable --------------------------------------------------- + // Only the compositor touches mDisplayIdx. + LLRenderTarget& frontBuffer() override { return mRTs[mDisplayIdx]; } + std::string compositableName() const override { return "World"; } + + // Compositor thread: grab the latest published buffer if there is + // one, and hand the old display buffer back for reuse. + bool tryAcquireNewFront() override; + +public: void forceDisconnect(const std::string& msg); // Force disconnection, with a message to the user. @@ -380,7 +457,30 @@ class LLAppViewer : public LLApp S32 mNumSessions; std::string mSerialNumber; - LLSwapChain mSwapChain; + LLCompositor mCompositor; + // Triple-buffer mailbox (see getBackBuffer for how it works). + // Compositor-paced (when vsync is on) vs free-running (vsync off) + // is just whether the producer waits in markFrameRendered. + void resetMailbox(); + LLRenderTarget mRTs[3]; + U32 mBackIdx{0}; // producer-owned (viewer thread) + bool mBackBufferRendered{false}; // viewer-thread-only render gate + const char* mBackBufferRenderedBy{"none"}; // which path set the gate + std::atomic mLatestIdx{-1}; // mailbox slot; -1 = empty + std::atomic mFreeIdx{1}; // recycle slot; -1 = empty + S32 mDisplayIdx{2}; // compositor-owned + LLRenderTarget mScratchParent; // wrap parent for cubeSnapshot raw clear + + // Compositor sync ticks for vsync pacing (RenderVSyncMode). The + // sync handler pushes from OS main, the viewer thread pops; tryPush + // drops when we're behind, so neither side ever blocks the other. + LLThreadSafeQueue mSyncTicks{16}; + boost::signals2::connection mSyncConnection; + + // The viewer thread. Runs viewerThreadTick in a loop with its own + // shared GL context. Spawned at the end of init(), joined in cleanup() + // before window teardown. nullptr when we're running single threaded. + std::unique_ptr mViewerThread; bool mPurgeCache; bool mPurgeCacheOnExit; bool mPurgeUserDataOnExit; diff --git a/indra/newview/lldrawpoolbump.cpp b/indra/newview/lldrawpoolbump.cpp index 8f451772360..fa5b3f08180 100644 --- a/indra/newview/lldrawpoolbump.cpp +++ b/indra/newview/lldrawpoolbump.cpp @@ -688,7 +688,7 @@ void LLBumpImageList::addTextureStats(U8 bump, const LLUUID& base_image_id, F32 void LLBumpImageList::updateImages() { - llassert(LLCoros::on_main_thread_main_coro()); // This code is not thread safe + llassert(LLCoros::on_viewer_thread_main_coro()); // This code is not thread safe for (bump_image_map_t::iterator iter = mBrightnessEntries.begin(); iter != mBrightnessEntries.end(); ) { diff --git a/indra/newview/llfloatermodelpreview.cpp b/indra/newview/llfloatermodelpreview.cpp index ef29be8200c..68fa4fb1c37 100644 --- a/indra/newview/llfloatermodelpreview.cpp +++ b/indra/newview/llfloatermodelpreview.cpp @@ -1682,7 +1682,7 @@ void LLFloaterModelPreview::addStringToLogTab(const std::string& str, bool flash void LLFloaterModelPreview::setDetails(F32 x, F32 y, F32 z) { - assert_main_thread(); + assert_viewer_thread(); childSetTextArg("import_dimensions", "[X]", llformat("%.3f", x)); childSetTextArg("import_dimensions", "[Y]", llformat("%.3f", y)); childSetTextArg("import_dimensions", "[Z]", llformat("%.3f", z)); @@ -1698,7 +1698,7 @@ void LLFloaterModelPreview::setPreviewLOD(S32 lod) void LLFloaterModelPreview::onBrowseLOD(S32 lod) { - assert_main_thread(); + assert_viewer_thread(); loadModel(lod); } @@ -1706,7 +1706,7 @@ void LLFloaterModelPreview::onBrowseLOD(S32 lod) //static void LLFloaterModelPreview::onReset(void* user_data) { - assert_main_thread(); + assert_viewer_thread(); LLFloaterModelPreview* fmp = (LLFloaterModelPreview*) user_data; @@ -1728,7 +1728,7 @@ void LLFloaterModelPreview::onReset(void* user_data) //static void LLFloaterModelPreview::onUpload(void* user_data) { - assert_main_thread(); + assert_viewer_thread(); LLFloaterModelPreview* mp = (LLFloaterModelPreview*) user_data; mp->clearLogTab(); @@ -1984,14 +1984,14 @@ void LLFloaterModelPreview::setModelPhysicsFeeErrorStatus(S32 status, const std: /*virtual*/ void LLFloaterModelPreview::onModelUploadSuccess() { - assert_main_thread(); + assert_viewer_thread(); closeFloater(false); } /*virtual*/ void LLFloaterModelPreview::onModelUploadFailure() { - assert_main_thread(); + assert_viewer_thread(); toggleCalculateButton(true); mUploadBtn->setEnabled(true); } diff --git a/indra/newview/llfloaterperformance.cpp b/indra/newview/llfloaterperformance.cpp index 3eb0c849d3a..8696c38a122 100644 --- a/indra/newview/llfloaterperformance.cpp +++ b/indra/newview/llfloaterperformance.cpp @@ -29,12 +29,14 @@ #include "llagent.h" #include "llagentcamera.h" #include "llappearancemgr.h" +#include "llappviewer.h" #include "llavataractions.h" #include "llavatarrendernotifier.h" #include "llcheckboxctrl.h" #include "llcombobox.h" #include "llfeaturemanager.h" #include "llfloaterpreference.h" // LLAvatarComplexityControls +#include "llfloaterpreferencesgraphicsadvanced.h" // populateVSyncCombo #include "llfloaterreg.h" #include "llnamelistctrl.h" #include "llnotificationsutil.h" @@ -189,6 +191,15 @@ void LLFloaterPerformance::showAutoadjustmentsPanel() showSelectedPanel(mAutoadjustmentsPanel); } +void LLFloaterPerformance::onOpen(const LLSD& key) +{ + LLFloater::onOpen(key); + // Mirror the advanced-graphics vsync dropdown: refresh-rate-aware + // labels, re-detected each open. + LLFloaterPreferenceGraphicsAdvanced::populateVSyncCombo( + mAutoadjustmentsPanel->getChild("vsync_mode")); +} + void LLFloaterPerformance::draw() { enableAutotuneWarning(); @@ -518,9 +529,10 @@ void LLFloaterPerformance::setFPSText() mTextFPSValue->setValue(current_fps); std::string fps_text = getString("fps_text"); - static LLCachedControl vsync_enabled(gSavedSettings, "RenderVSyncEnable", true); - S32 refresh_rate = gViewerWindow->getWindow()->getRefreshRate(); - if (vsync_enabled && (refresh_rate > 0) && (current_fps >= refresh_rate)) + static LLCachedControl vsync_mode(gSavedSettings, "RenderVSyncMode", 1); + const U32 mode = llclamp((U32)vsync_mode, 0u, 4u); + S32 refresh_rate = LLAppViewer::instance()->getDisplayRefreshRate(); + if (mode > 0 && (refresh_rate > 0) && (current_fps >= refresh_rate / (S32)mode)) { fps_text += getString("max_text"); } diff --git a/indra/newview/llfloaterperformance.h b/indra/newview/llfloaterperformance.h index 6cca85a009c..d66bed5b302 100644 --- a/indra/newview/llfloaterperformance.h +++ b/indra/newview/llfloaterperformance.h @@ -41,6 +41,7 @@ class LLFloaterPerformance : public LLFloater virtual ~LLFloaterPerformance(); /*virtual*/ bool postBuild(); + /*virtual*/ void onOpen(const LLSD& key); /*virtual*/ void draw(); void showSelectedPanel(LLPanel* selected_panel); diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index 537dca99da4..619454c056b 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -878,7 +878,7 @@ void LLFloaterPreference::setHardwareDefaults() void LLFloaterPreference::setRecommendedSettings() { resetAutotuneSettings(); - gSavedSettings.getControl("RenderVSyncEnable")->resetToDefault(true); + gSavedSettings.getControl("RenderVSyncMode")->resetToDefault(true); LLFeatureManager::getInstance()->applyRecommendedSettings(); diff --git a/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp b/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp index a8a1e507a85..1038b831b8c 100644 --- a/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp +++ b/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp @@ -36,9 +36,12 @@ #include "llsliderctrl.h" #include "lltextbox.h" #include "lltrans.h" +#include "llappviewer.h" #include "llviewershadermgr.h" #include "llviewertexturelist.h" +#include "llviewerwindow.h" #include "llvoavatar.h" +#include "llwindow.h" #include "pipeline.h" @@ -111,9 +114,37 @@ bool LLFloaterPreferenceGraphicsAdvanced::postBuild() void LLFloaterPreferenceGraphicsAdvanced::onOpen(const LLSD& key) { + refreshVSyncLabels(); refresh(); } +void LLFloaterPreferenceGraphicsAdvanced::refreshVSyncLabels() +{ + populateVSyncCombo(getChild("vsync_mode")); +} + +// static +void LLFloaterPreferenceGraphicsAdvanced::populateVSyncCombo(LLComboBox* combo) +{ + // Label each mode with the refresh rate it targets - the display's + // rate divided by the mode (swap interval). Refreshed on every open + // since the rate can change (different monitor, display settings). + // On a dynamic-refresh display these are the upper bound per mode, + // since the OS varies the real rate. If we can't detect a rate + // (non-Windows, pre-detection) the static XML labels stand. + S32 refresh = LLAppViewer::instance()->getDisplayRefreshRate(); + if (refresh > 0) + { + combo->clearRows(); + combo->add("Off", LLSD(0)); + combo->add(llformat("%dHz", refresh), LLSD(1)); + combo->add(llformat("%dHz", refresh / 2), LLSD(2)); + combo->add(llformat("%dHz", refresh / 3), LLSD(3)); + combo->add(llformat("%dHz", refresh / 4), LLSD(4)); + combo->setValue(LLSD((S32)gSavedSettings.getU32("RenderVSyncMode"))); + } +} + void LLFloaterPreferenceGraphicsAdvanced::onClickCloseBtn(bool app_quitting) { LLFloaterPreference* instance = LLFloaterReg::findTypedInstance("preferences"); diff --git a/indra/newview/llfloaterpreferencesgraphicsadvanced.h b/indra/newview/llfloaterpreferencesgraphicsadvanced.h index 6f793c13799..535e87478e1 100644 --- a/indra/newview/llfloaterpreferencesgraphicsadvanced.h +++ b/indra/newview/llfloaterpreferencesgraphicsadvanced.h @@ -29,6 +29,7 @@ #include "llcontrol.h" #include "llfloater.h" +class LLComboBox; class LLSliderCtrl; class LLTextBox; @@ -43,6 +44,10 @@ class LLFloaterPreferenceGraphicsAdvanced : public LLFloater void disableUnavailableSettings(); void refreshEnabledGraphics(); void refreshEnabledState(); + void refreshVSyncLabels(); + // Fill a "vsync_mode" combo with refresh-rate-aware labels. Shared + // with the performance floater so both surfaces read identically. + static void populateVSyncCombo(LLComboBox* combo); void updateSliderText(LLSliderCtrl* ctrl, LLTextBox* text_box); void updateMaxNonImpostors(); void updateIndirectMaxNonImpostors(const LLSD& newvalue); diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index b2e21bea1ca..a9ef49d37c4 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -238,7 +238,7 @@ LLModelPreview::~LLModelPreview() void LLModelPreview::updateDimentionsAndOffsets() { - assert_main_thread(); + assert_viewer_thread(); rebuildUploadData(); @@ -283,7 +283,7 @@ void LLModelPreview::updateDimentionsAndOffsets() void LLModelPreview::rebuildUploadData() { - assert_main_thread(); + assert_viewer_thread(); mDefaultPhysicsShapeP = NULL; mUploadData.clear(); @@ -742,7 +742,7 @@ void LLModelPreview::getJointAliases(JointMap& joint_map) void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable_slm) { - assert_main_thread(); + assert_viewer_thread(); LLMutexLock lock(this); @@ -878,7 +878,7 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable void LLModelPreview::setPhysicsFromLOD(S32 lod) { - assert_main_thread(); + assert_viewer_thread(); if (lod >= 0 && lod <= 3) { @@ -969,7 +969,7 @@ void LLModelPreview::clearIncompatible(S32 lod) void LLModelPreview::loadModelCallback(S32 loaded_lod) { - assert_main_thread(); + assert_viewer_thread(); LLMutexLock lock(this); if (!mModelLoader) @@ -1294,7 +1294,7 @@ void LLModelPreview::resetPreviewTarget() void LLModelPreview::generateNormals() { - assert_main_thread(); + assert_viewer_thread(); S32 which_lod = mPreviewLOD; @@ -2189,7 +2189,7 @@ void LLModelPreview::updateStatusMessages() TOOMANYVERTSINHULL = 8 }; - assert_main_thread(); + assert_viewer_thread(); U32 has_physics_error{ PhysicsError::NONE }; // physics error bitmap //triangle/vertex/submesh count for each mesh asset for each lod @@ -3284,7 +3284,7 @@ void LLModelPreview::addEmptyFace(LLModel* pTarget) // Note: Render happens each frame with skinned avatars bool LLModelPreview::render() { - assert_main_thread(); + assert_viewer_thread(); LLMutexLock lock(this); mNeedsUpdate = false; diff --git a/indra/newview/llpbrterrainfeatures.cpp b/indra/newview/llpbrterrainfeatures.cpp index d652e23dd5d..2cc7ce4bbe0 100644 --- a/indra/newview/llpbrterrainfeatures.cpp +++ b/indra/newview/llpbrterrainfeatures.cpp @@ -37,7 +37,7 @@ LLPBRTerrainFeatures gPBRTerrainFeatures; // static void LLPBRTerrainFeatures::queueQuery(LLViewerRegion& region, void(*done_callback)(LLUUID, bool, const LLModifyRegion&)) { - llassert(on_main_thread()); + llassert(on_viewer_thread()); llassert(LLCoros::on_main_coro()); LLUUID region_id = region.getRegionID(); @@ -52,7 +52,7 @@ void LLPBRTerrainFeatures::queueQuery(LLViewerRegion& region, void(*done_callbac // static void LLPBRTerrainFeatures::queueModify(LLViewerRegion& region, const LLModifyRegion& composition) { - llassert(on_main_thread()); + llassert(on_viewer_thread()); llassert(LLCoros::on_main_coro()); LLSD updates = LLSD::emptyMap(); diff --git a/indra/newview/llperfstats.cpp b/indra/newview/llperfstats.cpp index 37bb59a65c3..0adb75519c5 100644 --- a/indra/newview/llperfstats.cpp +++ b/indra/newview/llperfstats.cpp @@ -67,7 +67,7 @@ namespace LLPerfStats void Tunables::applyUpdates() { - assert_main_thread(); + assert_viewer_thread(); // these following variables are proxies for pipeline statics we do not need a two way update (no llviewercontrol handler) if( tuningFlag & NonImpostors ){ gSavedSettings.setU32("RenderAvatarMaxNonImpostors", nonImpostors); }; if( tuningFlag & ReflectionDetail ){ gSavedSettings.setS32("RenderReflectionDetail", reflectionDetail); }; @@ -87,7 +87,7 @@ namespace LLPerfStats void Tunables::updateRenderCostLimitFromSettings() { - assert_main_thread(); + assert_viewer_thread(); const auto newval = gSavedSettings.getF32("RenderAvatarMaxART"); if(newval < log10(LLPerfStats::ART_UNLIMITED_NANOS/1000)) { @@ -117,7 +117,7 @@ namespace LLPerfStats void Tunables::initialiseFromSettings() { - assert_main_thread(); + assert_viewer_thread(); // the following variables are two way and have "push" in llviewercontrol LLPerfStats::tunables.userMinDrawDistance = gSavedSettings.getF32("AutoTuneRenderFarClipMin"); LLPerfStats::tunables.userTargetDrawDistance = gSavedSettings.getF32("AutoTuneRenderFarClipTarget"); @@ -125,7 +125,7 @@ namespace LLPerfStats LLPerfStats::tunables.userImpostorDistanceTuningEnabled = gSavedSettings.getBOOL("AutoTuneImpostorByDistEnabled"); LLPerfStats::tunables.userFPSTuningStrategy = gSavedSettings.getU32("TuningFPSStrategy"); LLPerfStats::tunables.userTargetFPS = gSavedSettings.getU32("TargetFPS"); - LLPerfStats::tunables.vsyncEnabled = gSavedSettings.getBOOL("RenderVSyncEnable"); + LLPerfStats::tunables.vsyncEnabled = gSavedSettings.getU32("RenderVSyncMode") > 0; LLPerfStats::tunables.userAutoTuneLock = gSavedSettings.getBOOL("AutoTuneLock") && gSavedSettings.getU32("KeepAutoTuneLock"); @@ -151,7 +151,10 @@ namespace LLPerfStats // create a queue tunables.initialiseFromSettings(); LLPerfStats::cpu_hertz = (F64)LLTrace::BlockTimer::countsPerSecond(); - LLPerfStats::vsync_max_fps = gViewerWindow->getWindow()->getRefreshRate(); + // Effective FPS cap = refresh / vsync mode (mode 0 = off, value + // unused). setVSyncMode keeps this current as the mode changes. + const U32 vsync_mode = llclamp(gSavedSettings.getU32("RenderVSyncMode"), 1u, 4u); + LLPerfStats::vsync_max_fps = gViewerWindow->getWindow()->getRefreshRate() / vsync_mode; } // static diff --git a/indra/newview/llperfstats.h b/indra/newview/llperfstats.h index 38deb872377..e5c87484770 100644 --- a/indra/newview/llperfstats.h +++ b/indra/newview/llperfstats.h @@ -61,6 +61,7 @@ namespace LLPerfStats static constexpr U32 TUNE_SCENE_ONLY{2}; extern F64 cpu_hertz; + extern U32 vsync_max_fps; // effective FPS cap when vsync is on (refresh / mode) extern std::atomic tunedAvatars; extern std::atomic renderAvatarMaxART_ns; diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index be5bbe06fb5..10cf733e783 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -313,10 +313,10 @@ void LLSceneMonitor::capture() gGL.getTexUnit(0)->bind(&cur_target); - // Read from the most recently presented swap chain image -- i.e. the - // frame currently on screen. The current image is mid-render at this - // point; the previous one is what the user sees. - LLRenderTarget& src = LLAppViewer::instance()->getSwapChain().getPreviousImage(); + // Read from the most recently presented swap chain image - the + // frame the user is actually seeing. The compositor owns the + // swap chain now. + LLRenderTarget& src = const_cast(LLAppViewer::instance()->getCompositor().getSwapChain()).getPreviousImage(); src.bindForRead(); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, cur_target.getWidth(), cur_target.getHeight()); //copy the content diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index cd4c9d40b1f..04ee83d582d 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -277,7 +277,10 @@ #include "threadpool.h" #include "llperfstats.h" +#include "workqueue.h" +// Work that has to run on the OS main thread. Defined in llappviewer.cpp. +extern LL::WorkQueue gOSMainWork; #if LL_WINDOWS #include "lldxhardware.h" @@ -976,9 +979,10 @@ bool idle_startup() gLoginMenuBarView->setEnabled( true ); show_debug_menus(); - // Hide the splash screen + // Hide the splash screen. The splash window belongs to the OS + // main thread, so the hide has to happen over there. LL_DEBUGS("AppInit") << "Hide the splash screen and show window" << LL_ENDL; - LLSplashScreen::hide(); + gOSMainWork.post([]{ LLSplashScreen::hide(); }); // Push our window frontmost gViewerWindow->getWindow()->show(); diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 0c93b247510..d7420addd5d 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -300,20 +300,15 @@ static bool handleAnisotropicChanged(const LLSD& newvalue) return true; } -static bool handleVSyncChanged(const LLSD& newvalue) +static bool handleVSyncModeChanged(const LLSD& newvalue) { - LLPerfStats::tunables.vsyncEnabled = newvalue.asBoolean(); - if (gViewerWindow && gViewerWindow->getWindow()) - { - gViewerWindow->getWindow()->toggleVSync(newvalue.asBoolean()); - - if (newvalue.asBoolean()) - { - U32 current_target = gSavedSettings.getU32("TargetFPS"); - gSavedSettings.setU32("TargetFPS", std::min((U32)gViewerWindow->getWindow()->getRefreshRate(), current_target)); - } - } + LLAppViewer::instance()->setVSyncMode((U32)newvalue.asInteger()); + return true; +} +static bool handleCompositorShowRefreshChanged(const LLSD& newvalue) +{ + LLAppViewer::instance()->getCompositor().setShowRefreshOverlay(newvalue.asBoolean()); return true; } @@ -855,7 +850,8 @@ void settings_setup_listeners() setting_setup_signal_listener(gSavedSettings, "RenderFogRatio", handleFogRatioChanged); setting_setup_signal_listener(gSavedSettings, "RenderMaxPartCount", handleMaxPartCountChanged); setting_setup_signal_listener(gSavedSettings, "RenderDynamicLOD", handleRenderDynamicLODChanged); - setting_setup_signal_listener(gSavedSettings, "RenderVSyncEnable", handleVSyncChanged); + setting_setup_signal_listener(gSavedSettings, "RenderVSyncMode", handleVSyncModeChanged); + setting_setup_signal_listener(gSavedSettings, "RenderCompositorShowRefresh", handleCompositorShowRefreshChanged); setting_setup_signal_listener(gSavedSettings, "RenderDeferredNoise", handleReleaseGLBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderDebugPipeline", handleRenderDebugPipelineChanged); setting_setup_signal_listener(gSavedSettings, "RenderResolutionDivisor", handleRenderResolutionDivisorChanged); diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 882d1d41f7e..d1a18490004 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -168,11 +168,20 @@ void display_startup() LLGLState::checkStates(); - // Route startup-screen rendering through the swap chain so we never name - // FBO 0 here, AND so dynamic-texture updates below have a parent on the - // RT stack when they flush. - LLSwapChain& startup_sc = LLAppViewer::instance()->getSwapChain(); - startup_sc.acquireNextImage().bindTarget(); + // This function is invoked a good bit during startup. Ordering is + // important here, in order to avoid any cross threaded asserts + // since we're now running the viewer in its own thread. Skip the + // inline render when the back buffer is not yet ready for us. + if (!LLAppViewer::instance()->isBackBufferReady()) + { + return; + } + + // Render the startup screen into our back buffer. The dynamic + // texture updates below also need a parent on the RT stack when + // they flush. + LLRenderTarget& startup_rt = LLAppViewer::instance()->getBackBuffer(); + startup_rt.bindTarget(); if (frame_count++ > 1) // make sure we have rendered a frame first { @@ -199,11 +208,13 @@ void display_startup() LLGLState::checkStates(); - if (LLRenderTarget::getCurrentBoundTarget() == &startup_sc.getCurrentImage()) + if (LLRenderTarget::getCurrentBoundTarget() == &startup_rt) { - startup_sc.getCurrentImage().flush(); + startup_rt.flush(); } - startup_sc.present(); + // Just set the flag; the actual publish happens at the end of + // renderViewerFrame. + LLAppViewer::instance()->setBackBufferRendered("startup"); glClear(GL_DEPTH_BUFFER_BIT); } @@ -431,18 +442,9 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) LL_DEBUGS("Window") << "Resizing window" << LL_ENDL; LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Resize Window"); gGL.flush(); - // Route the resize-skip black clear through the swap chain so we - // don't reach for FBO 0 here either. Only run when no other RT is on - // the stack -- snapshots set their own bottom-of-stack target. - LLSwapChain& sc = LLAppViewer::instance()->getSwapChain(); - if (LLRenderTarget::getCurrentBoundTarget() == nullptr) - { - LLRenderTarget& img = sc.acquireNextImage(); - img.bindTarget(); - img.clear(GL_COLOR_BUFFER_BIT); - img.flush(); - sc.present(); - } + // Don't publish anything this tick - leave the back buffer and + // the rendered flag alone, and the compositor will keep showing + // the last good frame instead of a black one. LLPipeline::refreshCachedSettings(); gPipeline.resizeScreenTexture(); gResizeScreenTexture = false; @@ -714,17 +716,28 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) // Actually push all of our triangles to the screen. // - // Bind the OS window's presentation image as the bottom of the render - // target stack for this frame. Everything downstream -- dynamic-texture - // updates, pipeline G-buffers, lighting, post-proc ping-pongs, - // renderFinalize's fullscreen triangle, and the UI -- sits on top of this - // in the LLRenderTarget stack and pops back to it on flush. swap() - // flushes the image and calls present(). Snapshot renders go to their - // own off-screen target, so we skip the bind in that case. - LLSwapChain& swap_chain = LLAppViewer::instance()->getSwapChain(); - if (!for_snapshot && LLRenderTarget::getCurrentBoundTarget() == nullptr) + // Nothing renders on the way out. The world render below is gated + // on isExiting anyway, but it would leave the back buffer bound + // with nothing to flush it - so don't bind it in the first place. + // The compositor keeps presenting the last published frame, and + // the final login-screen snapshot reads that same content. + if (LLApp::isExiting()) { - swap_chain.acquireNextImage().bindTarget(); + return; + } + + // Bind our back buffer as the bottom of the render target stack for + // this frame. Everything downstream renders on top of it and pops + // back to it on flush. Snapshots render to their own off-screen + // target, so we skip the bind in that case. Nothing should still be + // bound from a previous frame at this point - hence the assert. + if (!for_snapshot) + { + llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) + { + LLAppViewer::instance()->getBackBuffer().bindTarget(); + } } // do render-to-texture stuff here @@ -1586,24 +1599,25 @@ void render_ui(F32 zoom_factor, int subfield) void swap() { - LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SWAP ); // render time capture - Swap buffer time - can signify excessive data transfer to/from GPU + LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SWAP ); LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Swap"); - LL_PROFILE_GPU_ZONE("swap"); - // Flush the swap chain's current image off the RT stack before presenting. - // The OS framebuffer stays bound (that's what present needs); this just - // pops the LLRenderTarget bookkeeping. - LLSwapChain& swap_chain = LLAppViewer::instance()->getSwapChain(); - if (LLRenderTarget::getCurrentBoundTarget() == &swap_chain.getCurrentImage()) - { - swap_chain.getCurrentImage().flush(); - } + // Flush the back buffer off the RT stack and mark a fresh frame + // ready. The actual present happens later in doFrame. + LLRenderTarget& world_rt = LLAppViewer::instance()->getBackBuffer(); + + // By the time we get here, display() should have left our back + // buffer at the bottom of the stack. If not, something upstream + // didn't pop its render targets. + llassert(LLRenderTarget::getCurrentBoundTarget() == &world_rt); - if (gDisplaySwapBuffers) + if (LLRenderTarget::getCurrentBoundTarget() == &world_rt) { - swap_chain.present(); + world_rt.flush(); } - gDisplaySwapBuffers = true; + // Just set the flag here - we only publish once per frame, at the + // end of renderViewerFrame. Publishing twice gets us stale frames. + LLAppViewer::instance()->setBackBufferRendered("world"); } void renderCoordinateAxes() diff --git a/indra/newview/llviewerdisplay.h b/indra/newview/llviewerdisplay.h index 673f51600d8..20e4de2da97 100644 --- a/indra/newview/llviewerdisplay.h +++ b/indra/newview/llviewerdisplay.h @@ -34,6 +34,9 @@ void display_cleanup(); void display(bool rebuild = true, F32 zoom_factor = 1.f, int subfield = 0, bool for_snapshot = false); +// Viewer thread only. Snapshot code clears this so its readback-only +// frames don't get published; renderViewerFrame's publish gate reads +// and resets it. extern bool gDisplaySwapBuffers; extern bool gDepthDirty; extern bool gTeleportDisplay; @@ -41,6 +44,8 @@ extern LLFrameTimer gTeleportDisplayTimer; extern bool gForceRenderLandFence; extern bool gResizeScreenTexture; extern bool gResizeShadowTexture; +// Viewer thread only - the resize callback lands here via the window +// function queue that gatherInput drains. extern bool gWindowResized; #endif // LL_LLVIEWERDISPLAY_H diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 27865f75980..3b23af58ae1 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -81,6 +81,7 @@ LLGLSLShader gOcclusionProgram; LLGLSLShader gSkinnedOcclusionProgram; LLGLSLShader gOcclusionCubeProgram; LLGLSLShader gGlowCombineProgram; +LLGLSLShader gCompositorBlitProgram; LLGLSLShader gReflectionMipProgram; LLGLSLShader gGaussianProgram; LLGLSLShader gRadianceGenProgram; @@ -3228,6 +3229,16 @@ bool LLViewerShaderMgr::loadShadersInterface() success = gPathfindingNoNormalsProgram.createShader(); } + if (success) + { + gCompositorBlitProgram.mName = "Compositor Blit Shader"; + gCompositorBlitProgram.mShaderFiles.clear(); + gCompositorBlitProgram.mShaderFiles.push_back(make_pair("interface/compositorblitV.glsl", GL_VERTEX_SHADER)); + gCompositorBlitProgram.mShaderFiles.push_back(make_pair("interface/compositorblitF.glsl", GL_FRAGMENT_SHADER)); + gCompositorBlitProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gCompositorBlitProgram.createShader(); + } + if (success) { gGlowCombineProgram.mName = "Glow Combine Shader"; diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index 46da30017db..124b3813194 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -154,6 +154,7 @@ extern LLVector4 gShinyOrigin; extern LLGLSLShader gOcclusionProgram; extern LLGLSLShader gOcclusionCubeProgram; extern LLGLSLShader gGlowCombineProgram; +extern LLGLSLShader gCompositorBlitProgram; extern LLGLSLShader gReflectionMipProgram; extern LLGLSLShader gGaussianProgram; extern LLGLSLShader gRadianceGenProgram; diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 27c8d970730..6770b70d7bf 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -1209,7 +1209,7 @@ void LLViewerFetchedTexture::init(bool firstinit) LLViewerFetchedTexture::~LLViewerFetchedTexture() { - assert_main_thread(); + assert_viewer_thread(); //*NOTE getTextureFetch can return NULL when Viewer is shutting down. // This is due to LLWearableList is singleton and is destroyed after // LLAppViewer::cleanup() was called. (see ticket EXT-177) @@ -2924,8 +2924,8 @@ void LLViewerFetchedTexture::readbackRawImage() LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; // readback the raw image from vram if the current raw image is null or smaller than the texture - // Split conditions so each lease op (getHasGLTexture, getWidth, getHeight) - // runs sequentially -- avoids nested shared leases on the same mutex. + // Each of these calls takes its own lease, so keep them sequential + // rather than nested. if (mGLTexturep.notNull() && mGLTexturep->getHasGLTexture() && (mRawImage.isNull() || mRawImage->getWidth() < mGLTexturep->getWidth() || mRawImage->getHeight() < mGLTexturep->getHeight() )) { diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 2d0e32d25f8..d12d02118ac 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -696,7 +696,7 @@ LLViewerFetchedTexture *LLViewerTextureList::findImage(const LLUUID &image_id, E void LLViewerTextureList::addImageToList(LLViewerFetchedTexture *image) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - assert_main_thread(); + assert_viewer_thread(); llassert_always(mInitialized) ; llassert(image); if (image->isInImageList()) @@ -716,7 +716,7 @@ void LLViewerTextureList::addImageToList(LLViewerFetchedTexture *image) void LLViewerTextureList::removeImageFromList(LLViewerFetchedTexture *image) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - assert_main_thread(); + assert_viewer_thread(); llassert_always(mInitialized) ; llassert(image); @@ -1175,17 +1175,16 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) LLGLDisable blend(GL_BLEND); gGL.setColorMask(true, true); - // Give mDownResMap a parent on the RT stack so its flush below pops - // back to the swap chain image instead of falling off the bottom. - // This pump can be called from idle() (no parent bound) or from - // display() (swap chain image already bound) -- only wrap when the - // stack is empty. - LLSwapChain& dr_sc = LLAppViewer::instance()->getSwapChain(); - bool dr_sc_bound = false; + // Give mDownResMap a parent on the RT stack so its flush below + // pops back to our back buffer instead of falling off the + // bottom. We can get here from idle() with nothing bound, so + // only wrap when the stack is empty. + LLRenderTarget& world_rt = LLAppViewer::instance()->getBackBuffer(); + bool world_bound = false; if (LLRenderTarget::getCurrentBoundTarget() == nullptr) { - dr_sc.acquireNextImage().bindTarget(); - dr_sc_bound = true; + world_rt.bindTarget(); + world_bound = true; } // just in case we downres textures, bind downresmap and copy program @@ -1225,10 +1224,10 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) gCopyProgram.unbind(); gPipeline.mDownResMap.flush(); - if (dr_sc_bound && - LLRenderTarget::getCurrentBoundTarget() == &dr_sc.getCurrentImage()) + if (world_bound && + LLRenderTarget::getCurrentBoundTarget() == &world_rt) { - dr_sc.getCurrentImage().flush(); + world_rt.flush(); } } diff --git a/indra/newview/llviewerthread.cpp b/indra/newview/llviewerthread.cpp new file mode 100644 index 00000000000..153d999c76d --- /dev/null +++ b/indra/newview/llviewerthread.cpp @@ -0,0 +1,107 @@ +/** + * @file llviewerthread.cpp + * @brief LLViewerThread implementation. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llviewerthread.h" + +#include "llappviewer.h" +#include "llrender.h" +#include "llviewerwindow.h" +#include "llwindow.h" + +LLViewerThread::LLViewerThread(LLWindow* window) +: LLThread("Viewer"), + mWindow(window), + mContext(nullptr) +{ + llassert(mWindow != nullptr); + // The shared context has to be created on the OS main thread, + // which is where this constructor runs. + mContext = mWindow->createSharedContext(); + llassert(mContext != nullptr); +} + +LLViewerThread::~LLViewerThread() +{ + // run() normally destroys the context before the thread exits. If + // we still hold it here, the thread never started - drop it now. + if (mContext) + { + mWindow->destroySharedContext(mContext); + mContext = nullptr; + } +} + +void LLViewerThread::run() +{ + // Anchor the viewer thread identity before any render code asks + // about it. + set_viewer_thread(); + + // Make our shared GL context current. Everything we render from + // here on targets this context. + mWindow->makeContextCurrent(mContext); + + // gGL is thread_local, so this thread needs its own init. Pass + // true so we can build vertex buffers here too. + gGL.init(true); + + // Allocate our render targets now that our context is current. + // FBOs aren't shared between contexts, but textures are, which is + // how the compositor gets at our color buffers. + if (gViewerWindow) + { + const U32 w = gViewerWindow->getWindowWidthRaw(); + const U32 h = gViewerWindow->getWindowHeightRaw(); + LLAppViewer::instance()->allocateRenderTargets(w, h); + } + + LL_INFOS("Viewer") << "Viewer thread running, context current." << LL_ENDL; + + // The frame loop. Each tick runs input, idle, and the render, then + // waits on the compositor's present. On shutdown the compositor + // unblocks us and we fall out of the loop. + while (!isQuitting()) + { + LLAppViewer::instance()->viewerThreadTick(); + } + + LL_INFOS("Viewer") << "Viewer thread exiting, destroying context." + << LL_ENDL; + + // The old main loop's exit tail (final snapshot, voice, service + // pump, watchdog), run while the world and our GL context are still + // intact - cleanup is blocked joining us. + LLAppViewer::instance()->viewerThreadShutdown(); + + // Tear down per-thread gGL before destroying the context. + gGL.shutdown(); + + // The context has to be destroyed on the thread that created it. + mWindow->destroySharedContext(mContext); + mContext = nullptr; +} diff --git a/indra/newview/llviewerthread.h b/indra/newview/llviewerthread.h new file mode 100644 index 00000000000..9c8bb20b04d --- /dev/null +++ b/indra/newview/llviewerthread.h @@ -0,0 +1,61 @@ +/** + * @file llviewerthread.h + * @brief Dedicated thread that runs the viewer's frame loop. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLVIEWERTHREAD_H +#define LL_LLVIEWERTHREAD_H + +#include "llthread.h" + +class LLWindow; + +// Dedicated thread that runs the viewer's frame loop - the work that +// used to live on the OS main thread. It owns its own GL context, +// shared with the primary one, so textures are visible to both. The +// OS main thread keeps the message pump and the compositor. +// +// Construct and start() this on the OS main thread once the window's +// context is up. On shutdown, unblock the compositor first, then +// requestStop() and join via shutdown(). +class LLViewerThread : public LLThread +{ +public: + LLViewerThread(LLWindow* window); + ~LLViewerThread() override; + + // Flag the thread for shutdown. If we're blocked waiting on a + // present, call LLCompositor::requestShutdown() first to unstick + // us, then join via shutdown(). + void requestStop() { setQuitting(); } + +protected: + void run() override; + +private: + LLWindow* mWindow; + void* mContext; +}; + +#endif // LL_LLVIEWERTHREAD_H diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 57ff3a863ff..86b68ffe77d 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -224,6 +224,9 @@ extern bool gResizeScreenTexture; extern bool gCubeSnapshot; extern bool gSnapshotNoPost; +#include "workqueue.h" +extern LL::WorkQueue gMainloopWork; + LLViewerWindow *gViewerWindow = NULL; LLFrameTimer gAwayTimer; @@ -596,6 +599,28 @@ class LLDebugText { LLTrace::Recording& last_frame_recording = LLTrace::get_frame_recording().getLastRecording(); + // Compositor and per-compositable stats. addText stacks + // lines bottom-up, so we emit them in reverse order. + { + LLCompositor& compositor = LLAppViewer::instance()->getCompositor(); + std::vector layer_stats; + compositor.getLayerStats(layer_stats); + for (auto it = layer_stats.rbegin(); it != layer_stats.rend(); ++it) + { + addText(xpos, ypos, llformat(" %s: %.2f ms/%.0f FPS", + it->name.c_str(), + it->frameMs, + it->fps)); + ypos += y_inc; + } + addText(xpos, ypos, std::string("Compositables:")); + ypos += y_inc; + addText(xpos, ypos, llformat("Compositor: %.2f ms/%.0f FPS", + compositor.getLastPresentMs(), + compositor.getPresentFps())); + ypos += y_inc; + } + //show streaming cost/triangle count of known prims in current region OR selection { F32 cost = 0.f; @@ -944,6 +969,30 @@ class LLDebugText { LL_RECORD_BLOCK_TIME(FTM_DISPLAY_DEBUG_TEXT); + // Draw a dark backdrop behind the debug text so it stays + // readable against bright scenes, same as the texture console. + if (!mLineList.empty()) + { + const LLFontGL* font = LLFontGL::getFontMonospace(); + const S32 line_height = (S32)font->getLineHeight(); + S32 left = S32_MAX; + S32 right = S32_MIN; + S32 top = S32_MIN; + S32 bottom = S32_MAX; + for (line_list_t::iterator iter = mLineList.begin(); + iter != mLineList.end(); ++iter) + { + const Line& line = *iter; + left = llmin(left, line.x); + right = llmax(right, line.x + (S32)font->getWidth(line.text)); + top = llmax(top, line.y); + bottom = llmin(bottom, line.y - line_height); + } + const S32 pad = 4; + mBackColor.setAlpha(0.5f); + gl_rect_2d(left - pad, top + pad, right + pad, bottom - pad, mBackColor, true); + } + // Camera matrix text is hard to see again a white background // Add a dark background underneath the matrices for readability (contrast) if (mBackRectCamera1.mTop >= 0) @@ -1975,7 +2024,7 @@ LLViewerWindow::LLViewerWindow(const Params& p) p.title, p.name, p.x, p.y, p.width, p.height, 0, p.fullscreen, gHeadlessClient, - gSavedSettings.getBOOL("RenderVSyncEnable"), + true, // the compositor always presents vsynced; RenderVSyncMode paces the world !gHeadlessClient, p.ignore_pixel_depth, 0, @@ -2596,9 +2645,14 @@ void LLViewerWindow::reshape(S32 width, S32 height) mWindowRectRaw.mRight = mWindowRectRaw.mLeft + width; mWindowRectRaw.mTop = mWindowRectRaw.mBottom + height; - // Keep the presentation swap chain in sync with the window size so - // its image's viewport tracks the OS window. - LLAppViewer::instance()->getSwapChain().resize((U32)width, (U32)height); + // Resize the compositor right here since we're on its thread. + // The viewer's render targets belong to the viewer thread, so + // we post that resize over instead. + LLAppViewer::instance()->getCompositor().resize((U32)width, (U32)height); + U32 w_u = (U32)width, h_u = (U32)height; + gMainloopWork.post([w_u, h_u]() { + LLAppViewer::instance()->resizeRenderTargets(w_u, h_u); + }); //glViewport(0, 0, width, height ); @@ -5305,6 +5359,10 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei swap(); } + // The rendered frame lives in our back buffer, not + // FBO 0, so read from there. + LLAppViewer::instance()->getBackBuffer().bindForRead(); + for (U32 out_y = 0; out_y < read_height ; out_y++) { S32 output_buffer_offset = ( diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 4ef88f5deb3..1bf55501fe7 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -230,7 +230,6 @@ const F32 DEFERRED_LIGHT_FALLOFF = 0.5f; const U32 DEFERRED_VB_MASK = LLVertexBuffer::MAP_VERTEX | LLVertexBuffer::MAP_TEXCOORD0 | LLVertexBuffer::MAP_TEXCOORD1; extern S32 gBoxFrame; -extern bool gDisplaySwapBuffers; extern bool gDebugGL; extern bool gCubeSnapshot; extern bool gSnapshotNoPost; diff --git a/indra/newview/skins/default/xui/en/floater_performance.xml b/indra/newview/skins/default/xui/en/floater_performance.xml index 5fb9eeb2de3..76151fac79c 100644 --- a/indra/newview/skins/default/xui/en/floater_performance.xml +++ b/indra/newview/skins/default/xui/en/floater_performance.xml @@ -1,6 +1,6 @@ - + + VSync: + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/panel_performance_autoadjustments.xml b/indra/newview/skins/default/xui/en/panel_performance_autoadjustments.xml index 1bea605d91b..11ff233760f 100644 --- a/indra/newview/skins/default/xui/en/panel_performance_autoadjustments.xml +++ b/indra/newview/skins/default/xui/en/panel_performance_autoadjustments.xml @@ -228,18 +228,48 @@ top_pad="3" left="20" width="540"/> - + name="vsync_label" + width="180"> + VSync: + + + + + + + + Date: Tue, 23 Jun 2026 01:13:45 -0400 Subject: [PATCH 4/9] Add macOS support + general refinements. - Supports VSync much better on macOS - Use thread safe ref counting where we need it - Make the swapchain less swapchain (more or less scaffolding right now) and have zero-copy writing to the back buffer. - Simplify LLRenderTarget to be closer to its pre-compositor state - With the notable exception of we use LLImageGLs now instead of raw texture names - Make leases opt-in rather than "everyone gets a lease" --- indra/cmake/Linking.cmake | 2 + indra/llappearance/lltexlayer.h | 2 +- indra/llrender/llcompositable.h | 5 - indra/llrender/llcompositor.cpp | 99 +++- indra/llrender/llcompositor.h | 43 +- indra/llrender/llglslshader.cpp | 4 +- indra/llrender/llgpuresource.cpp | 4 - indra/llrender/llgpuresource.h | 40 +- indra/llrender/llimagegl.cpp | 108 +++-- indra/llrender/llimagegl.h | 53 ++- indra/llrender/llrender.cpp | 3 +- indra/llrender/llrendertarget.cpp | 370 ++++++--------- indra/llrender/llrendertarget.h | 103 ++-- indra/llrender/llswapchain.cpp | 111 ++--- indra/llrender/llswapchain.h | 82 ++-- indra/llrender/lltestsquarecompositable.cpp | 60 +-- indra/llrender/lltestsquarecompositable.h | 23 +- indra/llrender/lltexture.h | 2 +- indra/llwindow/llwindow.h | 6 + indra/llwindow/llwindowmacosx-objc.h | 9 +- indra/llwindow/llwindowmacosx-objc.mm | 86 +++- indra/llwindow/llwindowmacosx.cpp | 491 ++++++++++++++------ indra/llwindow/llwindowmacosx.h | 85 +++- indra/newview/app_settings/settings.xml | 11 - indra/newview/featuretable_mac.txt | 4 - indra/newview/llappviewer.cpp | 84 ++-- indra/newview/llappviewer.h | 24 +- indra/newview/llscenemonitor.cpp | 2 +- indra/newview/llviewercontrol.cpp | 13 - indra/newview/llviewerdisplay.cpp | 4 +- indra/newview/llviewerthread.cpp | 20 + indra/newview/llviewerthread.h | 12 +- 32 files changed, 1187 insertions(+), 778 deletions(-) diff --git a/indra/cmake/Linking.cmake b/indra/cmake/Linking.cmake index 5d4e8d86348..56bd085c107 100644 --- a/indra/cmake/Linking.cmake +++ b/indra/cmake/Linking.cmake @@ -76,6 +76,7 @@ else() find_library(APPKIT_LIBRARY AppKit) find_library(COREAUDIO_LIBRARY CoreAudio) find_library(COREGRAPHICS_LIBRARY CoreGraphics) + find_library(COREVIDEO_LIBRARY CoreVideo) find_library(AUDIOTOOLBOX_LIBRARY AudioToolbox) find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers) @@ -88,6 +89,7 @@ else() ${COREAUDIO_LIBRARY} ${AUDIOTOOLBOX_LIBRARY} ${COREGRAPHICS_LIBRARY} + ${COREVIDEO_LIBRARY} ${UNIFORMTYPEIDENTIFIERS_LIBRARY} ) endif() diff --git a/indra/llappearance/lltexlayer.h b/indra/llappearance/lltexlayer.h index 876ea6f6007..4d72c2e1efb 100644 --- a/indra/llappearance/lltexlayer.h +++ b/indra/llappearance/lltexlayer.h @@ -262,7 +262,7 @@ class LLTexLayerSetInfo // // The composite image that a LLTexLayerSet writes to. Each LLTexLayerSet has one. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class LLTexLayerSetBuffer : public virtual LLRefCount +class LLTexLayerSetBuffer : public virtual LLThreadSafeRefCount { LOG_CLASS(LLTexLayerSetBuffer); diff --git a/indra/llrender/llcompositable.h b/indra/llrender/llcompositable.h index 6c7ec02f6d3..036842044c4 100644 --- a/indra/llrender/llcompositable.h +++ b/indra/llrender/llcompositable.h @@ -67,9 +67,6 @@ class LLCompositable // the next, written by produceFrame() and read by the stats overlay. F32 lastFrameMs() const { return mFrameMs.load(std::memory_order_relaxed); } - // Monotonic count of published frames. Rate = produced FPS. - U32 framesProduced() const { return mFramesProduced.load(std::memory_order_relaxed); } - protected: // Producers call this when a frame is complete - stamps the cycle // metrics. @@ -82,12 +79,10 @@ class LLCompositable std::memory_order_relaxed); } mLastProduceTime = now; // producer-thread-only - mFramesProduced.fetch_add(1, std::memory_order_relaxed); } private: std::atomic mFrameMs{0.f}; - std::atomic mFramesProduced{0}; F64 mLastProduceTime = 0.0; }; diff --git a/indra/llrender/llcompositor.cpp b/indra/llrender/llcompositor.cpp index 720a39bd6f8..257a7daab9d 100644 --- a/indra/llrender/llcompositor.cpp +++ b/indra/llrender/llcompositor.cpp @@ -31,11 +31,13 @@ #include "lltestsquarecompositable.h" #include "lltimer.h" #include "llrendertarget.h" +#include "llimagegl.h" #include "llgl.h" #include "llglslshader.h" #include "llwindow.h" #include +#include LLCompositor::LLCompositor() = default; @@ -144,6 +146,13 @@ void LLCompositor::presentFrame() { LL_PROFILE_ZONE_SCOPED; + // Once shutdown is requested we stop presenting so producers waiting + // on a present unblock and the viewer thread can be joined. + if (mShutdownRequested.load(std::memory_order_relaxed)) + { + return; + } + // The RT stack should be empty when we get here. llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); @@ -180,10 +189,10 @@ void LLCompositor::presentFrame() const F64 present_start = LLTimer::getTotalSeconds(); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - LLRenderTarget::sCurFBO = 0; - glViewport(0, 0, dst_w, dst_h); - glDrawBuffer(GL_BACK); + // Acquire the present surface through the swap-chain seam (GL: FBO 0, full + // window) and composite the layers straight into it. + mSwapChain.acquireNextImage(); + mSwapChain.bindForRender(); glDisable(GL_BLEND); glDisable(GL_SCISSOR_TEST); glDisable(GL_DEPTH_TEST); @@ -218,15 +227,29 @@ void LLCompositor::presentFrame() const GLint w = (GLint)front.getWidth(); const GLint h = (GLint)front.getHeight(); - // Textures are shared between contexts, FBOs aren't. The guard - // holds the RT's shared lease across the draw and fences. - LLScopedTexName src_tex_guard = front.getTexture(0); - const U32 src_tex = src_tex_guard.get(); + // The cross-context primitive is the color texture (LLImageGL), not the + // FBO-centric RT. Sample it directly: the guard holds its shared lease + // across the draw and its fence orders the GPU. Single-context layers + // (no attachment image) fall back to the raw RT name (no lease needed). + LLImageGL* sync = front.getColorAttachmentImage(); + LLScopedTexName src_tex_guard; + U32 src_tex; + if (sync) + { + src_tex_guard = sync->getTexName(); + src_tex = src_tex_guard.get(); + } + else + { + src_tex = front.getTexture(0); + } llassert(src_tex != 0); // Wait on the producer's fence so we only sample finished pixels. - // No-op if the RT didn't opt in to cross-context sync. - front.waitFrameCompleteFence(); + if (sync) + { + sync->waitFrameCompleteFence(); + } // Layer rect in NDC; GL origin bottom-left matches the // compositeOffset convention. @@ -244,7 +267,10 @@ void LLCompositor::presentFrame() // Reverse fence: the producer waits on this before writing into // the buffer again. - front.placeReadCompleteFence(); + if (sync) + { + sync->placeReadCompleteFence(); + } } if (shader_ready) @@ -252,8 +278,10 @@ void LLCompositor::presentFrame() mBlitShader->unbind(); } - // The layers were drawn straight into FBO 0; just swap. - mSwapChain.presentDirect(); + // Layers composited into the present surface; flush any batched GL and + // present (the old img.flush() path flushed gGL before swapBuffers). + gGL.flush(); + mSwapChain.present(); const F64 present_end = LLTimer::getTotalSeconds(); mLastPresentMs.store( @@ -275,7 +303,46 @@ void LLCompositor::presentFrame() mPresentWindowStart = present_end; } - // Let subscribers pace themselves to our clock. - ++mFrameIndex; - mOnSync(mFrameIndex); + // Publish the present so producers parked in waitForPresent can pace + // themselves to the vblank clock. LIVENESS INVARIANT: this must bump on + // EVERY present - including when we re-present an unchanged frame + // (tryAcquireNewFront returned false) - because the viewer thread makes + // progress only when this index advances. Never gate present() on having + // new output, or the vsync gate deadlocks. + { + std::lock_guard lk(mSyncMutex); + ++mPresentIndex; + } + mSyncCV.notify_all(); +} + +U64 LLCompositor::waitForPresent(U64 target, const std::function& should_stop) +{ + std::unique_lock lk(mSyncMutex); + auto ready = [&]{ + return mSyncInterrupted + || (should_stop && should_stop()) + || mPresentIndex >= target; + }; + // wait_for (not wait) so an external should_stop set without a notify is + // still picked up promptly - the present notify handles the common case. + while (!ready()) + { + mSyncCV.wait_for(lk, std::chrono::milliseconds(100)); + } + return mPresentIndex; // < target means interrupted; the caller should stop +} + +void LLCompositor::interruptSync() +{ + { + std::lock_guard lk(mSyncMutex); + mSyncInterrupted = true; + } + mSyncCV.notify_all(); +} + +void LLCompositor::wakeSyncWaiters() +{ + mSyncCV.notify_all(); } diff --git a/indra/llrender/llcompositor.h b/indra/llrender/llcompositor.h index c1c4d904cd9..0d479f136b8 100644 --- a/indra/llrender/llcompositor.h +++ b/indra/llrender/llcompositor.h @@ -31,9 +31,9 @@ #include "llswapchain.h" #include "llcompositable.h" -#include - #include +#include +#include #include #include #include @@ -67,7 +67,7 @@ class LLCompositor // Tear everything down. Called before window destruction. void release(); - bool isInitialized() const { return mSwapChain.getImageCount() > 0; } + bool isInitialized() const { return mSwapChain.isAttached(); } // Add a compositable. Lower-index layers draw first (bottom). Order is // fixed at registration; no re-ordering yet. @@ -88,18 +88,34 @@ class LLCompositor // its last front re-drawn. void presentFrame(); + // Signal shutdown so producers stop waiting on us. Once set, the + // compositor stops presenting and any producer parked waiting for a + // present (e.g. the mailbox back-pressure spins) bails out, so the + // viewer thread can reach its quit check and the join completes. + // Callable from any thread; checked via isShutdownRequested(). + void requestShutdown() { mShutdownRequested.store(true, std::memory_order_relaxed); } + bool isShutdownRequested() const { return mShutdownRequested.load(std::memory_order_relaxed); } + // Present every Nth vblank (1 = every vblank). The driver does the // pacing and the sync signal inherits the divided cadence. Callable // from any thread - applied on our own context at the next present. void setSwapInterval(S32 interval) { mPendingSwapInterval.store(interval, std::memory_order_relaxed); } - // Fired at the end of presentFrame() with the frame index. Compositables - // subscribe to this to pace themselves to our clock. - typedef boost::signals2::signal sync_signal_t; - boost::signals2::connection onSync(const sync_signal_t::slot_type& cb) - { - return mOnSync.connect(cb); - } + // Producers pace themselves to the present (vblank) clock by waiting on a + // published monotonic present index. waitForPresent blocks until the index + // reaches `target` (or the compositor shuts down / should_stop fires) and + // returns the current index; a value < target means it was interrupted. + // Pace to every Nth present by waiting for index = last + N. + U64 waitForPresent(U64 target, const std::function& should_stop = {}); + + // Permanently release every producer parked in waitForPresent (full + // compositor/viewer shutdown). Sticky: subsequent waits return at once. + void interruptSync(); + + // Wake producers parked in waitForPresent so they re-check their own + // should_stop - for a producer disconnecting while the compositor keeps + // running (e.g. a debug test square toggled off). Non-sticky. + void wakeSyncWaiters(); // Read access for resize handling and similar plumbing. Presenting // stays in here. @@ -133,9 +149,12 @@ class LLCompositor LLSwapChain mSwapChain; LLWindow* mWindow = nullptr; std::vector mCompositables; - sync_signal_t mOnSync; - U64 mFrameIndex = 0; + std::mutex mSyncMutex; + std::condition_variable mSyncCV; + U64 mPresentIndex = 0; // published present (vblank) count + bool mSyncInterrupted = false; // sticky full-shutdown release std::atomic mPendingSwapInterval{-1}; // applied at next present; -1 = none + std::atomic mShutdownRequested{false}; // set on shutdown to unblock producers // Bring-up refresh overlay: colored squares that repaint at // different sync intervals, behind RenderCompositorShowRefresh. diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index 9f9b8832f3e..c358f64b8ed 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -1165,8 +1165,8 @@ S32 LLGLSLShader::bindTexture(S32 uniform, LLRenderTarget* texture, bool depth, } else { bool has_mips = mode == LLTexUnit::TFO_TRILINEAR || mode == LLTexUnit::TFO_ANISOTROPIC; - auto guard = texture->getTexture(index); - gGL.getTexUnit(uniform)->bindManual(texture->getUsage(), guard.get(), has_mips); + U32 name = texture->getTexture(index); + gGL.getTexUnit(uniform)->bindManual(texture->getUsage(), name, has_mips); } gGL.getTexUnit(uniform)->setTextureFilteringOption(mode); diff --git a/indra/llrender/llgpuresource.cpp b/indra/llrender/llgpuresource.cpp index 3b8f9567daa..e63925c8e22 100644 --- a/indra/llrender/llgpuresource.cpp +++ b/indra/llrender/llgpuresource.cpp @@ -54,7 +54,6 @@ LLSharedLease::LLSharedLease(LLGPUResource* res) if (mResource) { mResource->mLeaseMutex->lock_shared(); - mResource->onSharedAcquire(); } } @@ -84,7 +83,6 @@ void LLSharedLease::release() { if (mResource) { - mResource->onSharedRelease(); mResource->mLeaseMutex->unlock_shared(); mResource = nullptr; } @@ -98,7 +96,6 @@ LLUniqueLease::LLUniqueLease(LLGPUResource* res) if (mResource) { mResource->mLeaseMutex->lock(); - mResource->onUniqueAcquire(); } } @@ -128,7 +125,6 @@ void LLUniqueLease::release() { if (mResource) { - mResource->onUniqueRelease(); mResource->mLeaseMutex->unlock(); mResource = nullptr; } diff --git a/indra/llrender/llgpuresource.h b/indra/llrender/llgpuresource.h index 31b7ae64252..e6b823bf119 100644 --- a/indra/llrender/llgpuresource.h +++ b/indra/llrender/llgpuresource.h @@ -30,15 +30,19 @@ #include "llresourcelease.h" #include "llgltypes.h" +#include #include #include +#include // Base class for GPU resources that need cross-thread synchronization. // // Provides two kinds of RAII lease: shared (many readers) and unique (one -// writer). Subclasses can override the acquire/release hooks to place GL -// fences at the lease boundary; the defaults do nothing, so resources that -// don't need cross-context fences pay nothing. +// writer) - a plain reader/writer lock, nothing more. Leasing is opt-in +// (setLeaseEnabled): only resources actually shared across threads take the +// lock; everything else gets an empty lease and pays nothing on the bind hot +// path. Cross-context GL fences are placed/waited explicitly by callers (see +// LLImageGL), not by the lease. // // Leases follow std::shared_mutex rules: non-recursive, so don't take a // second lease on the same resource while you already hold one. @@ -53,25 +57,35 @@ class LLGPUResource // Movable so containers of these can reallocate. Only move when no // leases are live; a moved-from resource has a null mutex and can't - // be leased again. - LLGPUResource(LLGPUResource&& other) noexcept = default; + // be leased again. Spelled out because std::atomic isn't movable. + LLGPUResource(LLGPUResource&& other) noexcept + : mLeaseMutex(std::move(other.mLeaseMutex)), + mLeaseEnabled(other.mLeaseEnabled.load(std::memory_order_relaxed)) {} LLGPUResource& operator=(LLGPUResource&&) = delete; - LLSharedLease getSharedLease() const { return LLSharedLease(const_cast(this)); } - LLUniqueLease getUniqueLease() { return LLUniqueLease(this); } + // Opt-in: a resource that hasn't enabled leasing returns an empty lease, + // so the lock is skipped entirely. + LLSharedLease getSharedLease() const + { + return LLSharedLease(mLeaseEnabled.load(std::memory_order_acquire) + ? const_cast(this) : nullptr); + } + LLUniqueLease getUniqueLease() + { + return LLUniqueLease(mLeaseEnabled.load(std::memory_order_acquire) ? this : nullptr); + } protected: - // Lease lifecycle hooks, called with the lock held. Override the ones - // you care about; the defaults do nothing. - virtual void onSharedAcquire() {} - virtual void onSharedRelease() {} - virtual void onUniqueAcquire() {} - virtual void onUniqueRelease() {} + // Enable leasing for this resource. Off by default - flip it on once the + // resource is actually handed across threads (e.g. an off-thread texture + // upload), before the first cross-thread lease. + void setLeaseEnabled(bool b) { mLeaseEnabled.store(b, std::memory_order_release); } private: friend class LLSharedLease; friend class LLUniqueLease; mutable std::unique_ptr mLeaseMutex; + std::atomic mLeaseEnabled{false}; }; // Guarded GL texture name - the guard holds a shared lease for as long as diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index d8455dc89da..c310f708d7f 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1745,12 +1745,17 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) LL_PROFILE_ZONE_SCOPED; llassert(!on_viewer_thread()); - // Take and drop a unique lease - the release hook places the fence - // the consumer will wait on. + // This name is produced off the viewer thread, so the image is genuinely + // cross-thread: opt into cross-context sync before the first use below. + setNeedsCrossContextSync(true); + + // Place the frame-complete fence the viewer thread waits on before it + // installs the new name. Under the unique lease so a second upload can't + // race it. { LL_PROFILE_ZONE_NAMED("cglt - sync"); LLUniqueLease lease = getUniqueLease(); - // release places fence + glFlush via the hook + placeFrameCompleteFence(); } ref(); @@ -1768,62 +1773,88 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) LL_PROFILER_GPU_COLLECT; } -// ---- LLGPUResource hooks -------------------------------------------------- +// ---- Cross-context GPU sync ------------------------------------------------ -void LLImageGL::onUniqueRelease() +// Wrap a fresh fence in a shared_ptr that deletes it when the last reference +// drops. Sync objects belong to the share group, so either context can do the +// delete. +static std::shared_ptr make_fence() { - if (on_viewer_thread()) + return std::shared_ptr( + glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), + [](void* sync) { if (sync) glDeleteSync((GLsync)sync); }); +} + +void LLImageGL::setNeedsCrossContextSync(bool b) +{ + if (b && !mCrossSync) { - // No cross-context handoff on main thread. - return; + mCrossSync = std::make_shared(); } - - // Replace any prior fence. We hold the unique lock, so nothing can - // be mid-wait on the old one. - if (mPendingFence != nullptr) + else if (!b) { - glDeleteSync(mPendingFence); - mPendingFence = nullptr; + mCrossSync.reset(); } + // Cross-context textures take the CPU lease too (field accessors); + // single-context textures pay neither. + setLeaseEnabled(b); +} - glFlush(); - mPendingFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - mPendingFenceThread.store(std::this_thread::get_id(), std::memory_order_release); - glFlush(); +void LLImageGL::placeFrameCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence = make_fence(); + glFlush(); // so the consumer's context can see it + std::lock_guard lock(mCrossSync->fenceMutex); + mCrossSync->frameCompleteFence = std::move(fence); } -void LLImageGL::onSharedAcquire() +void LLImageGL::waitFrameCompleteFence() { - // Multiple shared holders can wait on the same fence - that's fine. - // The next unique holder cleans it up. - if (mPendingFence != nullptr && - mPendingFenceThread.load(std::memory_order_acquire) != std::this_thread::get_id()) + if (!mCrossSync) return; + std::shared_ptr fence; { - glWaitSync(mPendingFence, 0, GL_TIMEOUT_IGNORED); + std::lock_guard lock(mCrossSync->fenceMutex); + fence = mCrossSync->frameCompleteFence; + } + if (fence) + { + // Server-side wait; the held shared_ptr keeps the sync alive even if + // the producer swaps in a new one mid-wait. + glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); } } -void LLImageGL::onUniqueAcquire() +void LLImageGL::placeReadCompleteFence() { - if (mPendingFence != nullptr && - mPendingFenceThread.load(std::memory_order_acquire) != std::this_thread::get_id()) + if (!mCrossSync) return; + std::shared_ptr fence = make_fence(); + glFlush(); + std::lock_guard lock(mCrossSync->fenceMutex); + mCrossSync->readCompleteFence = std::move(fence); +} + +void LLImageGL::waitReadCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence; { - glWaitSync(mPendingFence, 0, GL_TIMEOUT_IGNORED); + std::lock_guard lock(mCrossSync->fenceMutex); + fence = mCrossSync->readCompleteFence; } - // Exclusive - drop the fence so the next release can place a fresh one. - if (mPendingFence != nullptr) + if (fence) { - glDeleteSync(mPendingFence); - mPendingFence = nullptr; + glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); } } void LLImageGL::syncTexName(LLGLuint texname) { - // We're changing mTexName, so take the unique lease. Acquiring also - // waits on any pending fence. + // We're changing mTexName, so take the unique lease, and wait the + // frame-complete fence so the new pixels are done before the name goes live. LLUniqueLease lease = getUniqueLease(); + waitFrameCompleteFence(); if (texname != 0) { if (mTexName != 0 && mTexName != texname) @@ -2440,9 +2471,18 @@ bool LLImageGL::getHasGLTexture() const LLScopedTexName LLImageGL::getTexName() const { + // Opt-in lease: ordinary textures aren't leasable, so getSharedLease() + // returns an empty lease and the bind hot path stays lock-free. A + // cross-context texture (mailbox color, CEF) takes the shared read lease + // here - the reader half of its reader/writer lock. return LLScopedTexName(getSharedLease(), mTexName); } +LLGLuint LLImageGL::getTexNameRaw() const +{ + return mTexName; +} + bool LLImageGL::getMask(const LLVector2 &tc) { LLSharedLease lease = getSharedLease(); diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 2c5b4c4f66f..1393aa1fc1f 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -41,6 +41,8 @@ #include "threadpool.h" #include "workqueue.h" #include +#include +#include #include #include @@ -60,7 +62,7 @@ namespace LLImageGLMemory } //============================================================================ -class LLImageGL : public LLRefCount, public LLGPUResource +class LLImageGL : public LLThreadSafeRefCount, public LLGPUResource { friend class LLTexUnit; public: @@ -117,12 +119,6 @@ class LLImageGL : public LLRefCount, public LLGPUResource void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); - // LLGPUResource hooks. Releasing a unique lease off-thread places a GL - // fence; the next acquire from a different thread waits on it. - // Same-thread acquires pay nothing. - void onUniqueRelease() override; - void onSharedAcquire() override; - void onUniqueAcquire() override; public: virtual void dump(); // debugging info to LL_INFOS() @@ -183,6 +179,31 @@ class LLImageGL : public LLRefCount, public LLGPUResource // See LLScopedTexName for usage. LLScopedTexName getTexName() const; + // Unguarded raw GL name - takes no lease. Only for callers that already + // hold a lease on this image (or guarantee single-thread access): the + // FBO-attach and in-place resize paths in LLRenderTarget read it under a + // held lease, where getTexName() would re-enter the same mutex. + LLGLuint getTexNameRaw() const; + + // Cross-context GPU sync. Opt in (setNeedsCrossContextSync) for textures + // read from another GL context - the off-thread upload path and the + // compositor mailbox. Null otherwise, so single-context textures carry no + // sync state. frameComplete is placed by the producer and waited by the + // consumer; readComplete goes the other way, so a recycled buffer isn't + // overwritten while a read is still in flight. + void setNeedsCrossContextSync(bool b); + bool needsCrossContextSync() const { return (bool)mCrossSync; } + void placeFrameCompleteFence(); + void waitFrameCompleteFence(); + void placeReadCompleteFence(); + void waitReadCompleteFence(); + + // Whether this image owns its GL name. False (default): ~LLImageGL deletes + // the name. True: the name is owned elsewhere (e.g. an LLRenderTarget color + // attachment we only wrap for cross-context fencing) and must not be freed + // here. + void setExternalTexture(bool b) { mExternalTexture = b; } + bool getIsAlphaMask() const; bool getIsResident(bool test_now = false); // not const @@ -263,11 +284,17 @@ class LLImageGL : public LLRefCount, public LLGPUResource U16 mHeight; S8 mCurrentDiscardLevel; - // Fence from the last off-thread unique release; the next cross-thread - // acquire waits on it. Only the unique lock writes or deletes it - - // shared waiters just read. - GLsync mPendingFence = nullptr; - std::atomic mPendingFenceThread; + // Cross-context GPU sync state, allocated only for textures that opt in + // (setNeedsCrossContextSync). Fences are shared_ptr with glDeleteSync + // as the deleter, so a fence can't be freed under a waiter that still holds + // a reference. The mutex guards only the pointer swap, never a GL call. + struct CrossContextSync + { + std::mutex fenceMutex; + std::shared_ptr frameCompleteFence; + std::shared_ptr readCompleteFence; + }; + std::shared_ptr mCrossSync; bool mAllowCompression; @@ -291,7 +318,7 @@ class LLImageGL : public LLRefCount, public LLGPUResource LLGLenum mFormatType; bool mFormatSwapBytes;// if true, use glPixelStorei(GL_UNPACK_SWAP_BYTES, 1) - bool mExternalTexture; + bool mExternalTexture = false; // STATICS public: diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 2dbef9b1956..791ced8ef82 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -441,8 +441,7 @@ bool LLTexUnit::bind(LLRenderTarget* renderTarget, bool bindDepth) } else { - auto guard = renderTarget->getTexture(); - bindManual(renderTarget->getUsage(), guard.get()); + bindManual(renderTarget->getUsage(), renderTarget->getTexture()); } return true; diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 1db3bae7043..cfb4c30fe52 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -29,6 +29,7 @@ #include "llrendertarget.h" #include "llrender.h" #include "llgl.h" +#include "llimagegl.h" thread_local LLRenderTarget* LLRenderTarget::sBoundTarget = NULL; U32 LLRenderTarget::sBytesAllocated = 0; @@ -64,10 +65,12 @@ LLRenderTarget::LLRenderTarget() : mResX(0), mResY(0), mFBO(0), - mDepth(0), mUseDepth(false), mUsage(LLTexUnit::TT_TEXTURE) { + // The RT is a context-local FBO wrapper - never leasable. Cross-context + // sync lives on the color attachment (an LLImageGL), not here. + setLeaseEnabled(false); } LLRenderTarget::~LLRenderTarget() @@ -75,15 +78,40 @@ LLRenderTarget::~LLRenderTarget() release(); } +LLRenderTarget::LLRenderTarget(LLRenderTarget&& other) noexcept : + LLGPUResource(std::move(other)), + mResX(other.mResX), + mResY(other.mResY), + mTex(std::move(other.mTex)), + mInternalFormat(std::move(other.mInternalFormat)), + mFBO(other.mFBO), + mPreviousRT(other.mPreviousRT), + mDepth(std::move(other.mDepth)), + mSharedDepth(std::move(other.mSharedDepth)), + mUseDepth(other.mUseDepth), + mGenerateMipMaps(other.mGenerateMipMaps), + mMipLevels(other.mMipLevels), + mUsage(other.mUsage), + mIsSwapChainImage(other.mIsSwapChainImage) +{ + // Leave the moved-from RT inert so its destructor frees nothing - mFBO is a + // raw GL name we just took, and a defaulted move would copy-not-zero it, + // double-deleting the framebuffer the destination now owns. + other.mFBO = 0; + other.mResX = 0; + other.mResY = 0; + other.mUseDepth = false; + other.mPreviousRT = nullptr; + other.mIsSwapChainImage = false; +} + // --------------------------------------------------------------------------- -// Public mutators - each takes a unique lease and delegates to a _locked -// helper if it needs to call into another mutator. +// The RT is not leasable (see ctor); cross-context sync rides its color +// attachment (an LLImageGL). These methods touch only context-local GL state. // --------------------------------------------------------------------------- void LLRenderTarget::resize(U32 resx, U32 resy) { - LLUniqueLease lease = getUniqueLease(); - S32 pix_diff = (resx*resy)-(mResX*mResY); mResX = resx; @@ -93,33 +121,57 @@ void LLRenderTarget::resize(U32 resx, U32 resy) for (U32 i = 0; i < mTex.size(); ++i) { - gGL.getTexUnit(0)->bindManual(mUsage, mTex[i]); + // resize is the writer: take the attachment's unique lease so the + // re-spec can't land while another context samples it (a no-op unless + // this attachment opted into cross-context sync). Read the name + // unguarded - we hold the lease, so getTexName() would self-deadlock. + // The GL name is stable across re-spec, so the LLImageGL stays valid. + LLUniqueLease lease = mTex[i]->getUniqueLease(); + gGL.getTexUnit(0)->bindManual(mUsage, mTex[i]->getTexNameRaw()); LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, mInternalFormat[i], mResX, mResY, GL_RGBA, GL_UNSIGNED_BYTE, NULL, false); sBytesAllocated += pix_diff*4; } - if (mDepth) + // Only the owner re-specs depth; a borrower (mDepth null, mSharedDepth set) + // sees the owner's re-spec through the shared GL name. + if (mDepth.notNull()) { - gGL.getTexUnit(0)->bindManual(mUsage, mDepth); + LLUniqueLease lease = mDepth->getUniqueLease(); U32 internal_type = LLTexUnit::getInternalType(mUsage); + gGL.getTexUnit(0)->bindManual(mUsage, mDepth->getTexNameRaw()); LLImageGL::setManualImage(internal_type, 0, GL_DEPTH_COMPONENT24, mResX, mResY, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL, false); sBytesAllocated += pix_diff*4; } } +LLPointer LLRenderTarget::createAttachmentImage(U32 internal_fmt, U32 primary_fmt, U32 type, bool use_mipmaps) +{ + // Bare owned attachment: a name + format, storage allocated via + // setManualImage with no source pixels. We never call setSize (which + // enforces power-of-two) - RTs are routinely NPOT; mResX/mResY are the + // authoritative size. setDiscardLevel(0) keeps getWidth/getHeight from + // shifting by the -1 default discard level. + LLPointer img = new LLImageGL(use_mipmaps); + img->setTarget(LLTexUnit::getInternalType(mUsage), mUsage); + img->setExplicitFormat((LLGLint)internal_fmt, primary_fmt, type); + img->setDiscardLevel(0); + img->createGLTexture(); + gGL.getTexUnit(0)->bindManual(mUsage, img->getTexNameRaw()); + LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, internal_fmt, mResX, mResY, primary_fmt, type, NULL, false); + return img; +} + bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLTexUnit::eTextureType usage, LLTexUnit::eTextureMipGeneration generateMipMaps) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(usage == LLTexUnit::TT_TEXTURE); llassert(!isBoundInStack()); - LLUniqueLease lease = getUniqueLease(); - resx = llmin(resx, (U32) gGLManager.mGLMaxTextureSize); resy = llmin(resy, (U32) gGLManager.mGLMaxTextureSize); - release_locked(); + release(); mResX = resx; mResY = resy; @@ -135,34 +187,25 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT if (depth) { - // Inlined allocateDepth body - still under our unique lease. - LLImageGL::generateTextures(1, &mDepth); - gGL.getTexUnit(0)->bindManual(mUsage, mDepth); - U32 internal_type = LLTexUnit::getInternalType(mUsage); - stop_glerror(); - clear_glerror(); - LLImageGL::setManualImage(internal_type, 0, GL_DEPTH_COMPONENT24, mResX, mResY, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL, false); - gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_POINT); - sBytesAllocated += mResX*mResY*4; - if (glGetError() != GL_NO_ERROR) + if (!allocateDepth()) { - LL_WARNS() << "Unable to allocate depth buffer for render target." << LL_ENDL; + LL_WARNS() << "Failed to allocate depth buffer for render target." << LL_ENDL; return false; } } glGenFramebuffers(1, (GLuint *) &mFBO); - if (mDepth) + if (mDepth.notNull()) { glBindFramebuffer(GL_FRAMEBUFFER, mFBO); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), mDepth, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), mDepth->getTexNameRaw(), 0); glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } - return addColorAttachment_locked(color_fmt); + return addColorAttachment(color_fmt); } void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) @@ -170,12 +213,10 @@ void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) LL_PROFILE_ZONE_SCOPED; llassert(img != nullptr); llassert(sUseFBO); - llassert(mDepth == 0); + llassert(mDepth.isNull()); llassert(mTex.empty()); llassert(!isBoundInStack()); - LLUniqueLease lease = getUniqueLease(); - if (mFBO == 0) { glGenFramebuffers(1, (GLuint*)&mFBO); @@ -193,7 +234,10 @@ void LLRenderTarget::setColorAttachment(LLImageGL* img, LLGLuint use_name) use_name = guard.get(); } - mTex.push_back(use_name); + // Borrow the caller's image: store the LLPointer (refcount keeps it alive) + // but the RT doesn't own/free it - the caller does. Attach use_name (which + // equals img's name unless the caller overrode it). + mTex.push_back(img); glBindFramebuffer(GL_FRAMEBUFFER, mFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, @@ -212,8 +256,6 @@ void LLRenderTarget::releaseColorAttachment() llassert(mTex.size() == 1); llassert(mFBO != 0); - LLUniqueLease lease = getUniqueLease(); - glBindFramebuffer(GL_FRAMEBUFFER, mFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, LLTexUnit::getInternalType(mUsage), 0, 0); glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); @@ -226,12 +268,6 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(!isBoundInStack()); - LLUniqueLease lease = getUniqueLease(); - return addColorAttachment_locked(color_fmt); -} - -bool LLRenderTarget::addColorAttachment_locked(U32 color_fmt) -{ if (color_fmt == 0) { return true; @@ -251,20 +287,13 @@ bool LLRenderTarget::addColorAttachment_locked(U32 color_fmt) return false; } - U32 tex; - LLImageGL::generateTextures(1, &tex); - gGL.getTexUnit(0)->bindManual(mUsage, tex); - stop_glerror(); - + clear_glerror(); + LLPointer img = createAttachmentImage(color_fmt, GL_RGBA, GL_UNSIGNED_BYTE, mGenerateMipMaps != LLTexUnit::TMG_NONE); + if (glGetError() != GL_NO_ERROR) { - clear_glerror(); - LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, color_fmt, mResX, mResY, GL_RGBA, GL_UNSIGNED_BYTE, NULL, false); - if (glGetError() != GL_NO_ERROR) - { - LL_WARNS() << "Could not allocate color buffer for render target." << LL_ENDL; - return false; - } + LL_WARNS() << "Could not allocate color buffer for render target." << LL_ENDL; + return false; } sBytesAllocated += mResX*mResY*4; @@ -298,20 +327,20 @@ bool LLRenderTarget::addColorAttachment_locked(U32 color_fmt) { glBindFramebuffer(GL_FRAMEBUFFER, mFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+offset, - LLTexUnit::getInternalType(mUsage), tex, 0); + LLTexUnit::getInternalType(mUsage), img->getTexNameRaw(), 0); check_framebuffer_status(); glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } - mTex.push_back(tex); + mTex.push_back(img); mInternalFormat.push_back(color_fmt); if (gDebugGL) { //bind and unbind to validate target - bindTarget_locked(); - flush_locked(); + bindTarget(); + flush(); } return true; @@ -320,15 +349,10 @@ bool LLRenderTarget::addColorAttachment_locked(U32 color_fmt) bool LLRenderTarget::allocateDepth() { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; - LLUniqueLease lease = getUniqueLease(); - - LLImageGL::generateTextures(1, &mDepth); - gGL.getTexUnit(0)->bindManual(mUsage, mDepth); - U32 internal_type = LLTexUnit::getInternalType(mUsage); stop_glerror(); clear_glerror(); - LLImageGL::setManualImage(internal_type, 0, GL_DEPTH_COMPONENT24, mResX, mResY, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL, false); + mDepth = createAttachmentImage(GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, false); gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_POINT); sBytesAllocated += mResX*mResY*4; @@ -346,17 +370,12 @@ void LLRenderTarget::shareDepthBuffer(LLRenderTarget& target) { llassert(!isBoundInStack()); - // Lock this first, then target - keep the order consistent so we - // can't deadlock. - LLUniqueLease lease_this = getUniqueLease(); - LLUniqueLease lease_other = target.getUniqueLease(); - if (!mFBO || !target.mFBO) { LL_ERRS() << "Cannot share depth buffer between non FBO render targets." << LL_ENDL; } - if (target.mDepth) + if (target.mDepth.notNull() || target.mSharedDepth.notNull()) { LL_ERRS() << "Attempting to override existing depth buffer. Detach existing buffer first." << LL_ENDL; } @@ -366,132 +385,95 @@ void LLRenderTarget::shareDepthBuffer(LLRenderTarget& target) LL_ERRS() << "Attempting to override existing shared depth buffer. Detach existing buffer first." << LL_ENDL; } - if (mDepth) + if (mDepth.notNull()) { glBindFramebuffer(GL_FRAMEBUFFER, target.mFBO); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), mDepth, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), mDepth->getTexNameRaw(), 0); check_framebuffer_status(); glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); + // Borrow the depth image: a non-owning shared reference keeps it alive + // for the borrower regardless of which RT releases first. + target.mSharedDepth = mDepth; target.mUseDepth = true; } } -void LLRenderTarget::release() -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; - llassert(!isBoundInStack()); - - LLUniqueLease lease = getUniqueLease(); - release_locked(); -} - -namespace +void LLRenderTarget::setNeedsCrossContextSync(bool b) { - // Wrap a fresh fence in a shared_ptr that deletes it when the last - // reference drops. Sync objects belong to the share group, so either - // context can do the delete. - std::shared_ptr make_fence() + // The color attachment (mTex[0]) is the cross-context primitive: it carries + // the lease (sample = shared, resize = unique) and the fence. The RT itself + // stays non-leasable - it just wraps this texture in an FBO to draw into. + if (!mTex.empty() && mTex[0].notNull()) { - return std::shared_ptr( - glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), - [](void* sync) { if (sync) glDeleteSync((GLsync)sync); }); + mTex[0]->setNeedsCrossContextSync(b); } } -void LLRenderTarget::placeFrameCompleteFence() +bool LLRenderTarget::needsCrossContextSync() const { - if (!mCrossSync) return; - std::shared_ptr fence = make_fence(); - std::lock_guard lock(mCrossSync->fenceMutex); - mCrossSync->frameCompleteFence = std::move(fence); + return !mTex.empty() && mTex[0].notNull() && mTex[0]->needsCrossContextSync(); } -void LLRenderTarget::waitFrameCompleteFence() +LLImageGL* LLRenderTarget::getColorAttachmentImage() const { - if (!mCrossSync) return; - std::shared_ptr fence; + // Null unless this RT opted in - every attachment is an LLImageGL now, but + // only opted-in ones expose the fence. Callers (compositor, mailbox) treat + // null as "single-context, no cross-context sync". + if (!mTex.empty() && mTex[0].notNull() && mTex[0]->needsCrossContextSync()) { - std::lock_guard lock(mCrossSync->fenceMutex); - fence = mCrossSync->frameCompleteFence; + return mTex[0].get(); } - if (fence) - { - // Server-side wait so we don't stall the CPU. Holding the - // shared_ptr keeps the sync alive even if the producer swaps in - // a new one mid-wait. - glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); - } -} - -void LLRenderTarget::placeReadCompleteFence() -{ - if (!mCrossSync) return; - std::shared_ptr fence = make_fence(); - // Flush so the other context can actually see the fence. - glFlush(); - std::lock_guard lock(mCrossSync->fenceMutex); - mCrossSync->readCompleteFence = std::move(fence); + return nullptr; } -void LLRenderTarget::waitReadCompleteFence() +void LLRenderTarget::release() { - if (!mCrossSync) return; - std::shared_ptr fence; - { - std::lock_guard lock(mCrossSync->fenceMutex); - fence = mCrossSync->readCompleteFence; - } - if (fence) - { - glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); - } -} + LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; + llassert(!isBoundInStack()); -void LLRenderTarget::release_locked() -{ mIsSwapChainImage = false; - // Drops the fences; glDeleteSync runs when the last holder lets go. - mCrossSync.reset(); - - if (mDepth) - { - LLImageGL::deleteTextures(1, &mDepth); - - mDepth = 0; - - sBytesAllocated -= mResX*mResY*4; - } - else if (mFBO) + // Detach every attachment from the FBO first, so dropping the attachment + // images below (which may free their GL names) can't leave the FBO + // referencing a freed, recycled name. + if (mFBO) { glBindFramebuffer(GL_FRAMEBUFFER, mFBO); - if (mUseDepth) + if (mUseDepth || mDepth.notNull()) { glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, LLTexUnit::getInternalType(mUsage), 0, 0); - mUseDepth = false; + } + for (U32 z = 0; z < mTex.size(); ++z) + { + glFramebufferTexture2D(GL_FRAMEBUFFER, static_cast(GL_COLOR_ATTACHMENT0+z), LLTexUnit::getInternalType(mUsage), 0, 0); } glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); } - if (mFBO && (mTex.size() > 1)) + // Account for the freed VRAM. Only owned attachments incremented + // sBytesAllocated: a borrowed shared depth (mSharedDepth) never did, and a + // borrowed color (setColorAttachment) is detached via releaseColorAttachment + // before release, so mTex is empty here for those RTs. + sBytesAllocated -= (S32)mTex.size() * mResX * mResY * 4; + if (mDepth.notNull()) { - glBindFramebuffer(GL_FRAMEBUFFER, mFBO); - size_t z; - for (z = mTex.size() - 1; z >= 1; z--) - { - sBytesAllocated -= mResX*mResY*4; - glFramebufferTexture2D(GL_FRAMEBUFFER, static_cast(GL_COLOR_ATTACHMENT0+z), LLTexUnit::getInternalType(mUsage), 0, 0); - LLImageGL::deleteTextures(1, &mTex[z]); - } - glBindFramebuffer(GL_FRAMEBUFFER, sCurFBO); + sBytesAllocated -= mResX * mResY * 4; } + // Drop the attachment images. Owned images (mExternalTexture=false) free + // their GL name in ~LLImageGL; borrowed ones (setColorAttachment color, the + // shared depth) survive because their owner still holds a reference. + mTex.clear(); + mDepth = nullptr; + mSharedDepth = nullptr; + mUseDepth = false; + if (mFBO) { if (mFBO == sCurFBO) @@ -504,13 +486,6 @@ void LLRenderTarget::release_locked() mFBO = 0; } - if (mTex.size() > 0) - { - sBytesAllocated -= mResX*mResY*4; - LLImageGL::deleteTextures(1, &mTex[0]); - } - - mTex.clear(); mInternalFormat.clear(); mResX = mResY = 0; @@ -522,12 +497,6 @@ void LLRenderTarget::bindTarget() llassert(!isBoundInStack()); llassert(mFBO); - LLUniqueLease lease = getUniqueLease(); - bindTarget_locked(); -} - -void LLRenderTarget::bindTarget_locked() -{ glBindFramebuffer(GL_FRAMEBUFFER, mFBO); sCurFBO = mFBO; @@ -576,8 +545,6 @@ void LLRenderTarget::clear(U32 mask_in) LL_PROFILE_GPU_ZONE("clear"); llassert(mFBO); - LLUniqueLease lease = getUniqueLease(); - U32 mask = GL_COLOR_BUFFER_BIT; if (mUseDepth) { @@ -599,36 +566,27 @@ void LLRenderTarget::clear(U32 mask_in) } } -LLScopedTexName LLRenderTarget::getTexture(U32 attachment) const +U32 LLRenderTarget::getTexture(U32 attachment) const { - LLSharedLease lease = getSharedLease(); - if (attachment >= mTex.size()) + if (attachment >= mTex.size() || mTex[attachment].isNull()) { LL_WARNS() << "Invalid attachment index " << attachment << " for size " << mTex.size() << LL_ENDL; llassert(false); - return LLScopedTexName(std::move(lease), 0); + return 0; } - LLGLuint name = mTex[attachment]; - return LLScopedTexName(std::move(lease), name); + return mTex[attachment]->getTexNameRaw(); } U32 LLRenderTarget::getNumTextures() const { - LLSharedLease lease = getSharedLease(); return static_cast(mTex.size()); } void LLRenderTarget::bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options) { - LLSharedLease lease = getSharedLease(); - bindTexture_locked(index, channel, filter_options); -} - -void LLRenderTarget::bindTexture_locked(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options) -{ - // Read mTex directly - calling getTexture() here would re-enter the - // lease we already hold. - LLGLuint name = (index < mTex.size()) ? mTex[index] : 0; + // Context-local sampling. Cross-context sampling goes through + // getColorAttachmentImage()->getTexName() with the real lease. + LLGLuint name = (index < mTex.size() && mTex[index].notNull()) ? mTex[index]->getTexNameRaw() : 0; gGL.getTexUnit(channel)->bindManual(mUsage, name, filter_options == LLTexUnit::TFO_TRILINEAR || filter_options == LLTexUnit::TFO_ANISOTROPIC); gGL.getTexUnit(channel)->setTextureFilteringOption(filter_options); } @@ -641,16 +599,10 @@ void LLRenderTarget::flush() llassert(mFBO); llassert(sCurFBO == mFBO); - LLUniqueLease lease = getUniqueLease(); - flush_locked(); -} - -void LLRenderTarget::flush_locked() -{ if (mGenerateMipMaps == LLTexUnit::TMG_AUTO) { LL_PROFILE_GPU_ZONE("rt generate mipmaps"); - bindTexture_locked(0, 0, LLTexUnit::TFO_TRILINEAR); + bindTexture(0, 0, LLTexUnit::TFO_TRILINEAR); glGenerateMipmap(GL_TEXTURE_2D); } @@ -665,10 +617,8 @@ void LLRenderTarget::flush_locked() if (mIsSwapChainImage) { - // Bottom of the stack for a render batch. Just pop the bookkeeping - // and leave the FBO bound - LLSwapChain::present() will rebind FBO 0 - // for the blit, and the next acquireNextImage() will set up the next - // image's bind. + // Bottom of the stack for a render batch. Pop the bookkeeping and leave + // the framebuffer bound for the next bind. sBoundTarget = nullptr; return; } @@ -693,7 +643,6 @@ void LLRenderTarget::flush_locked() void LLRenderTarget::bindForRead() { - LLSharedLease lease = getSharedLease(); llassert(mFBO); glBindFramebuffer(GL_READ_FRAMEBUFFER, mFBO); glReadBuffer(GL_COLOR_ATTACHMENT0); @@ -708,13 +657,11 @@ void LLRenderTarget::unbindRead() bool LLRenderTarget::isComplete() const { - LLSharedLease lease = getSharedLease(); - return !mTex.empty() || mDepth; + return !mTex.empty() || mDepth.notNull(); } void LLRenderTarget::getViewport(S32* viewport) { - LLSharedLease lease = getSharedLease(); viewport[0] = 0; viewport[1] = 0; viewport[2] = mResX; @@ -736,10 +683,6 @@ bool LLRenderTarget::isBoundInStack() const void LLRenderTarget::swapFBORefs(LLRenderTarget& other) { - // Two-resource lock: this then other (consistent order). - LLUniqueLease lease_this = getUniqueLease(); - LLUniqueLease lease_other = other.getUniqueLease(); - llassert(mFBO); llassert(other.mFBO); @@ -763,32 +706,11 @@ void LLRenderTarget::swapFBORefs(LLRenderTarget& other) std::swap(mTex, other.mTex); } -// --------------------------------------------------------------------------- -// Simple scalar readers - each takes its own shared lease. -// --------------------------------------------------------------------------- - -U32 LLRenderTarget::getWidth() const -{ - LLSharedLease lease = getSharedLease(); - return mResX; -} - -U32 LLRenderTarget::getHeight() const -{ - LLSharedLease lease = getSharedLease(); - return mResY; -} - -LLTexUnit::eTextureType LLRenderTarget::getUsage() const -{ - LLSharedLease lease = getSharedLease(); - return mUsage; -} - U32 LLRenderTarget::getDepth() const { - LLSharedLease lease = getSharedLease(); - return mDepth; + // Raw GL name for the one external depth-sampling caller (llrender.cpp). + // 0 for a borrower (mDepth null), matching the previous behavior. + return mDepth.notNull() ? mDepth->getTexNameRaw() : 0; } U32 LLRenderTarget::getFBO() const @@ -800,12 +722,10 @@ U32 LLRenderTarget::getFBO() const void LLRenderTarget::markAsSwapChainImage(bool yes) { - LLUniqueLease lease = getUniqueLease(); mIsSwapChainImage = yes; } bool LLRenderTarget::isSwapChainImage() const { - LLSharedLease lease = getSharedLease(); return mIsSwapChainImage; } diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index afded7ea7a9..35d76158f07 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -62,8 +62,10 @@ */ -// Inherits LLGPUResource: mutators take a unique lease internally, readers -// a shared one. getTexture and getFBO are a bit different - see their docs. +// Inherits LLGPUResource as dormant scaffolding but is opted out of leasing +// (setLeaseEnabled(false) in the ctor): an RT is a context-local FBO wrapper +// and synchronizes nothing itself. Cross-context sync lives on its color +// attachment (an LLImageGL) - see getColorAttachmentImage(). class LLRenderTarget : public LLGPUResource { public: @@ -94,7 +96,7 @@ class LLRenderTarget : public LLGPUResource // Movable so std::vector can reallocate. Only safe to // move when no leases are held - see LLGPUResource. - LLRenderTarget(LLRenderTarget&&) noexcept = default; + LLRenderTarget(LLRenderTarget&&) noexcept; LLRenderTarget(const LLRenderTarget&) = delete; LLRenderTarget& operator=(const LLRenderTarget&) = delete; LLRenderTarget& operator=(LLRenderTarget&&) = delete; @@ -183,16 +185,17 @@ class LLRenderTarget : public LLGPUResource void getViewport(S32* viewport); //get X resolution - U32 getWidth() const; + U32 getWidth() const { return mResX; } //get Y resolution - U32 getHeight() const; + U32 getHeight() const { return mResY; } - LLTexUnit::eTextureType getUsage() const; + LLTexUnit::eTextureType getUsage() const { return mUsage; } - // Returns a guard that owns a shared lease for the caller's scope. See - // LLScopedTexName for the safe / unsafe usage patterns. - LLScopedTexName getTexture(U32 attachment = 0) const; + // Raw GL texture name of a color attachment (0 = first). Only meaningful on + // the thread that owns this RT; for cross-context sampling go through + // getColorAttachmentImage()->getTexName() (which holds the image's lease). + U32 getTexture(U32 attachment = 0) const; U32 getNumTextures() const; U32 getDepth() const; @@ -206,33 +209,18 @@ class LLRenderTarget : public LLGPUResource void bindTexture(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options = LLTexUnit::TFO_BILINEAR); // Cross-context GPU sync. Opt in for RTs that another GL context reads - // from; everything else carries no sync state at all. - void setNeedsCrossContextSync(bool b) - { - if (b && !mCrossSync) - { - mCrossSync = std::make_shared(); - } - else if (!b) - { - mCrossSync.reset(); - } - } - bool needsCrossContextSync() const { return (bool)mCrossSync; } - - // The writer places this fence when it finishes a frame; the reader - // waits on it before sampling so it only sees complete pixels. - void placeFrameCompleteFence(); - - // Server-side wait on the writer's fence. No-op if there's no fence - // or sync is disabled. - void waitFrameCompleteFence(); - - // Reverse direction: the reader places this after sampling, and the - // writer waits on it before rendering into the RT again so the two - // don't race on the GPU. - void placeReadCompleteFence(); - void waitReadCompleteFence(); + // from: the color attachment image (mTex[0]) carries the lease + fence. + // Single-context RTs carry nothing. Call after allocate(), once the color + // attachment exists. + void setNeedsCrossContextSync(bool b); + bool needsCrossContextSync() const; + + // The cross-context color attachment, or null unless this RT opted in + // (setNeedsCrossContextSync). Callers drive the frame/read-complete fences + // on it directly (LLImageGL::placeFrameCompleteFence etc.). Null for a + // single-context RT even though every attachment is now an LLImageGL - the + // null result is the "no cross-context sync" contract callers rely on. + LLImageGL* getColorAttachmentImage() const; //flush rendering operations //must be called when rendering is complete @@ -262,12 +250,21 @@ class LLRenderTarget : public LLGPUResource protected: U32 mResX; U32 mResY; - std::vector mTex; + // Color attachments as genuine LLImageGL objects (no raw GL handles): each + // owns its GL name (mExternalTexture=false) unless supplied by + // setColorAttachment (borrowed). mTex[0] is the cross-context primitive + // when opted in - it carries the lease + fence. + std::vector> mTex; std::vector mInternalFormat; U32 mFBO; LLRenderTarget* mPreviousRT = nullptr; - U32 mDepth; + // Owned depth attachment (null on a borrower - see mSharedDepth). + LLPointer mDepth; + // Depth borrowed from another RT via shareDepthBuffer: a non-owning + // reference that keeps the shared image alive for as long as we use it, so + // release order between owner and borrower can't dangle it. + LLPointer mSharedDepth; bool mUseDepth; LLTexUnit::eTextureMipGeneration mGenerateMipMaps; U32 mMipLevels; @@ -286,31 +283,13 @@ class LLRenderTarget : public LLGPUResource // part of the broader render-state cleanup for Vulkan port prep. bool mIsSwapChainImage = false; - // Cross-context GPU sync state. Only allocated for RTs that opt in - - // a null mCrossSync means no sync. frameComplete is placed by the - // producer and waited on by the compositor; readComplete goes the - // other way. - // - // Fences are shared_ptr with glDeleteSync as the deleter, so a - // fence can't be deleted out from under a waiter that still holds a - // reference. The mutex only guards the pointer swap, never a GL call. - struct CrossContextSync - { - std::mutex fenceMutex; - std::shared_ptr frameCompleteFence; - std::shared_ptr readCompleteFence; - }; - std::shared_ptr mCrossSync; - private: - // The _locked helpers don't take a lease - the public wrappers own it. - // Internal callers use these to avoid re-entering the shared_mutex on - // the same thread. - void release_locked(); - bool addColorAttachment_locked(U32 color_fmt); - void bindTarget_locked(); - void flush_locked(); - void bindTexture_locked(U32 index, S32 channel, LLTexUnit::eTextureFilterOptions filter_options); + // Allocate an owned LLImageGL attachment of (mResX, mResY) with the given + // formats and no source pixels, leaving it bound on tex unit 0 for the + // caller to set filtering/address and attach to the FBO. NPOT-safe: it + // never calls setSize (which enforces power-of-two) - mResX/mResY are the + // RT's authoritative size. + LLPointer createAttachmentImage(U32 internal_fmt, U32 primary_fmt, U32 type, bool use_mipmaps); }; #endif diff --git a/indra/llrender/llswapchain.cpp b/indra/llrender/llswapchain.cpp index 6081d136faf..663c83fd5f5 100644 --- a/indra/llrender/llswapchain.cpp +++ b/indra/llrender/llswapchain.cpp @@ -29,6 +29,7 @@ #include "llswapchain.h" #include "llgl.h" +#include "llrendertarget.h" #include "llwindow.h" @@ -40,29 +41,12 @@ LLSwapChain::~LLSwapChain() void LLSwapChain::attachToWindow(LLWindow* window, U32 width, U32 height) { llassert(window != nullptr); - llassert(mImages.empty()); // call release() before re-attaching llassert(width > 0 && height > 0); mWindow = window; mWidth = width; mHeight = height; - // Allocate kImageCount off-screen color+depth RTs at window resolution. - // Each carries the swap-chain-image flag so its bindTarget restores the - // world-view viewport when intermediate RTs pop back to it during rendering. - mImages.resize(kImageCount); - for (auto& img : mImages) - { - if (!img.allocate(width, height, GL_RGBA, /*depth=*/true)) - { - LL_WARNS("SwapChain") << "Failed to allocate swap chain image at " - << width << "x" << height << LL_ENDL; - } - img.markAsSwapChainImage(true); - } - - mCurrentIndex = 0; - // From now on, top-level RT flushes must have a parent on the stack. // Pre-attach paths (gpu_benchmark in feature-manager init, etc.) ran // before this point and used the FBO-0 fallback inside flush(). @@ -73,82 +57,54 @@ void LLSwapChain::resize(U32 width, U32 height) { if (width == 0 || height == 0) { - return; // minimized / iconified - keep existing storage + return; // minimized / iconified - keep existing dimensions } mWidth = width; mHeight = height; - - for (auto& img : mImages) - { - img.resize(width, height); - } + // GL: the default framebuffer follows the window; nothing to reallocate. } -LLRenderTarget& LLSwapChain::acquireNextImage() +void LLSwapChain::acquireNextImage() { - llassert(!mImages.empty()); - - // Rotate to the next image. GL has no real "acquire" - the driver owns - // the back buffer rotation under SwapBuffers - so this is just structural - // cycling. Vk/XR backends will do the real WSI acquire here. - mCurrentIndex = (mCurrentIndex + 1) % (U32)mImages.size(); - return mImages[mCurrentIndex]; -} - -LLRenderTarget& LLSwapChain::getCurrentImage() -{ - llassert(!mImages.empty()); - return mImages[mCurrentIndex]; + // GL: no real acquire - the driver owns the back-buffer rotation under + // SwapBuffers. Vk/XR backends do the WSI acquire here. } -LLRenderTarget& LLSwapChain::getPreviousImage() +void LLSwapChain::bindForRender() { - llassert(!mImages.empty()); - const U32 n = (U32)mImages.size(); - const U32 prev = (mCurrentIndex + n - 1) % n; - return mImages[prev]; + // Composite straight into the window's default framebuffer (FBO 0), full + // window - zero-copy. The driver double-buffers under swapBuffers. FBO 0 is + // named only here; LLRenderTarget never knows about it. + glBindFramebuffer(GL_FRAMEBUFFER, 0); + LLRenderTarget::sCurFBO = 0; + glDrawBuffer(GL_BACK); + glReadBuffer(GL_BACK); + glViewport(0, 0, mWidth, mHeight); + LLRenderTarget::sCurResX = mWidth; + LLRenderTarget::sCurResY = mHeight; } void LLSwapChain::present() { - llassert(!mImages.empty()); llassert(mWindow != nullptr); - LLRenderTarget& img = mImages[mCurrentIndex]; - - // Blit the current image's color into FBO 0, then SwapBuffers. - // This is the only place in the codebase that names FBO 0 by literal. - { - LL_PROFILE_GPU_ZONE("swapchain present blit"); - - // Hold a shared lease on the image while we blit from it. - LLSharedLease img_lease = img.getSharedLease(); - - // Save current read FB so we don't disturb anyone else's state. - // (sCurFBO tracks the current draw FB; flush() asserts it matches.) - const U32 prev_fbo = LLRenderTarget::sCurFBO; - - glBindFramebuffer(GL_READ_FRAMEBUFFER, img.getFBO()); - glReadBuffer(GL_COLOR_ATTACHMENT0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glDrawBuffer(GL_BACK); - - glBlitFramebuffer(0, 0, (GLint)mWidth, (GLint)mHeight, - 0, 0, (GLint)mWidth, (GLint)mHeight, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - // Restore unified read+draw binding to whatever was current before. - glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); - } - + // The layers were composited straight into the back buffer (FBO 0), so + // just swap. Vk/XR backends issue the real present here. mWindow->swapBuffers(); } -void LLSwapChain::presentDirect() +void LLSwapChain::bindForRead() { - llassert(mWindow != nullptr); - mWindow->swapBuffers(); + // Read the default framebuffer (the on-screen frame) for readback. + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glReadBuffer(GL_BACK); +} + +void LLSwapChain::unbindRead() +{ + glBindFramebuffer(GL_READ_FRAMEBUFFER, LLRenderTarget::sCurFBO); + glReadBuffer(LLRenderTarget::sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); } void LLSwapChain::release() @@ -157,10 +113,7 @@ void LLSwapChain::release() // flushes don't trip the assert. LLRenderTarget::sFlushRequiresParent = false; - // LLRenderTarget destructors release GL resources. - mImages.clear(); - mWindow = nullptr; - mWidth = 0; - mHeight = 0; - mCurrentIndex = 0; + mWindow = nullptr; + mWidth = 0; + mHeight = 0; } diff --git a/indra/llrender/llswapchain.h b/indra/llrender/llswapchain.h index d4e63acc16d..0a96e9c89c0 100644 --- a/indra/llrender/llswapchain.h +++ b/indra/llrender/llswapchain.h @@ -1,6 +1,6 @@ /** * @file llswapchain.h - * @brief Backend-agnostic swap chain wrapping the window's presentation images. + * @brief Backend-agnostic presentation surface for the window. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Second Life Viewer Source Code @@ -29,79 +29,69 @@ #include "llrendertarget.h" -#include - class LLWindow; -// Owns the collection of LLRenderTargets that act as the window's presentation -// images, plus the present primitive itself. The viewer acquires an image -// each frame, renders into it via the usual LLRenderTarget interface, then -// asks the swap chain to present. +// Backend-agnostic presentation surface. The compositor acquires the surface +// each frame, binds it as the render target, composites into it, then presents. // -// GL backend (today): the chain holds N off-screen color+depth FBOs sized to -// the window. acquireNextImage() rotates the index. present() blits the -// current image's color to FBO 0 and calls swapBuffers(). The literal 0 -// only appears inside present() - viewer code never names it. +// GL backend (today): zero-copy, and there are no images to own. The present +// surface is the window's default framebuffer (FBO 0): bindForRender() binds 0 +// with the full-window viewport, present() calls swapBuffers(). The driver +// double-buffers the back buffer under SwapBuffers, so the swap chain just +// holds the window and dimensions. FBO 0 is named only here - never in +// LLRenderTarget, which stays a pure off-screen RT. // -// Vulkan / OpenXR backends (future): mImages map onto the runtime's actual -// swap chain images; acquireNextImage / present become the real WSI calls -// (vkAcquireNextImageKHR / vkQueuePresentKHR or xrAcquire/Release pairs). -// The viewer code that uses this interface doesn't change. +// Vulkan / OpenXR backends (future): own the runtime's real swap-chain images. +// acquireNextImage becomes vkAcquireNextImageKHR / xrAcquireSwapchainImage, +// bindForRender binds the acquired image's framebuffer, and present becomes +// vkQueuePresentKHR / xrReleaseSwapchainImage. The compositor interface is +// unchanged. class LLSwapChain { public: - // How many images cycle in the chain. 2 is the minimum useful number; - // there is no parallelism gain on GL above 2 (the driver does its own - // back-buffer rotation under SwapBuffers). - static constexpr U32 kImageCount = 2; - LLSwapChain() = default; ~LLSwapChain(); LLSwapChain(const LLSwapChain&) = delete; LLSwapChain& operator=(const LLSwapChain&) = delete; - // GL backend: allocates kImageCount off-screen color+depth RTs at - // (width, height) and remembers the window so present() can swapBuffers. + // GL: remember the window so present() can swapBuffers; record dimensions. void attachToWindow(LLWindow* window, U32 width, U32 height); - // Resize all images to (width, height). No-op on zero dimensions. + // Update the presentation dimensions. GL: FBO 0 follows the window, so this + // just records the size - nothing to reallocate. No-op on zero dimensions. void resize(U32 width, U32 height); - // Rotate to the next image in the chain and return it. Render into the - // returned RT this frame; call present() when done. Each call to - // acquireNextImage() should be balanced 1:1 with a present(). - LLRenderTarget& acquireNextImage(); + // Acquire the next presentable image. GL: structural - the driver owns the + // back-buffer rotation under SwapBuffers. Vk/XR: the real WSI acquire. + // Balance 1:1 with present(). + void acquireNextImage(); - // Image currently being rendered into (or just rendered). Use for - // readback consumers that want the in-progress frame. - LLRenderTarget& getCurrentImage(); + // Bind the acquired image as the render target for compositing. GL: binds + // the default framebuffer (FBO 0) with the full-window viewport. + void bindForRender(); - // Most recently presented image - i.e. the one whose contents are on - // screen right now. Useful for consumers (scene monitor) that want the - // last visible frame rather than the half-rendered current one. - LLRenderTarget& getPreviousImage(); - - // Blit the current image's color to FBO 0 and call window->swapBuffers(). + // Present the composited image. GL: swapBuffers(). Vk/XR: the real present. void present(); - // SwapBuffers only - for callers (LLCompositor's shader composite) - // that have already drawn the frame directly into FBO 0. - void presentDirect(); + // Bind the present surface as GL_READ_FRAMEBUFFER for readback consumers + // (the scene monitor reading the on-screen frame). Pair with unbindRead(). + void bindForRead(); + void unbindRead(); - // Drop image resources and detach. Safe to call redundantly. + // Drop resources and detach. Safe to call redundantly. void release(); + // True once attachToWindow has run (and before release). + bool isAttached() const { return mWindow != nullptr; } + U32 getWidth() const { return mWidth; } U32 getHeight() const { return mHeight; } - U32 getImageCount() const { return (U32)mImages.size(); } private: - std::vector mImages; - U32 mCurrentIndex = 0; - LLWindow* mWindow = nullptr; - U32 mWidth = 0; - U32 mHeight = 0; + LLWindow* mWindow = nullptr; + U32 mWidth = 0; + U32 mHeight = 0; }; #endif // LL_LLSWAPCHAIN_H diff --git a/indra/llrender/lltestsquarecompositable.cpp b/indra/llrender/lltestsquarecompositable.cpp index 7f0380adf04..4afdf04de4e 100644 --- a/indra/llrender/lltestsquarecompositable.cpp +++ b/indra/llrender/lltestsquarecompositable.cpp @@ -29,6 +29,7 @@ #include "llcompositor.h" #include "llrender.h" +#include "llimagegl.h" #include "llwindow.h" #include @@ -59,22 +60,16 @@ LLTestSquareCompositable::~LLTestSquareCompositable() void LLTestSquareCompositable::connect(LLCompositor& compositor) { - // Runs on the compositor's thread at the end of every present. tryPush - // means a full queue just drops the tick - we never block the - // compositor. - mSyncConnection = compositor.onSync( - [this](U64 frame_index) { mTicks.tryPush(frame_index); }); - + mCompositor = &compositor; start(); } void LLTestSquareCompositable::disconnect() { - mSyncConnection.disconnect(); if (!isStopped()) { setQuitting(); - mTicks.tryPush(kQuitTick); + if (mCompositor) mCompositor->wakeSyncWaiters(); // break our waitForPresent shutdown(); // joins } } @@ -92,40 +87,49 @@ void LLTestSquareCompositable::run() // First frame so the layer shows up before the first tick. paint(); - mRT.placeFrameCompleteFence(); - glFlush(); + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->placeFrameCompleteFence(); // also flushes + } + else + { + glFlush(); + } produceFrame(); while (!isQuitting()) { - U64 tick = 0; - try - { - tick = mTicks.pop(); // blocks until the next sync tick - } - catch (const LLThreadSafeQueueInterrupt&) - { - break; // queue closed - } - - if (tick == kQuitTick || isQuitting()) + // Pace to the compositor's present clock; the should_stop predicate + + // wakeSyncWaiters (on disconnect) break the wait promptly. + U64 tick = mCompositor->waitForPresent(mLastTick + 1, [this]{ return isQuitting(); }); + if (tick <= mLastTick || isQuitting()) { - break; + break; // interrupted (disconnect / shutdown) } + mLastTick = tick; if (tick % mInterval == 0) { // Don't overwrite pixels the compositor may still be // copying - GPU-side wait on its read-complete fence. - mRT.waitReadCompleteFence(); + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->waitReadCompleteFence(); + } mStep = (mStep + 3) % mSize; paint(); // Publish: the compositor waits on this fence before sampling. - // Flush so the other context can actually see it. - mRT.placeFrameCompleteFence(); - glFlush(); + // placeFrameCompleteFence flushes so the other context sees it. + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->placeFrameCompleteFence(); + } + else + { + glFlush(); + } produceFrame(); } } @@ -170,8 +174,8 @@ void LLTestSquareCompositable::paint() } } - LLScopedTexName tex = mRT.getTexture(0); - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, tex.get()); + U32 tex = mRT.getTexture(0); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, tex); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)mSize, (GLsizei)mSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); diff --git a/indra/llrender/lltestsquarecompositable.h b/indra/llrender/lltestsquarecompositable.h index 8c29ddb0df6..07b003bb2e8 100644 --- a/indra/llrender/lltestsquarecompositable.h +++ b/indra/llrender/lltestsquarecompositable.h @@ -30,9 +30,6 @@ #include "llcompositable.h" #include "llrendertarget.h" #include "llthread.h" -#include "llthreadsafequeue.h" - -#include class LLCompositor; class LLWindow; @@ -40,8 +37,8 @@ class LLWindow; // Compositor bring-up test layer, and a miniature model of the real // producer architecture: a dedicated thread with its own shared GL context // paints a sweeping bar into its own RT and publishes it through the -// cross-context fence pair, paced by the compositor's sync signal -// delivered over a queue. +// cross-context fence pair, paced to the compositor's present clock via +// waitForPresent. class LLTestSquareCompositable : public LLCompositable, public LLThread { public: @@ -56,7 +53,7 @@ class LLTestSquareCompositable : public LLCompositable, public LLThread U32 size = 100); ~LLTestSquareCompositable() override; - // Subscribe to the compositor's sync signal and start the producer + // Store the compositor (for the present clock) and start the producer // thread. Call on the compositor's thread. void connect(LLCompositor& compositor); @@ -71,16 +68,14 @@ class LLTestSquareCompositable : public LLCompositable, public LLThread protected: // - LLThread ------------------------------------------------------- - // Producer loop: make the shared context current, allocate the RT, - // then consume sync ticks until the quit sentinel arrives. + // Producer loop: make the shared context current, allocate the RT, then + // pace to the compositor's present clock until disconnect. void run() override; private: // Fill the RT's color texture: solid color + sweep bar at mStep. void paint(); - static constexpr U64 kQuitTick = ~U64(0); - LLRenderTarget mRT; std::string mName; U8 mColor[3]; @@ -93,12 +88,8 @@ class LLTestSquareCompositable : public LLCompositable, public LLThread LLWindow* mWindow; void* mContext; - // Sync ticks from the compositor. Bounded, and the handler uses - // tryPush, so a slow square never blocks the compositor - extra - // ticks just drop. - LLThreadSafeQueue mTicks{64}; - - boost::signals2::connection mSyncConnection; + LLCompositor* mCompositor = nullptr; // present clock (waitForPresent) + U64 mLastTick = 0; // last present index we paced to }; #endif // LL_LLTESTSQUARECOMPOSITABLE_H diff --git a/indra/llrender/lltexture.h b/indra/llrender/lltexture.h index 35cded86f22..8c1d0ae6f5b 100644 --- a/indra/llrender/lltexture.h +++ b/indra/llrender/lltexture.h @@ -42,7 +42,7 @@ class LLFontGL ; // //this is an abstract class as the parent for the class LLGLTexture // -class LLTexture : public virtual LLRefCount +class LLTexture : public virtual LLThreadSafeRefCount { friend class LLTexUnit ; friend class LLFontGL ; diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h index f2e4e7ddacd..11b60c1cda6 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -221,6 +221,12 @@ class LLWindow : public LLInstanceTracker // it between a base and boost rate). Computed as a side effect of // getRefreshRate() where supported. virtual bool isDynamicRefreshRate() { return false; } + + // Run any AppKit/OS-main-only work the viewer thread deferred (e.g. + // cursor changes on macOS). Called on the OS main thread from the + // frame loop. Default no-op: platforms with their own window thread + // (Windows) don't use this. + virtual void drainOSMainQueue() {} protected: LLWindow(LLWindowCallbacks* callbacks, bool fullscreen, U32 flags); virtual ~LLWindow(); diff --git a/indra/llwindow/llwindowmacosx-objc.h b/indra/llwindow/llwindowmacosx-objc.h index ec9afb18448..51de10befad 100644 --- a/indra/llwindow/llwindowmacosx-objc.h +++ b/indra/llwindow/llwindowmacosx-objc.h @@ -31,6 +31,7 @@ #include #include #include +#include //fir CGSize #include @@ -107,8 +108,14 @@ NSWindowRef createNSWindow(int x, int y, int width, int height); #include +// Run func() inside an @autoreleasepool. For threads with no Cocoa run +// loop (the viewer thread) so autoreleased ObjC objects don't leak. +void ll_macos_run_in_autorelease_pool(const std::function& func); + GLViewRef createOpenGLView(NSWindowRef window, unsigned int samples, bool vsync); -void glSwapBuffers(void* context); +unsigned int getWindowDisplayID(NSWindowRef window); +double getWindowRefreshRate(NSWindowRef window); +bool isWindowDynamicRefresh(NSWindowRef window); CGLContextObj getCGLContextObj(GLViewRef view); unsigned long getVramSize(GLViewRef view); float getDeviceUnitSize(GLViewRef view); diff --git a/indra/llwindow/llwindowmacosx-objc.mm b/indra/llwindow/llwindowmacosx-objc.mm index d902a82a3c6..df6ea7ab8de 100644 --- a/indra/llwindow/llwindowmacosx-objc.mm +++ b/indra/llwindow/llwindowmacosx-objc.mm @@ -238,9 +238,91 @@ GLViewRef createOpenGLView(NSWindowRef window, unsigned int samples, bool vsync) return glview; } -void glSwapBuffers(void* context) +// CGDirectDisplayID of the screen the window currently occupies. Falls +// back to the main display if the window isn't on a screen yet. +unsigned int getWindowDisplayID(NSWindowRef window) { - [(NSOpenGLContext*)context flushBuffer]; + NSScreen *screen = [(LLNSWindow*)window screen]; + NSNumber *num = screen ? screen.deviceDescription[@"NSScreenNumber"] : nil; + return num ? (unsigned int)[num unsignedIntValue] : (unsigned int)CGMainDisplayID(); +} + +// Refresh rate of the window's current display. Uses the fixed display +// mode rate when available; ProMotion / built-in panels report 0 there, +// so fall back to the screen's maximum frames-per-second. Returns 0 if +// nothing usable is found. +double getWindowRefreshRate(NSWindowRef window) +{ + CGDirectDisplayID did = (CGDirectDisplayID)getWindowDisplayID(window); + + double rate = 0.0; + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did); + if (mode) + { + rate = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + } + + if (rate == 0.0) + { + if (@available(macOS 12.0, *)) + { + NSScreen *screen = [(LLNSWindow*)window screen]; + if (screen) + { + rate = (double)screen.maximumFramesPerSecond; + } + } + } + + return rate; +} + +// True when the window's display runs a variable refresh rate (ProMotion). +// macOS 14 exposes the min/max refresh interval directly; older systems +// fall back to comparing the screen's max FPS against the fixed mode rate. +bool isWindowDynamicRefresh(NSWindowRef window) +{ + NSScreen *screen = [(LLNSWindow*)window screen]; + if (!screen) + { + return false; + } + + if (@available(macOS 14.0, *)) + { + return screen.minimumRefreshInterval != screen.maximumRefreshInterval; + } + + if (@available(macOS 12.0, *)) + { + CGDirectDisplayID did = (CGDirectDisplayID)getWindowDisplayID(window); + double fixed_rate = 0.0; + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did); + if (mode) + { + fixed_rate = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + } + const double max_fps = (double)screen.maximumFramesPerSecond; + // A boost rate above the fixed mode rate (or a panel that reports + // no fixed rate but can exceed 60) implies a dynamic display. + return (fixed_rate > 0.0) ? (max_fps > fixed_rate) : (max_fps > 60.0); + } + + return false; +} + +void ll_macos_run_in_autorelease_pool(const std::function& func) +{ + // The viewer thread has no Cocoa run loop, so nothing drains an + // autorelease pool for it. Wrap each unit of work so transient ObjC + // objects (fonts, file paths, clipboard, etc.) are released promptly + // instead of leaking until the thread exits. + @autoreleasepool + { + func(); + } } CGLContextObj getCGLContextObj(GLViewRef view) diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp index 1dd795cd3a4..a0b2ae83b3b 100644 --- a/indra/llwindow/llwindowmacosx.cpp +++ b/indra/llwindow/llwindowmacosx.cpp @@ -36,12 +36,14 @@ #include "llgl.h" #include "llstring.h" #include "lldir.h" +#include "llthread.h" #include "indra_constants.h" #include #include #include #include +#include #include #include "llwindowmacosx_iokit.h" @@ -66,43 +68,6 @@ namespace // LLWindowMacOSX // -bool LLWindowMacOSX::sUseMultGL = false; - -//static -void LLWindowMacOSX::setUseMultGL(bool use_mult_gl) -{ - bool was_enabled = sUseMultGL; - - sUseMultGL = use_mult_gl; - - if (gGLManager.mInited) - { - CGLContextObj ctx = CGLGetCurrentContext(); - //enable multi-threaded OpenGL (whether or not sUseMultGL actually changed) - if (sUseMultGL) - { - CGLError cgl_err; - - cgl_err = CGLEnable( ctx, kCGLCEMPEngine); - - if (cgl_err != kCGLNoError ) - { - LL_INFOS("GLInit") << "Multi-threaded OpenGL not available." << LL_ENDL; - sUseMultGL = false; - } - else - { - LL_INFOS("GLInit") << "Multi-threaded OpenGL enabled." << LL_ENDL; - } - } - else if (was_enabled) - { - CGLDisable( ctx, kCGLCEMPEngine); - LL_INFOS("GLInit") << "Multi-threaded OpenGL disabled." << LL_ENDL; - } - } -} - // Cross-platform bits: bool check_for_card(const char* RENDERER, const char* bad_card) @@ -246,14 +211,30 @@ LLWindowMacOSX::LLWindowMacOSX(LLWindowCallbacks* callbacks, bool callKeyUp(NSKeyEventRef event, unsigned short key, unsigned int mask) { - mRawKeyEvent = event; - bool retVal = gKeyboard->handleKeyUp(key, mask); - mRawKeyEvent = NULL; - return retVal; + if (!gWindowImplementation || !event) + { + return false; + } + // Defer the deep handler to the viewer thread. Capture the native key + // data by value - the caller's NativeKeyEventData is a stack temporary + // that's gone by the time the queue is drained. mRawKeyEvent points at + // the captured copy only while the handler runs (drained serially). + const NativeKeyEventData data = *event; + gWindowImplementation->post([=]() + { + mRawKeyEvent = &data; + gKeyboard->handleKeyUp(key, mask); + mRawKeyEvent = NULL; + }); + return false; } bool callKeyDown(NSKeyEventRef event, unsigned short key, unsigned int mask, wchar_t character) { + if (!gWindowImplementation || !event) + { + return false; + } //if (mask!=MASK_NONE) { if((key == gKeyboard->inverseTranslateKey('Z')) && (character == 'y')) @@ -266,15 +247,33 @@ bool callKeyDown(NSKeyEventRef event, unsigned short key, unsigned int mask, wch } } - mRawKeyEvent = event; - bool retVal = gKeyboard->handleKeyDown(key, mask); - mRawKeyEvent = NULL; - return retVal; + const NativeKeyEventData data = *event; + gWindowImplementation->post([=]() + { + mRawKeyEvent = &data; + gKeyboard->handleKeyDown(key, mask); + mRawKeyEvent = NULL; + }); + + // keyDown: needs a synchronous accepts-text answer to decide IME + // routing, but the real handler runs later on the viewer thread. Use + // whether a text control currently holds focus as the proxy (kept on + // the OS main thread). IME composition is intentionally OS-main-only. + return gWindowImplementation->allowsLanguageInput(); } void callResetKeys() { - gKeyboard->resetKeys(); + if (!gWindowImplementation) + { + gKeyboard->resetKeys(); + return; + } + // Keep ordering with the deferred key events. + gWindowImplementation->post([]() + { + gKeyboard->resetKeys(); + }); } bool callUnicodeCallback(wchar_t character, unsigned int mask) @@ -295,18 +294,23 @@ bool callUnicodeCallback(wchar_t character, unsigned int mask) eventData.mEventUnmodChars = character; eventData.mEventRepeat = false; - mRawKeyEvent = &eventData; - - bool result = gWindowImplementation->getCallbacks()->handleUnicodeChar(character, mask); - mRawKeyEvent = NULL; - return result; + gWindowImplementation->post([=]() + { + mRawKeyEvent = &eventData; + gWindowImplementation->getCallbacks()->handleUnicodeChar(character, mask); + mRawKeyEvent = NULL; + }); + return false; } void callFocus() { if (gWindowImplementation && gWindowImplementation->getCallbacks()) { - gWindowImplementation->getCallbacks()->handleFocus(gWindowImplementation); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleFocus(gWindowImplementation); + }); } } @@ -314,7 +318,10 @@ void callFocusLost() { if (gWindowImplementation && gWindowImplementation->getCallbacks()) { - gWindowImplementation->getCallbacks()->handleFocusLost(gWindowImplementation); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleFocusLost(gWindowImplementation); + }); } } @@ -324,6 +331,7 @@ void callRightMouseDown(float *pos, MASK mask) { return; } + // IME commit touches AppKit and must stay on the OS main thread. if (gWindowImplementation->allowsLanguageInput()) { gWindowImplementation->interruptLanguageTextInput(); @@ -332,7 +340,11 @@ void callRightMouseDown(float *pos, MASK mask) LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); - gWindowImplementation->getCallbacks()->handleRightMouseDown(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleRightMouseDown(gWindowImplementation, outCoords, m); + }); } void callRightMouseUp(float *pos, MASK mask) @@ -349,7 +361,11 @@ void callRightMouseUp(float *pos, MASK mask) LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); - gWindowImplementation->getCallbacks()->handleRightMouseUp(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleRightMouseUp(gWindowImplementation, outCoords, m); + }); } void callLeftMouseDown(float *pos, MASK mask) @@ -366,7 +382,11 @@ void callLeftMouseDown(float *pos, MASK mask) LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); - gWindowImplementation->getCallbacks()->handleMouseDown(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleMouseDown(gWindowImplementation, outCoords, m); + }); } void callLeftMouseUp(float *pos, MASK mask) @@ -383,8 +403,11 @@ void callLeftMouseUp(float *pos, MASK mask) LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); - gWindowImplementation->getCallbacks()->handleMouseUp(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); - + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleMouseUp(gWindowImplementation, outCoords, m); + }); } void callDoubleClick(float *pos, MASK mask) @@ -401,14 +424,21 @@ void callDoubleClick(float *pos, MASK mask) LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); - gWindowImplementation->getCallbacks()->handleDoubleClick(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleDoubleClick(gWindowImplementation, outCoords, m); + }); } void callResize(unsigned int width, unsigned int height) { if (gWindowImplementation && gWindowImplementation->getCallbacks()) { - gWindowImplementation->getCallbacks()->handleResize(gWindowImplementation, width, height); + gWindowImplementation->post([=]() + { + gWindowImplementation->getCallbacks()->handleResize(gWindowImplementation, width, height); + }); } } @@ -418,6 +448,9 @@ void callMouseMoved(float *pos, MASK mask) { return; } + // Stash the latest position (deltas applied here, on the OS main + // thread, so the delta accumulators never cross threads). gatherInput + // emits one handleMouseMove per frame from this, matching win32. LLCoordGL outCoords; outCoords.mX = ll_round(pos[0]); outCoords.mY = ll_round(pos[1]); @@ -425,8 +458,7 @@ void callMouseMoved(float *pos, MASK mask) gWindowImplementation->getMouseDeltas(deltas); outCoords.mX += deltas[0]; outCoords.mY += deltas[1]; - gWindowImplementation->getCallbacks()->handleMouseMove(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); - //gWindowImplementation->getCallbacks()->handleScrollWheel(gWindowImplementation, 0); + gWindowImplementation->setLatestMousePos(outCoords); } void callMouseDragged(float *pos, MASK mask) @@ -442,15 +474,22 @@ void callMouseDragged(float *pos, MASK mask) gWindowImplementation->getMouseDeltas(deltas); outCoords.mX += deltas[0]; outCoords.mY += deltas[1]; - gWindowImplementation->getCallbacks()->handleMouseDragged(gWindowImplementation, outCoords, gKeyboard->currentMask(true)); + const MASK m = gKeyboard->currentMask(true); + gWindowImplementation->postMouseButtonEvent([=]() + { + gWindowImplementation->getCallbacks()->handleMouseDragged(gWindowImplementation, outCoords, m); + }); } void callScrollMoved(float deltaX, float deltaY) { if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) { - gWindowImplementation->getCallbacks()->handleScrollHWheel(gWindowImplementation, deltaX); - gWindowImplementation->getCallbacks()->handleScrollWheel(gWindowImplementation, deltaY); + gWindowImplementation->post([=]() + { + gWindowImplementation->getCallbacks()->handleScrollHWheel(gWindowImplementation, deltaX); + gWindowImplementation->getCallbacks()->handleScrollWheel(gWindowImplementation, deltaY); + }); } } @@ -460,14 +499,20 @@ void callMouseExit() { return; } - gWindowImplementation->getCallbacks()->handleMouseLeave(gWindowImplementation); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleMouseLeave(gWindowImplementation); + }); } void callWindowFocus() { if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) { - gWindowImplementation->getCallbacks()->handleFocus (gWindowImplementation); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleFocus(gWindowImplementation); + }); } else { @@ -481,7 +526,10 @@ void callWindowUnfocus() { if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) { - gWindowImplementation->getCallbacks()->handleFocusLost(gWindowImplementation); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleFocusLost(gWindowImplementation); + }); } } @@ -489,7 +537,10 @@ void callWindowHide() { if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) { - gWindowImplementation->getCallbacks()->handleActivate(gWindowImplementation, false); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleActivate(gWindowImplementation, false); + }); } } @@ -497,15 +548,28 @@ void callWindowUnhide() { if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) { - gWindowImplementation->getCallbacks()->handleActivate(gWindowImplementation, true); + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleActivate(gWindowImplementation, true); + }); } } void callWindowDidChangeScreen() { - if ( gWindowImplementation && gWindowImplementation->getCallbacks() ) + if ( gWindowImplementation ) { - gWindowImplementation->getCallbacks()->handleWindowDidChangeScreen(gWindowImplementation); + // The window may have moved to a display with a different refresh + // rate; recompute before the rest of the viewer reacts. + gWindowImplementation->updateRefreshRate(); + + if ( gWindowImplementation->getCallbacks() ) + { + gWindowImplementation->post([]() + { + gWindowImplementation->getCallbacks()->handleWindowDidChangeScreen(gWindowImplementation); + }); + } } } @@ -532,14 +596,17 @@ void callOtherMouseDown(float *pos, MASK mask, int button) outCoords.mX += deltas[0]; outCoords.mY += deltas[1]; - if (button == 2) + gWindowImplementation->postMouseButtonEvent([=]() { - gWindowImplementation->getCallbacks()->handleMiddleMouseDown(gWindowImplementation, outCoords, mask); - } - else - { - gWindowImplementation->getCallbacks()->handleOtherMouseDown(gWindowImplementation, outCoords, mask, button + 1); - } + if (button == 2) + { + gWindowImplementation->getCallbacks()->handleMiddleMouseDown(gWindowImplementation, outCoords, mask); + } + else + { + gWindowImplementation->getCallbacks()->handleOtherMouseDown(gWindowImplementation, outCoords, mask, button + 1); + } + }); } void callOtherMouseUp(float *pos, MASK mask, int button) @@ -555,14 +622,17 @@ void callOtherMouseUp(float *pos, MASK mask, int button) gWindowImplementation->getMouseDeltas(deltas); outCoords.mX += deltas[0]; outCoords.mY += deltas[1]; - if (button == 2) + gWindowImplementation->postMouseButtonEvent([=]() { - gWindowImplementation->getCallbacks()->handleMiddleMouseUp(gWindowImplementation, outCoords, mask); - } - else - { - gWindowImplementation->getCallbacks()->handleOtherMouseUp(gWindowImplementation, outCoords, mask, button + 1); - } + if (button == 2) + { + gWindowImplementation->getCallbacks()->handleMiddleMouseUp(gWindowImplementation, outCoords, mask); + } + else + { + gWindowImplementation->getCallbacks()->handleOtherMouseUp(gWindowImplementation, outCoords, mask, button + 1); + } + }); } void callModifier(MASK mask) @@ -768,10 +838,6 @@ bool LLWindowMacOSX::createContext(int x, int y, int width, int height, int bits GLint numPixelFormats; CGLChoosePixelFormat (attribs, &mPixelFormat, &numPixelFormats); - - if(mPixelFormat == NULL) { - CGLChoosePixelFormat (attribs, &mPixelFormat, &numPixelFormats); - } } } @@ -793,18 +859,15 @@ bool LLWindowMacOSX::createContext(int x, int y, int width, int height, int bits } } - mRefreshRate = CGDisplayModeGetRefreshRate(CGDisplayCopyDisplayMode(mDisplay)); - if(mRefreshRate == 0) - { - //consider adding more appropriate fallback later - mRefreshRate = DEFAULT_REFRESH_RATE; - } + updateRefreshRate(); + + // Stand up the CVDisplayLink for vblank-paced presents now that + // mDisplay is resolved. setSwapInterval below keys off it. + startDisplayLink(); // Disable vertical sync for swap toggleVSync(enable_vsync); - setUseMultGL(sUseMultGL); - makeFirstResponder(mWindow, mGLView); return true; @@ -825,6 +888,8 @@ void LLWindowMacOSX::destroyContext() // We don't have a context return; } + // Stop the vblank source before tearing the context down. + stopDisplayLink(); // Unhook the GL context from any drawable it may have if(mContext != NULL) { @@ -968,8 +1033,73 @@ bool LLWindowMacOSX::getFullscreen() return mFullscreen; } +void LLWindowMacOSX::post(const std::function& func) +{ + mFunctionQueue.pushFront(func); +} + +void LLWindowMacOSX::postMouseButtonEvent(const std::function& func) +{ + mMouseQueue.pushFront(func); +} + +void LLWindowMacOSX::setLatestMousePos(const LLCoordGL& pos) +{ + mMousePos = pos; +} + +void LLWindowMacOSX::runOnOSMain(const std::function& func) +{ + if (on_os_main_thread()) + { + func(); + } + else + { + mOSMainQueue.pushFront(func); + } +} + +void LLWindowMacOSX::drainOSMainQueue() +{ + llassert(on_os_main_thread()); + std::function func; + while (mOSMainQueue.tryPopBack(func)) + { + func(); + } +} + void LLWindowMacOSX::gatherInput() { + // Runs on the viewer thread (the OS main thread captures events and + // posts the deep handlers). Drain them here, mirroring + // LLWindowWin32::gatherInput. + std::function func; + + // General events (keyboard, scroll, resize, focus) first. + while (mFunctionQueue.tryPopBack(func)) + { + func(); + } + + // One mouse-move per frame, BEFORE mouse buttons, so a click lands at + // the latest cursor position. Mask is read here on the viewer thread. + if (mMousePos != mLastMousePos) + { + if (gWindowImplementation && getCallbacks()) + { + getCallbacks()->handleMouseMove(this, mMousePos, gKeyboard->currentMask(true)); + } + mLastMousePos = mMousePos; + } + + // Mouse button presses after the move. + while (mMouseQueue.tryPopBack(func)) + { + func(); + } + updateCursor(); } @@ -1092,6 +1222,15 @@ bool LLWindowMacOSX::setSizeImpl(const LLCoordWindow size) void LLWindowMacOSX::swapBuffers() { + // Present runs on the OS main thread, which holds mContext current. + llassert(on_os_main_thread()); + // Pace to vblank off the display link (CGL's swap interval doesn't + // reliably block on macOS). interval 0 = unthrottled. + const S32 interval = mSwapInterval.load(std::memory_order_relaxed); + if (mDisplayLink && interval > 0) + { + waitForVBlanks(interval); + } CGLFlushDrawable(mContext); } @@ -1256,10 +1395,12 @@ bool LLWindowMacOSX::setCursorPosition(const LLCoordWindow position) // Under certain circumstances, this will trigger us to decouple the cursor. adjustCursorDecouple(true); - // trigger mouse move callback + // Record the warped position; gatherInput emits the move once per + // frame on the viewer thread (don't call handleMouseMove directly - + // it would run the deep handler on whatever thread warped the cursor). LLCoordGL gl_pos; convertCoords(position, &gl_pos); - mCallbacks->handleMouseMove(this, gl_pos, (MASK)0); + setLatestMousePos(gl_pos); return result; } @@ -1617,6 +1758,14 @@ static void initPixmapCursor(int cursorid, int hotspotX, int hotspotY) void LLWindowMacOSX::updateCursor() { + // NSCursor is AppKit; bounce to the OS main thread when called from + // the viewer thread (gatherInput). + if (!on_os_main_thread()) + { + runOnOSMain([this]() { updateCursor(); }); + return; + } + S32 result = 0; if (mDragOverrideCursor != -1) @@ -1786,6 +1935,11 @@ void LLWindowMacOSX::releaseMouse() void LLWindowMacOSX::hideCursor() { + if (!on_os_main_thread()) + { + runOnOSMain([this]() { hideCursor(); }); + return; + } if(!mCursorHidden) { // LL_INFOS() << "hideCursor: hiding" << LL_ENDL; @@ -1803,6 +1957,11 @@ void LLWindowMacOSX::hideCursor() void LLWindowMacOSX::showCursor() { + if (!on_os_main_thread()) + { + runOnOSMain([this]() { showCursor(); }); + return; + } if(mCursorHidden || !isCGCursorVisible()) { // LL_INFOS() << "showCursor: showing" << LL_ENDL; @@ -2534,12 +2693,11 @@ class sharedContext void* LLWindowMacOSX::createSharedContext() { sharedContext* sc = new sharedContext(); - CGLCreateContext(mPixelFormat, mContext, &(sc->mContext)); - - if (sUseMultGL) - { - CGLEnable(mContext, kCGLCEMPEngine); - } + // Share against the primary context's own pixel format so the new + // context is renderer-compatible with mContext (which is backed by an + // NSOpenGLContext, not by mPixelFormat). + CGLPixelFormatObj pixel_format = CGLGetPixelFormat(mContext); + CGLCreateContext(pixel_format, mContext, &(sc->mContext)); return (void *)sc; } @@ -2547,25 +2705,6 @@ void* LLWindowMacOSX::createSharedContext() void LLWindowMacOSX::makeContextCurrent(void* context) { CGLSetCurrentContext(((sharedContext*)context)->mContext); - - //enable multi-threaded OpenGL - if (sUseMultGL) - { - CGLError cgl_err; - CGLContextObj ctx = CGLGetCurrentContext(); - - cgl_err = CGLEnable( ctx, kCGLCEMPEngine); - - if (cgl_err != kCGLNoError ) - { - LL_INFOS("GLInit") << "Multi-threaded OpenGL not available." << LL_ENDL; - } - else - { - LL_INFOS("GLInit") << "Multi-threaded OpenGL enabled." << LL_ENDL; - } - } - } void LLWindowMacOSX::destroySharedContext(void* context) @@ -2577,21 +2716,111 @@ void LLWindowMacOSX::destroySharedContext(void* context) delete sc; } -void LLWindowMacOSX::toggleVSync(bool enable_vsync) +void LLWindowMacOSX::updateRefreshRate() { - GLint frames_per_swap = 0; - if (!enable_vsync) + if (!mWindow) { - frames_per_swap = 0; + return; } - else + + mDisplay = (CGDirectDisplayID)getWindowDisplayID(mWindow); + + const double rate = getWindowRefreshRate(mWindow); + mRefreshRate = (rate > 0.0) ? (S32)llround(rate) : DEFAULT_REFRESH_RATE; + + mIsDynamicRefresh = isWindowDynamicRefresh(mWindow); + + // Follow the window onto its new display so vblank pacing matches the + // refresh rate the window is actually presenting against. + if (mDisplayLink) { - frames_per_swap = 1; + CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)mDisplayLink, mDisplay); } +} + +// CVDisplayLink output callback - fires once per vblank on a dedicated +// high-priority CoreVideo thread. +static CVReturn sDisplayLinkCallback(CVDisplayLinkRef /*link*/, + const CVTimeStamp* /*now*/, + const CVTimeStamp* /*output*/, + CVOptionFlags /*flagsIn*/, + CVOptionFlags* /*flagsOut*/, + void* ctx) +{ + static_cast(ctx)->onVBlank(); + return kCVReturnSuccess; +} +void LLWindowMacOSX::startDisplayLink() +{ + if (mDisplayLink) + { + return; + } + CVDisplayLinkRef link = nullptr; + if (CVDisplayLinkCreateWithCGDisplay(mDisplay, &link) != kCVReturnSuccess || !link) + { + LL_WARNS("Window") << "CVDisplayLink creation failed; falling back to " + "CGL swap interval for vsync." << LL_ENDL; + mDisplayLink = nullptr; + return; + } + CVDisplayLinkSetOutputCallback(link, &sDisplayLinkCallback, this); + CVDisplayLinkStart(link); + mDisplayLink = link; +} + +void LLWindowMacOSX::stopDisplayLink() +{ + if (!mDisplayLink) + { + return; + } + CVDisplayLinkRef link = (CVDisplayLinkRef)mDisplayLink; + CVDisplayLinkStop(link); + CVDisplayLinkRelease(link); + mDisplayLink = nullptr; + // Wake any present waiting on a vblank so it doesn't block on a dead link. + mVBlankCond.notify_all(); +} + +void LLWindowMacOSX::onVBlank() +{ + { + std::lock_guard lock(mVBlankMutex); + ++mVBlankCount; + } + mVBlankCond.notify_all(); +} + +void LLWindowMacOSX::waitForVBlanks(S32 interval) +{ + std::unique_lock lock(mVBlankMutex); + const U64 target = mLastSwapVBlank + (U64)interval; + // Time out so a stalled link (display sleep, etc.) can't wedge present. + mVBlankCond.wait_for(lock, std::chrono::milliseconds(100), + [&]{ return mVBlankCount >= target; }); + // Resync to the actual count - if we fell behind we pace to the current + // vblank rather than bursting to catch up. + mLastSwapVBlank = mVBlankCount; +} + +void LLWindowMacOSX::setSwapInterval(S32 interval) +{ + mSwapInterval.store(llmax(interval, 0), std::memory_order_relaxed); + + // When the display link is driving the pacing, disable CGL's own + // (unreliable) vsync so the two don't fight. Without a link, fall back + // to the CGL swap interval - imperfect, but better than nothing. + GLint frames_per_swap = mDisplayLink ? 0 : (GLint)llmax(interval, 0); CGLSetParameter(mContext, kCGLCPSwapInterval, &frames_per_swap); } +void LLWindowMacOSX::toggleVSync(bool enable_vsync) +{ + setSwapInterval(enable_vsync ? 1 : 0); +} + void LLWindowMacOSX::interruptLanguageTextInput() { commitCurrentPreedit(mGLView); diff --git a/indra/llwindow/llwindowmacosx.h b/indra/llwindow/llwindowmacosx.h index d703a84d027..d1919f49249 100644 --- a/indra/llwindow/llwindowmacosx.h +++ b/indra/llwindow/llwindowmacosx.h @@ -32,6 +32,12 @@ #include "llwindowmacosx-objc.h" #include "lltimer.h" +#include "llthreadsafequeue.h" + +#include +#include +#include +#include #include #include @@ -133,10 +139,40 @@ class LLWindowMacOSX : public LLWindow void updateMouseDeltas(float* deltas); void getMouseDeltas(float* delta); + // Refresh mDisplay, mRefreshRate and mIsDynamicRefresh from the + // window's current display. Touches AppKit (NSScreen), so it must run + // on the OS main thread - called at context creation and on screen + // change. + void updateRefreshRate(); + void handleDragNDrop(std::string url, LLWindowCallbacks::DragNDropAction action); bool allowsLanguageInput() { return mLanguageTextInputAllowed; } + // Input deferral, mirroring LLWindowWin32. The OS main thread (which + // does window-thread duty on macOS) captures Cocoa events and posts + // the deep handlers here; gatherInput() drains them on the viewer + // thread, so input handlers run alongside render - never racing it. + // post() is for general events, postMouseButtonEvent() for mouse + // buttons (drained after the once-per-frame mouse move). + void post(const std::function& func); + void postMouseButtonEvent(const std::function& func); + + // Record the latest cursor position (called on the OS main thread + // from the mouse-moved callback). gatherInput() emits one + // handleMouseMove per frame when it changes. + void setLatestMousePos(const LLCoordGL& pos); + + // Run AppKit-only work (NSCursor, etc.) on the OS main thread. If + // we're already there it runs inline; otherwise it's queued and run + // by drainOSMainQueue(). The viewer thread reaches cursor calls via + // gatherInput, but AppKit must stay on the OS main thread. + void runOnOSMain(const std::function& func); + + // Drain queued OS-main work. Called on the OS main thread (from the + // frame loop's OS-main segment). + void drainOSMainQueue() override; + //create a new GL context that shares a namespace with this Window's main GL context and make it current on the current thread // returns a pointer to be handed back to destroySharedConext/makeContextCurrent void* createSharedContext() override; @@ -148,8 +184,13 @@ class LLWindowMacOSX : public LLWindow void toggleVSync(bool enable_vsync) override; - // enable or disable multithreaded GL - static void setUseMultGL(bool use_mult_gl); + // Swap every Nth vblank (1 = every vblank). Paces mContext, the + // context swapBuffers() flushes. + void setSwapInterval(S32 interval) override; + + // True when the window's display runs a variable refresh rate + // (ProMotion). Maintained by updateRefreshRate(). + bool isDynamicRefreshRate() override { return mIsDynamicRefresh; } protected: LLWindowMacOSX(LLWindowCallbacks* callbacks, @@ -178,6 +219,18 @@ class LLWindowMacOSX : public LLWindow private: void restoreGLContext(); + // CVDisplayLink vsync: CGLFlushDrawable doesn't reliably block on + // vblank on macOS, so the present paces off a display-link tick. + void startDisplayLink(); + void stopDisplayLink(); + void waitForVBlanks(S32 interval); // OS main; blocks until interval ticks + +public: + // Called from the CVDisplayLink callback thread, once per vblank. + void onVBlank(); + +private: + protected: // // Platform specific methods @@ -227,16 +280,38 @@ class LLWindowMacOSX : public LLWindow bool mMinimized; U32 mFSAASamples; bool mForceRebuild; + bool mIsDynamicRefresh = false; S32 mDragOverrideCursor; // Input method management through Text Service Manager. - bool mLanguageTextInputAllowed; + // Atomic: read on the OS main thread (the keyDown: accepts-text + // proxy) while it may be written by viewer-thread UI focus changes. + std::atomic mLanguageTextInputAllowed; LLPreeditor* mPreeditor; -public: - static bool sUseMultGL; + // Deferred-input queues + once-per-frame mouse-move state. Cocoa + // callbacks push on the OS main thread; gatherInput() drains on the + // viewer thread. See post()/postMouseButtonEvent(). + LLThreadSafeQueue> mFunctionQueue; + LLThreadSafeQueue> mMouseQueue; + // Latest cursor position; written only on the OS (window) thread, + // readable from anywhere. mLastMousePos is viewer-thread-only. + LLCoordGL mMousePos; + LLCoordGL mLastMousePos; + + // AppKit work marshaled back to the OS main thread (see runOnOSMain). + LLThreadSafeQueue> mOSMainQueue; + + // CVDisplayLink vsync pacing (see startDisplayLink/waitForVBlanks). + void* mDisplayLink = nullptr; // CVDisplayLinkRef + std::mutex mVBlankMutex; + std::condition_variable mVBlankCond; + U64 mVBlankCount = 0; // ticked on the CVDisplayLink thread + U64 mLastSwapVBlank = 0; // OS-main only + std::atomic mSwapInterval{1}; // present every Nth vblank; 0 = unthrottled +public: friend class LLWindowManager; }; diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d3869dc72cd..48916f5b904 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -7336,17 +7336,6 @@ Value 0 - RenderAppleUseMultGL - - Comment - Whether we want to use multi-threaded OpenGL on Apple hardware (requires restart of SL). - Persist - 1 - Type - Boolean - Value - 0 - RenderAttachedLights Comment diff --git a/indra/newview/featuretable_mac.txt b/indra/newview/featuretable_mac.txt index c3e2dd0c41c..cf7afaf5eb9 100644 --- a/indra/newview/featuretable_mac.txt +++ b/indra/newview/featuretable_mac.txt @@ -69,7 +69,6 @@ RenderMaxTextureIndex 1 16 RenderGLContextCoreProfile 1 1 RenderGLMultiThreadedTextures 1 1 RenderGLMultiThreadedMedia 1 1 -RenderAppleUseMultGL 1 1 RenderReflectionsEnabled 1 1 RenderReflectionProbeDetail 1 2 RenderScreenSpaceReflections 1 1 @@ -431,19 +430,16 @@ RenderGLMultiThreadedMedia 1 0 list NVIDIA RenderGLMultiThreadedTextures 1 0 RenderGLMultiThreadedMedia 1 0 -RenderAppleUseMultGL 1 0 list Intel RenderAnisotropic 1 0 RenderFSAASamples 1 0 RenderGLMultiThreadedTextures 1 0 RenderGLMultiThreadedMedia 1 0 -RenderAppleUseMultGL 1 0 // AppleGPU and NonAppleGPU can be thought of as Apple silicon vs Intel Mac list AppleGPU RenderGLMultiThreadedMedia 1 0 -RenderAppleUseMultGL 1 0 RenderGLMultiThreadedTextures 1 0 RenderGLMultiThreadedMedia 1 0 diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 02d345259ac..b584a67f662 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -88,6 +88,7 @@ #include "llurldispatcher.h" #include "llurlhistory.h" #include "llrender.h" +#include "llimagegl.h" #include "llteleporthistory.h" #include "lltoast.h" #include "llsdutil_math.h" @@ -589,7 +590,6 @@ static void settings_to_globals() LLWorldMapView::setScaleSetting(gSavedSettings.getF32("MapScale")); #if LL_DARWIN - LLWindowMacOSX::sUseMultGL = gSavedSettings.getBOOL("RenderAppleUseMultGL"); gHiDPISupport = gSavedSettings.getBOOL("RenderHiDPI"); #endif } @@ -820,6 +820,14 @@ void LLAppViewer::markFrameRendered() U32 spins = 0; while (mLatestIdx.load(std::memory_order_acquire) != -1) { + // Bail if we're shutting down: the compositor has stopped + // presenting, so it will never claim this publish. Without + // this the viewer thread spins forever and the shutdown join + // hangs (then pthread_cancel fires on a GL-holding thread). + if (LLApp::isExiting() || mCompositor.isShutdownRequested()) + { + return; + } if (++spins < 64) { std::this_thread::yield(); @@ -831,10 +839,18 @@ void LLAppViewer::markFrameRendered() } } - // Publish into the mailbox. Fence and flush first so the compositor - // can wait on the completed frame GPU-side. - mRTs[mBackIdx].placeFrameCompleteFence(); - glFlush(); + // Publish into the mailbox. Fence + flush first so the compositor can wait + // on the completed frame GPU-side; the fence rides the cross-context color + // attachment and placeFrameCompleteFence already flushes - only flush here + // when there's no fence to carry it. + if (LLImageGL* sync = mRTs[mBackIdx].getColorAttachmentImage()) + { + sync->placeFrameCompleteFence(); + } + else + { + glFlush(); + } S32 old_latest = mLatestIdx.exchange((S32)mBackIdx, std::memory_order_acq_rel); if (old_latest != -1) @@ -853,6 +869,13 @@ void LLAppViewer::markFrameRendered() U32 spins = 0; while (free_idx == -1) { + // See the bail above: don't spin into a hung shutdown. We + // already published this frame; just keep the same back + // buffer (there's no next frame to render into anyway). + if (LLApp::isExiting() || mCompositor.isShutdownRequested()) + { + return; + } if (++spins < 64) { std::this_thread::yield(); @@ -885,20 +908,16 @@ bool LLAppViewer::tryAcquireNewFront() void LLAppViewer::viewerThreadTick() { - // Vsync pacing (RenderVSyncMode): 0 = off, the world renders as - // fast as it can. Otherwise run one tick per compositor sync - the - // driver already divides the present cadence via the swap interval, - // so waiting for the next sync IS refresh/N pacing. Stale ticks get - // drained first so a slow frame doesn't burst afterward. The pop - // times out so a stalled compositor can't wedge us, and the - // single-thread fallback skips the gate - the sync fires after our - // inline tick, so waiting on it would deadlock. + // Vsync pacing: the compositor presents every vblank, so run the world + // once per RenderVSyncMode presents (refresh/N). Mode 0 runs free. + // Single-thread fallback skips the gate (the present happens inline). static LLCachedControl vsync_mode(gSavedSettings, "RenderVSyncMode", 1); if ((U32)vsync_mode > 0 && mViewerThread) { - U64 tick = 0; - while (mSyncTicks.tryPop(tick)) {} - mSyncTicks.tryPopFor(std::chrono::milliseconds(100), tick); + // Block until the compositor presents N more frames. Pacing to + // index = last + N drops backlog implicitly (mLastPresent jumps to the + // current index) so a hitch doesn't burst; interruptSync() releases it. + mLastPresent = mCompositor.waitForPresent(mLastPresent + (U64)vsync_mode); } // Everything the old main loop did runs here, minus the compositor @@ -1222,7 +1241,10 @@ void LLAppViewer::renderViewerFrame() // Wait GPU-side until the compositor is done reading the buffer we // just picked up as our new back. - getBackBuffer().waitReadCompleteFence(); + if (LLImageGL* sync = getBackBuffer().getColorAttachmentImage()) + { + sync->waitReadCompleteFence(); + } display(); @@ -1804,9 +1826,8 @@ bool LLAppViewer::init() // Init is done - spawn the viewer thread. From here on the viewer // runs on its own thread and OS main just composites and pumps - // events. Windows only for now; other platforms stay single - // threaded. -#if LL_WINDOWS + // events. Windows and macOS; other platforms stay single threaded. +#if LL_WINDOWS || LL_DARWIN if (gViewerWindow && gViewerWindow->getWindow()) { mViewerThread = std::make_unique(gViewerWindow->getWindow()); @@ -1913,6 +1934,13 @@ bool LLAppViewer::doFrame() gOSMainWork.runFor(kOSMainWorkTime); } + // Run AppKit-only work the viewer thread deferred (cursor updates + // on macOS). No-op where the platform has its own window thread. + if (gViewerWindow && gViewerWindow->getWindow()) + { + gViewerWindow->getWindow()->drainOSMainQueue(); + } + if (!gHeadlessClient && gViewerWindow) { LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Display"); @@ -1959,11 +1987,9 @@ void LLAppViewer::setVSyncMode(U32 mode) { mode = llclamp(mode, 0u, 4u); - // The driver does the pacing: swap interval N presents every Nth - // vblank, and the sync signal (and the world with it) inherits that - // cadence. Mode 0 still presents every vblank - only the world runs - // free. The compositor applies it on its own context. - mCompositor.setSwapInterval(mode >= 1 ? (S32)mode : 1); + // Compositor always presents at vsync; only the world's rate follows + // the mode (the sync gate runs it once per Nth present). + mCompositor.setSwapInterval(1); LLPerfStats::tunables.vsyncEnabled = (mode > 0); if (mode > 0) @@ -2037,7 +2063,11 @@ bool LLAppViewer::cleanup() // own thread on the way out, since WGL requires that. if (mViewerThread) { + // Unblock both places the viewer thread can park on the compositor: + // the mailbox spins (requestShutdown) and the vsync gate (interruptSync). + mCompositor.requestShutdown(); mViewerThread->requestStop(); + mCompositor.interruptSync(); mViewerThread->shutdown(); mViewerThread.reset(); @@ -3725,10 +3755,6 @@ bool LLAppViewer::initWindow() // the compositor can use this program from the OS main thread. mCompositor.setBlitShader(&gCompositorBlitProgram); - // Sync ticks feed the vsync pacing gate in viewerThreadTick. - mSyncConnection = mCompositor.onSync( - [this](U64 frame_index) { mSyncTicks.tryPush(frame_index); }); - // Seed the vsync mode; the compositor applies the interval on // its own context at the first present. setVSyncMode(gSavedSettings.getU32("RenderVSyncMode")); diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 4792936b758..a6194dad2c3 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -109,9 +109,9 @@ class LLAppViewer : public LLApp, public LLCompositable // // Main application logic // - virtual bool init(); // Override to do application initialization - virtual bool cleanup(); // Override to do application cleanup - virtual bool frame(); // Override for application body logic + virtual bool init() override; // Override to do application initialization + virtual bool cleanup() override; // Override to do application cleanup + virtual bool frame() override; // Override for application body logic // Application control void flushLFSIO(); // waits for lfs transfers to complete @@ -172,7 +172,6 @@ class LLAppViewer : public LLApp, public LLCompositable // applies it on its own context), and everyone else adapts to the // resulting sync cadence. void setVSyncMode(U32 mode); - LLRenderTarget& getScratchParent() { return mScratchParent; } // Triple-buffer mailbox between the viewer thread and the compositor. // Each buffer lives in exactly one slot at a time: @@ -198,12 +197,10 @@ class LLAppViewer : public LLApp, public LLCompositable void markFrameRendered(); // Render paths call this once they've actually drawn into the back - // buffer; renderViewerFrame only publishes when set. The tag tells us - // which path drew, for diagnostics. Viewer thread only. - void setBackBufferRendered(const char* who) + // buffer; renderViewerFrame only publishes when set. Viewer thread only. + void setBackBufferRendered() { mBackBufferRendered = true; - mBackBufferRenderedBy = who; } // Runs display() plus the post-display updaters (reflection probes, @@ -386,7 +383,7 @@ class LLAppViewer : public LLApp, public LLCompositable virtual bool meetsRequirementsForMaximizedStart(); // Used on first login to decide to launch maximized - virtual void sendOutOfDiskSpaceNotification(); + virtual void sendOutOfDiskSpaceNotification() override; protected: @@ -465,17 +462,14 @@ class LLAppViewer : public LLApp, public LLCompositable LLRenderTarget mRTs[3]; U32 mBackIdx{0}; // producer-owned (viewer thread) bool mBackBufferRendered{false}; // viewer-thread-only render gate - const char* mBackBufferRenderedBy{"none"}; // which path set the gate std::atomic mLatestIdx{-1}; // mailbox slot; -1 = empty std::atomic mFreeIdx{1}; // recycle slot; -1 = empty S32 mDisplayIdx{2}; // compositor-owned LLRenderTarget mScratchParent; // wrap parent for cubeSnapshot raw clear - // Compositor sync ticks for vsync pacing (RenderVSyncMode). The - // sync handler pushes from OS main, the viewer thread pops; tryPush - // drops when we're behind, so neither side ever blocks the other. - LLThreadSafeQueue mSyncTicks{16}; - boost::signals2::connection mSyncConnection; + // Last compositor present index the viewer thread paced to. The vsync + // gate in viewerThreadTick waits for it to advance by RenderVSyncMode. + U64 mLastPresent = 0; // The viewer thread. Runs viewerThreadTick in a loop with its own // shared GL context. Spawned at the end of init(), joined in cleanup() diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index 10cf733e783..48c4eca3237 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -316,7 +316,7 @@ void LLSceneMonitor::capture() // Read from the most recently presented swap chain image - the // frame the user is actually seeing. The compositor owns the // swap chain now. - LLRenderTarget& src = const_cast(LLAppViewer::instance()->getCompositor().getSwapChain()).getPreviousImage(); + LLSwapChain& src = const_cast(LLAppViewer::instance()->getCompositor().getSwapChain()); src.bindForRead(); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, cur_target.getWidth(), cur_target.getHeight()); //copy the content diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index d7420addd5d..a75fa56c317 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -478,16 +478,6 @@ static bool handleReflectionProbeCountChanged(const LLSD& newvalue) return true; } -#if LL_DARWIN -static bool handleAppleUseMultGLChanged(const LLSD& newvalue) -{ - if (gGLManager.mInited) - { - LLWindowMacOSX::setUseMultGL(newvalue.asBoolean()); - } - return true; -} -#endif static bool handleHeroProbeResolutionChanged(const LLSD &newvalue) { @@ -859,9 +849,6 @@ void settings_setup_listeners() setting_setup_signal_listener(gSavedSettings, "RenderReflectionProbeDetail", handleReflectionProbeDetailChanged); setting_setup_signal_listener(gSavedSettings, "RenderReflectionProbeCount", handleReflectionProbeCountChanged); setting_setup_signal_listener(gSavedSettings, "RenderReflectionsEnabled", handleReflectionProbeDetailChanged); -#if LL_DARWIN - setting_setup_signal_listener(gSavedSettings, "RenderAppleUseMultGL", handleAppleUseMultGLChanged); -#endif setting_setup_signal_listener(gSavedSettings, "RenderScreenSpaceReflections", handleReflectionProbeDetailChanged); setting_setup_signal_listener(gSavedSettings, "RenderMirrors", handleReflectionProbeDetailChanged); setting_setup_signal_listener(gSavedSettings, "RenderHeroProbeResolution", handleHeroProbeResolutionChanged); diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index d1a18490004..61707bfd49a 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -214,7 +214,7 @@ void display_startup() } // Just set the flag; the actual publish happens at the end of // renderViewerFrame. - LLAppViewer::instance()->setBackBufferRendered("startup"); + LLAppViewer::instance()->setBackBufferRendered(); glClear(GL_DEPTH_BUFFER_BIT); } @@ -1617,7 +1617,7 @@ void swap() } // Just set the flag here - we only publish once per frame, at the // end of renderViewerFrame. Publishing twice gets us stale frames. - LLAppViewer::instance()->setBackBufferRendered("world"); + LLAppViewer::instance()->setBackBufferRendered(); } void renderCoordinateAxes() diff --git a/indra/newview/llviewerthread.cpp b/indra/newview/llviewerthread.cpp index 153d999c76d..392fdf9a0b9 100644 --- a/indra/newview/llviewerthread.cpp +++ b/indra/newview/llviewerthread.cpp @@ -33,6 +33,10 @@ #include "llviewerwindow.h" #include "llwindow.h" +#if LL_DARWIN +#include "llwindowmacosx-objc.h" +#endif + LLViewerThread::LLViewerThread(LLWindow* window) : LLThread("Viewer"), mWindow(window), @@ -87,7 +91,16 @@ void LLViewerThread::run() // unblocks us and we fall out of the loop. while (!isQuitting()) { +#if LL_DARWIN + // This thread has no Cocoa run loop; drain an autorelease pool + // per tick so transient ObjC objects don't leak until exit. + ll_macos_run_in_autorelease_pool([]() + { + LLAppViewer::instance()->viewerThreadTick(); + }); +#else LLAppViewer::instance()->viewerThreadTick(); +#endif } LL_INFOS("Viewer") << "Viewer thread exiting, destroying context." @@ -96,7 +109,14 @@ void LLViewerThread::run() // The old main loop's exit tail (final snapshot, voice, service // pump, watchdog), run while the world and our GL context are still // intact - cleanup is blocked joining us. +#if LL_DARWIN + ll_macos_run_in_autorelease_pool([]() + { + LLAppViewer::instance()->viewerThreadShutdown(); + }); +#else LLAppViewer::instance()->viewerThreadShutdown(); +#endif // Tear down per-thread gGL before destroying the context. gGL.shutdown(); diff --git a/indra/newview/llviewerthread.h b/indra/newview/llviewerthread.h index 9c8bb20b04d..ec5befa218a 100644 --- a/indra/newview/llviewerthread.h +++ b/indra/newview/llviewerthread.h @@ -37,17 +37,19 @@ class LLWindow; // OS main thread keeps the message pump and the compositor. // // Construct and start() this on the OS main thread once the window's -// context is up. On shutdown, unblock the compositor first, then -// requestStop() and join via shutdown(). +// context is up. On shutdown, call LLCompositor::requestShutdown() first +// to unblock any producer waiting on a present, then requestStop() and +// join via shutdown(). class LLViewerThread : public LLThread { public: LLViewerThread(LLWindow* window); ~LLViewerThread() override; - // Flag the thread for shutdown. If we're blocked waiting on a - // present, call LLCompositor::requestShutdown() first to unstick - // us, then join via shutdown(). + // Flag the thread for shutdown. The loop checks isQuitting() between + // ticks. If the tick may be parked waiting on a present, call + // LLCompositor::requestShutdown() first to unstick it, then join via + // shutdown(). void requestStop() { setQuitting(); } protected: From 3f87f5a18c2aa3e023468e2b0383e41a39f81735 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 26 Jun 2026 12:12:32 -0400 Subject: [PATCH 5/9] Take care of some snapshot asserts. --- indra/newview/llappviewer.cpp | 28 ++++++++++-------- indra/newview/llviewerdisplay.cpp | 48 +++++++++++++++++++------------ indra/newview/llviewerwindow.cpp | 15 ++++++++-- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index b584a67f662..2816b6b470a 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -1248,31 +1248,39 @@ void LLAppViewer::renderViewerFrame() display(); + // The world render is done. The post-display updaters below are + // standalone renders (reflection probes, snapshot floaters), not the + // world pass - a parentless flush in them is legitimate, so drop the + // requirement now rather than at the end of the frame. + LLRenderTarget::sFlushRequiresParent = false; + LLPerfStats::RecordSceneTime T(LLPerfStats::StatType_t::RENDER_IDLE); LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Snapshot"); pingMainloopTimeout("Main:Snapshot"); - // The updaters need a parent on the RT stack to pop back to, but it - // can't be the back buffer - cubeSnapshot clears whatever's bound, - // which would wipe the frame we just rendered. Give them the scratch - // parent instead. + // Reflection probes' cubeSnapshot does a raw clear on whatever RT is + // bound, which would wipe the frame we just rendered - give it the + // scratch parent to land on, then flush back to a clean stack. bool scratch_bound = false; if (LLRenderTarget::getCurrentBoundTarget() == nullptr) { mScratchParent.bindTarget(); scratch_bound = true; } - gPipeline.mReflectionMapManager.update(); - LLFloaterSnapshot::update(); - LLFloaterSimpleSnapshot::update(); - if (scratch_bound && LLRenderTarget::getCurrentBoundTarget() == &mScratchParent) { mScratchParent.flush(); } + // The snapshot floaters run their own full render (rawSnapshot, with + // its own render target). Let them run from a clean RT stack, exactly + // as they do when driven from idle() - binding a parent under them + // leaves the stack dirty for next frame's display(). + LLFloaterSnapshot::update(); + LLFloaterSimpleSnapshot::update(); + llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); // The one publish for this tick. Skip it if nothing drew into the @@ -1283,10 +1291,6 @@ void LLAppViewer::renderViewerFrame() markFrameRendered(); } gDisplaySwapBuffers = true; - - // Back out of the world-render scope; standalone passes that run - // before the next renderViewerFrame may flush parentless. - LLRenderTarget::sFlushRequiresParent = false; } class LLUITranslationBridge : public LLTranslationBridge diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 61707bfd49a..9ccbe78062f 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -728,16 +728,18 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) // Bind our back buffer as the bottom of the render target stack for // this frame. Everything downstream renders on top of it and pops - // back to it on flush. Snapshots render to their own off-screen - // target, so we skip the bind in that case. Nothing should still be - // bound from a previous frame at this point - hence the assert. - if (!for_snapshot) + // back to it on flush. Deferred snapshots reuse this same path (the + // sky needs it - see the for_snapshot reset above), but rawSnapshot + // binds its own oversized scratch target first; if something is + // already bound, leave it. A still-bound target on a true world frame + // means an upstream flush was missed - hence the assert. + if (LLRenderTarget::getCurrentBoundTarget() == nullptr) { - llassert(LLRenderTarget::getCurrentBoundTarget() == nullptr); - if (LLRenderTarget::getCurrentBoundTarget() == nullptr) - { - LLAppViewer::instance()->getBackBuffer().bindTarget(); - } + LLAppViewer::instance()->getBackBuffer().bindTarget(); + } + else + { + llassert(gSnapshot); } // do render-to-texture stuff here @@ -1602,22 +1604,30 @@ void swap() LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_SWAP ); LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Swap"); - // Flush the back buffer off the RT stack and mark a fresh frame - // ready. The actual present happens later in doFrame. + // Flush the bottom-of-stack target off the RT stack. For a world frame + // that's our back buffer, and we mark a fresh frame ready (the actual + // present happens later in doFrame). An oversized snapshot is instead + // sitting on its own scratch target here - leave it bound so rawSnapshot + // can read it back, and never publish it to the screen. LLRenderTarget& world_rt = LLAppViewer::instance()->getBackBuffer(); - // By the time we get here, display() should have left our back - // buffer at the bottom of the stack. If not, something upstream - // didn't pop its render targets. - llassert(LLRenderTarget::getCurrentBoundTarget() == &world_rt); - if (LLRenderTarget::getCurrentBoundTarget() == &world_rt) { world_rt.flush(); + // Only publish once per frame, at the end of renderViewerFrame, and + // only for real world frames - snapshots reading the back buffer + // must not publish themselves. + if (!gSnapshot) + { + LLAppViewer::instance()->setBackBufferRendered(); + } + } + else + { + // A snapshot's scratch target is the bottom of the stack here. + // Outside a snapshot this means an upstream flush was missed. + llassert(gSnapshot); } - // Just set the flag here - we only publish once per frame, at the - // end of renderViewerFrame. Publishing twice gets us stale frames. - LLAppViewer::instance()->setBackBufferRendered(); } void renderCoordinateAxes() diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 042efc75007..61f521c678e 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -5362,9 +5362,18 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei swap(); } - // The rendered frame lives in our back buffer, not - // FBO 0, so read from there. - LLAppViewer::instance()->getBackBuffer().bindForRead(); + // Read back the frame we just rendered. Oversized captures + // render into their own scratch target; everything else + // lands in the back buffer (FBO 0 is not a valid target on + // the render thread's offscreen context). + if (reset_deferred) + { + scratch_space.bindForRead(); + } + else + { + LLAppViewer::instance()->getBackBuffer().bindForRead(); + } for (U32 out_y = 0; out_y < read_height ; out_y++) { From e748893428f50a834bf83d1bae87ee7c13682a68 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Sun, 28 Jun 2026 20:26:04 -0400 Subject: [PATCH 6/9] Some defensive changes for a race condition involving getting a render target when it's not quite ready. --- indra/llrender/llcompositable.h | 14 ++++++++++++++ indra/llrender/llcompositor.cpp | 20 +++++++++++++++++++- indra/llrender/lltestsquarecompositable.cpp | 20 +++++++++++++++++++- indra/llrender/lltestsquarecompositable.h | 7 +++++++ indra/newview/llappviewer.cpp | 3 +++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/indra/llrender/llcompositable.h b/indra/llrender/llcompositable.h index 036842044c4..64a2e84904e 100644 --- a/indra/llrender/llcompositable.h +++ b/indra/llrender/llcompositable.h @@ -50,6 +50,14 @@ class LLCompositable // lease - the compositor handles that. virtual LLRenderTarget& frontBuffer() = 0; + // True once the producer's front buffer is allocated and safe to sample, + // false before the producer tears it down. The compositor MUST skip a + // layer whose isReady() is false: it may still be mid-allocation on its + // producer thread, or already releasing its RT. Reading frontBuffer() in + // either window touches an LLRenderTarget whose mTex vector is being + // mutated on another thread. + bool isReady() const { return mReady.load(std::memory_order_acquire); } + // Destination offset (pixels, GL origin = bottom-left) where this // layer's front buffer lands in the swap chain image. Fullscreen // layers keep the 0,0 default. @@ -68,6 +76,11 @@ class LLCompositable F32 lastFrameMs() const { return mFrameMs.load(std::memory_order_relaxed); } protected: + // Producers flip this true once their front buffer is allocated and + // first-painted, and false before they release it. Paired with the + // acquire load in isReady() so the compositor sees a fully-built RT. + void setReady(bool ready) { mReady.store(ready, std::memory_order_release); } + // Producers call this when a frame is complete - stamps the cycle // metrics. void produceFrame() @@ -82,6 +95,7 @@ class LLCompositable } private: + std::atomic mReady{false}; std::atomic mFrameMs{0.f}; F64 mLastProduceTime = 0.0; }; diff --git a/indra/llrender/llcompositor.cpp b/indra/llrender/llcompositor.cpp index 257a7daab9d..5cbd7b64949 100644 --- a/indra/llrender/llcompositor.cpp +++ b/indra/llrender/llcompositor.cpp @@ -203,12 +203,30 @@ void LLCompositor::presentFrame() mBlitShader->bind(); } - for (LLCompositable* c : mCompositables) + // Snapshot the layer list under its lock so a concurrent add/remove can't + // invalidate our iterator mid-present. We drop the lock before touching + // any layer - the readiness gate below covers per-layer lifetime. + std::vector layers; + { + std::lock_guard lock(mCompositablesMutex); + layers = mCompositables; + } + + for (LLCompositable* c : layers) { if (!shader_ready) { break; } + + // The producer must have finished allocating its front buffer and not + // be tearing it down: otherwise frontBuffer()'s RT is mid-mutation on + // the producer thread and its mTex is unsafe to read. + if (!c->isReady()) + { + continue; + } + // Mailbox claim for queue-paced layers. c->tryAcquireNewFront(); diff --git a/indra/llrender/lltestsquarecompositable.cpp b/indra/llrender/lltestsquarecompositable.cpp index 4afdf04de4e..59a928a4838 100644 --- a/indra/llrender/lltestsquarecompositable.cpp +++ b/indra/llrender/lltestsquarecompositable.cpp @@ -70,7 +70,13 @@ void LLTestSquareCompositable::disconnect() { setQuitting(); if (mCompositor) mCompositor->wakeSyncWaiters(); // break our waitForPresent - shutdown(); // joins + shutdown(); // waits for run() to return (polls a non-atomic flag) + + // shutdown()'s mStatus poll has no memory barrier (llthread.cpp:243), + // and run() stores mRunExited (release) just before mStatus goes + // STOPPED. Acquire-load it once so run()'s final writes - the RT + // release - are guaranteed visible before the object can be freed. + (void)mRunExited.load(std::memory_order_acquire); } } @@ -97,6 +103,9 @@ void LLTestSquareCompositable::run() } produceFrame(); + // RT is allocated and first-painted: the compositor may sample us now. + setReady(true); + while (!isQuitting()) { // Pace to the compositor's present clock; the should_stop predicate + @@ -134,12 +143,21 @@ void LLTestSquareCompositable::run() } } + // Stop the compositor from sampling us before we drop the RT. (Teardown + // runs on the compositor's thread, which is blocked in disconnect() here, + // so it can't be mid-present - but the flag keeps that guarantee local.) + setReady(false); + // Tear down on this thread - the context has to be destroyed where // it's current. mRT.release(); gGL.shutdown(); mWindow->destroySharedContext(mContext); mContext = nullptr; + + // Last thing we touch: publishes all of run()'s memory effects (the RT + // release above) to whoever joins us in disconnect(). + mRunExited.store(true, std::memory_order_release); } void LLTestSquareCompositable::paint() diff --git a/indra/llrender/lltestsquarecompositable.h b/indra/llrender/lltestsquarecompositable.h index 07b003bb2e8..75a7eb76a12 100644 --- a/indra/llrender/lltestsquarecompositable.h +++ b/indra/llrender/lltestsquarecompositable.h @@ -90,6 +90,13 @@ class LLTestSquareCompositable : public LLCompositable, public LLThread LLCompositor* mCompositor = nullptr; // present clock (waitForPresent) U64 mLastTick = 0; // last present index we paced to + + // Set true (release) as the very last thing run() does, after the RT is + // released and the context destroyed. disconnect() waits on it (acquire) + // so the object is never freed while run() is still touching our members. + // LLThread::shutdown() polls a non-atomic mStatus with no barrier (see the + // note at llthread.cpp:243); this is the missing happens-before. + std::atomic mRunExited{false}; }; #endif // LL_LLTESTSQUARECOMPOSITABLE_H diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 2816b6b470a..d770dd97310 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -782,6 +782,9 @@ void LLAppViewer::allocateRenderTargets(U32 width, U32 height) // cubeSnapshot's raw glClear to land each frame. mScratchParent.allocate(width, height, GL_RGBA, /*depth=*/true); mScratchParent.markAsSwapChainImage(true); + + // Front buffer exists now: the compositor may sample the world layer. + setReady(true); } void LLAppViewer::resizeRenderTargets(U32 width, U32 height) From be24f9d13525e9416b6078e1be28eb17b77bd9de Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Sun, 28 Jun 2026 21:27:19 -0400 Subject: [PATCH 7/9] Add some diagnostics for some cross threaded RT allocation. --- indra/llrender/lltestsquarecompositable.cpp | 16 +++++++++++++++- indra/llwindow/llwindowwin32.cpp | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/indra/llrender/lltestsquarecompositable.cpp b/indra/llrender/lltestsquarecompositable.cpp index 59a928a4838..2ae050a9632 100644 --- a/indra/llrender/lltestsquarecompositable.cpp +++ b/indra/llrender/lltestsquarecompositable.cpp @@ -88,7 +88,21 @@ void LLTestSquareCompositable::run() mWindow->makeContextCurrent(mContext); gGL.init(false); - mRT.allocate(mSize, mSize, GL_RGBA, /*depth=*/false); + // An optional dev-only layer must never take the viewer down. If the RT + // can't be created on this producer context (a known issue: glGenTextures + // can come back empty on a freshly-current shared context early in + // startup), bail cleanly - we never setReady(), so the compositor skips + // us, and the rest of the viewer runs. + if (!mRT.allocate(mSize, mSize, GL_RGBA, /*depth=*/false)) + { + LL_WARNS() << "TestSquare '" << mName + << "': RT allocate failed on producer context; layer disabled." << LL_ENDL; + gGL.shutdown(); + mWindow->destroySharedContext(mContext); + mContext = nullptr; + mRunExited.store(true, std::memory_order_release); + return; + } mRT.setNeedsCrossContextSync(true); // First frame so the layer shows up before the first tick. diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 7840f32d164..d95829fbf38 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -1986,7 +1986,21 @@ void* LLWindowWin32::createSharedContext() void LLWindowWin32::makeContextCurrent(void* contextPtr) { - wglMakeCurrent(mhDC, (HGLRC) contextPtr); + // TEMP ctxdiag: confirm whether wglMakeCurrent actually succeeds per + // thread. All contexts share the single mhDC; an HDC can only be current + // on one thread at a time, so concurrent producers may be failing here + // silently (no current context -> glGenTextures no-ops). + BOOL ok = wglMakeCurrent(mhDC, (HGLRC) contextPtr); + DWORD err = ok ? 0 : GetLastError(); + HGLRC after = wglGetCurrentContext(); + HDC afterdc = wglGetCurrentDC(); + LL_INFOS("ctxdiag") << "makeContextCurrent thread=" << LLThread::currentID() + << " req_rc=" << contextPtr + << " mhDC=" << (void*)mhDC + << " ok=" << (S32)ok + << " GetLastError=" << (U32)err + << " cur_rc=" << (void*)after + << " cur_dc=" << (void*)afterdc << LL_ENDL; LL_PROFILER_GPU_CONTEXT; } From e11961513954b982717b87ab2c834dd070c95c88 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 14 Jul 2026 00:36:57 -0400 Subject: [PATCH 8/9] Track down and kill a couple more races. --- indra/llrender/llcompositor.cpp | 217 ++++++++++++++++------------ indra/llrender/llcompositor.h | 17 ++- indra/llrender/llrendertarget.cpp | 30 +++- indra/llwindow/llwindowwin32.cpp | 42 ++++-- indra/newview/llappviewer.cpp | 6 +- indra/newview/llscenemonitor.cpp | 16 +- indra/newview/llviewershadermgr.cpp | 21 +++ indra/newview/llviewerthread.cpp | 9 +- 8 files changed, 235 insertions(+), 123 deletions(-) diff --git a/indra/llrender/llcompositor.cpp b/indra/llrender/llcompositor.cpp index 5cbd7b64949..ffeff20066d 100644 --- a/indra/llrender/llcompositor.cpp +++ b/indra/llrender/llcompositor.cpp @@ -33,6 +33,7 @@ #include "llrendertarget.h" #include "llimagegl.h" #include "llgl.h" +#include "llglstates.h" #include "llglslshader.h" #include "llwindow.h" @@ -178,11 +179,6 @@ void LLCompositor::presentFrame() // Draw every layer's front texture as a quad straight into the default // framebuffer, bottom first. Binding the texture for sampling is also // what pulls the producer context's writes into this one. - // - // The blit shader lives in LLViewerShaderMgr (compositorblitV/F.glsl). - // Until it's handed to us and compiled, just present - nothing to - // draw with yet. - const bool shader_ready = mBlitShader && mBlitShader->mProgramObject; const GLint dst_w = (GLint)mSwapChain.getWidth(); const GLint dst_h = (GLint)mSwapChain.getHeight(); @@ -193,109 +189,144 @@ void LLCompositor::presentFrame() // window) and composite the layers straight into it. mSwapChain.acquireNextImage(); mSwapChain.bindForRender(); - glDisable(GL_BLEND); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - if (shader_ready) - { - mBlitShader->bind(); - } - - // Snapshot the layer list under its lock so a concurrent add/remove can't - // invalidate our iterator mid-present. We drop the lock before touching - // any layer - the readiness gate below covers per-layer lifetime. - std::vector layers; - { - std::lock_guard lock(mCompositablesMutex); - layers = mCompositables; - } - for (LLCompositable* c : layers) { - if (!shader_ready) - { - break; - } - - // The producer must have finished allocating its front buffer and not - // be tearing it down: otherwise frontBuffer()'s RT is mid-mutation on - // the producer thread and its mTex is unsafe to read. - if (!c->isReady()) - { - continue; - } - - // Mailbox claim for queue-paced layers. - c->tryAcquireNewFront(); - - LLRenderTarget& front = c->frontBuffer(); - - // Don't hold an outer lease while calling accessors that take - // their own - leases don't nest. Each call below takes its own; - // only the texture guard's lease spans the draw. - if (!front.isComplete()) + // Hold the shader handoff lock across the composite. A shader reload + // on the viewer thread clears mBlitShader, re-links the program, and + // sets it again - the lock makes that wait out an in-flight composite + // instead of deleting the program we're drawing with. + std::lock_guard shader_lock(mBlitShaderMutex); + + // Not drawable: no compiled shader yet (startup, or mid-reload), or a + // degenerate swap chain (the NDC math below divides by these). We + // still run the loop either way - the mailbox drain must not stop. + const bool can_draw = mBlitShader && mBlitShader->mProgramObject + && dst_w > 0 && dst_h > 0; + + // Composite state, scoped and cache-aware so this thread's GL state + // mirrors stay truthful - on single-thread fallbacks the world render + // shares this context and trusts those caches. + LLGLDisable blend(GL_BLEND); + LLGLDisable scissor(GL_SCISSOR_TEST); + LLGLDepthTest depth(GL_FALSE, GL_FALSE); + gGL.setColorMask(true, true); + + if (can_draw) { - continue; // layer not allocated yet (early init) + mBlitShader->bind(); } - S32 dst_x = 0, dst_y = 0; - c->compositeOffset(dst_x, dst_y); - const GLint w = (GLint)front.getWidth(); - const GLint h = (GLint)front.getHeight(); - - // The cross-context primitive is the color texture (LLImageGL), not the - // FBO-centric RT. Sample it directly: the guard holds its shared lease - // across the draw and its fence orders the GPU. Single-context layers - // (no attachment image) fall back to the raw RT name (no lease needed). - LLImageGL* sync = front.getColorAttachmentImage(); - LLScopedTexName src_tex_guard; - U32 src_tex; - if (sync) + // Snapshot the layer list under its lock so a concurrent add/remove + // can't invalidate our iterator mid-present. We drop the lock before + // touching any layer - the readiness gate below covers per-layer + // lifetime. + std::vector layers; { - src_tex_guard = sync->getTexName(); - src_tex = src_tex_guard.get(); + std::lock_guard lock(mCompositablesMutex); + layers = mCompositables; } - else - { - src_tex = front.getTexture(0); - } - llassert(src_tex != 0); - // Wait on the producer's fence so we only sample finished pixels. - if (sync) + for (LLCompositable* c : layers) { - sync->waitFrameCompleteFence(); + // The producer must have finished allocating its front buffer and + // not be tearing it down: otherwise frontBuffer()'s RT is + // mid-mutation on the producer thread and its mTex is unsafe to + // read. + if (!c->isReady()) + { + continue; + } + + // Mailbox claim for queue-paced layers. This runs even on + // presents that can't draw - the claim is what unblocks the + // producer's publish wait, and skipping it would starve the + // world thread behind a shader reload or a degenerate window. + c->tryAcquireNewFront(); + + if (!can_draw) + { + continue; + } + + LLRenderTarget& front = c->frontBuffer(); + + // Don't hold an outer lease while calling accessors that take + // their own - leases don't nest. Each call below takes its own; + // only the texture guard's lease spans the draw. + if (!front.isComplete()) + { + continue; // layer not allocated yet (early init) + } + + S32 dst_x = 0, dst_y = 0; + c->compositeOffset(dst_x, dst_y); + + // The cross-context primitive is the color texture (LLImageGL), not + // the FBO-centric RT. Sample it directly: the guard holds its shared + // lease across the draw and its fence orders the GPU. Single-context + // layers (no attachment image) fall back to the raw RT name (no + // lease needed). + LLImageGL* sync = front.getColorAttachmentImage(); + LLScopedTexName src_tex_guard; + U32 src_tex; + if (sync) + { + src_tex_guard = sync->getTexName(); + src_tex = src_tex_guard.get(); + } + else + { + src_tex = front.getTexture(0); + } + + if (src_tex == 0) + { + // A layer that reached ready with no usable texture name. + // Never bind name 0 and draw whatever's resident there. + llassert(false); + continue; + } + + // Read the layer size under the texture guard's shared lease - + // resize() publishes the new size under the matching unique lease, + // so size and storage stay paired. + const GLint w = (GLint)front.getWidth(); + const GLint h = (GLint)front.getHeight(); + + // Wait on the producer's fence so we only sample finished pixels. + if (sync) + { + sync->waitFrameCompleteFence(); + } + + // Layer rect in NDC; GL origin bottom-left matches the + // compositeOffset convention. + const F32 x0 = 2.f * (F32)dst_x / (F32)dst_w - 1.f; + const F32 y0 = 2.f * (F32)dst_y / (F32)dst_h - 1.f; + const F32 x1 = 2.f * (F32)(dst_x + w) / (F32)dst_w - 1.f; + const F32 y1 = 2.f * (F32)(dst_y + h) / (F32)dst_h - 1.f; + + // Bind and draw the layer quad. + static const LLStaticHashedString sBlitRect("blit_rect"); + gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, src_tex); + mBlitShader->uniform4f(sBlitRect, x0, y0, x1, y1); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); + + // Reverse fence: the producer waits on this before writing into + // the buffer again. + if (sync) + { + sync->placeReadCompleteFence(); + } } - // Layer rect in NDC; GL origin bottom-left matches the - // compositeOffset convention. - const F32 x0 = 2.f * (F32)dst_x / (F32)dst_w - 1.f; - const F32 y0 = 2.f * (F32)dst_y / (F32)dst_h - 1.f; - const F32 x1 = 2.f * (F32)(dst_x + w) / (F32)dst_w - 1.f; - const F32 y1 = 2.f * (F32)(dst_y + h) / (F32)dst_h - 1.f; - - // Bind and draw the layer quad. - static const LLStaticHashedString sBlitRect("blit_rect"); - gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, src_tex); - mBlitShader->uniform4f(sBlitRect, x0, y0, x1, y1); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); - - // Reverse fence: the producer waits on this before writing into - // the buffer again. - if (sync) + if (can_draw) { - sync->placeReadCompleteFence(); + mBlitShader->unbind(); } } - if (shader_ready) - { - mBlitShader->unbind(); - } - // Layers composited into the present surface; flush any batched GL and // present (the old img.flush() path flushed gGL before swapBuffers). gGL.flush(); diff --git a/indra/llrender/llcompositor.h b/indra/llrender/llcompositor.h index 0d479f136b8..dbc3d97dcee 100644 --- a/indra/llrender/llcompositor.h +++ b/indra/llrender/llcompositor.h @@ -123,8 +123,15 @@ class LLCompositor // The blit shader we composite with (compositorblitV/F.glsl, owned // and compiled by LLViewerShaderMgr). Until it's set and compiled - // we present without drawing any layers. - void setBlitShader(LLGLSLShader* shader) { mBlitShader = shader; } + // we present without drawing any layers. Callable from any thread: + // the shader manager clears this before re-linking the program and + // sets it again after, and the lock makes that handoff wait out any + // in-flight composite instead of deleting the program under it. + void setBlitShader(LLGLSLShader* shader) + { + std::lock_guard lock(mBlitShaderMutex); + mBlitShader = shader; + } // Debug overlay stats (Show Render Info). Per-layer numbers come // straight from the producer's own metrics - we do no bookkeeping @@ -175,8 +182,12 @@ class LLCompositor U32 mPresentCount = 0; // compositor-thread-only F64 mPresentWindowStart = 0.0; // compositor-thread-only - // Set by the viewer once LLViewerShaderMgr has compiled it. + // Set by the viewer once LLViewerShaderMgr has compiled it. Guarded by + // mBlitShaderMutex: presentFrame holds the lock across the composite so + // a shader reload on the viewer thread can't delete the program while + // we're drawing with it. LLGLSLShader* mBlitShader = nullptr; + std::mutex mBlitShaderMutex; }; #endif // LL_LLCOMPOSITOR_H diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index cfb4c30fe52..af4c7960c4c 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -114,9 +114,6 @@ void LLRenderTarget::resize(U32 resx, U32 resy) { S32 pix_diff = (resx*resy)-(mResX*mResY); - mResX = resx; - mResY = resy; - llassert(mInternalFormat.size() == mTex.size()); for (U32 i = 0; i < mTex.size(); ++i) @@ -127,16 +124,43 @@ void LLRenderTarget::resize(U32 resx, U32 resy) // unguarded - we hold the lease, so getTexName() would self-deadlock. // The GL name is stable across re-spec, so the LLImageGL stays valid. LLUniqueLease lease = mTex[i]->getUniqueLease(); + + // The consumer's CPU-side lease is released before ours granted, but + // its GPU reads of the old storage can still be in flight. Same + // contract as any other writer into this attachment: wait its + // read-complete fence (server-side, orders our re-spec after the + // read) before touching the storage. No-op without cross-context + // sync. + mTex[i]->waitReadCompleteFence(); + + if (i == 0) + { + // Publish the new size under the first attachment's unique + // lease. The compositor reads getWidth/getHeight while holding + // this attachment's shared lease across its draw, so the lease + // pair orders size against storage. + mResX = resx; + mResY = resy; + } + gGL.getTexUnit(0)->bindManual(mUsage, mTex[i]->getTexNameRaw()); LLImageGL::setManualImage(LLTexUnit::getInternalType(mUsage), 0, mInternalFormat[i], mResX, mResY, GL_RGBA, GL_UNSIGNED_BYTE, NULL, false); sBytesAllocated += pix_diff*4; } + if (mTex.empty()) + { + // Depth-only RT: no color attachment lease to publish under. + mResX = resx; + mResY = resy; + } + // Only the owner re-specs depth; a borrower (mDepth null, mSharedDepth set) // sees the owner's re-spec through the shared GL name. if (mDepth.notNull()) { LLUniqueLease lease = mDepth->getUniqueLease(); + mDepth->waitReadCompleteFence(); U32 internal_type = LLTexUnit::getInternalType(mUsage); gGL.getTexUnit(0)->bindManual(mUsage, mDepth->getTexNameRaw()); LLImageGL::setManualImage(internal_type, 0, GL_DEPTH_COMPONENT24, mResX, mResY, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL, false); diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index d95829fbf38..067db112e73 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -62,6 +62,7 @@ #include #include #include +#include #include #include // std::pair @@ -1928,8 +1929,21 @@ void LLWindowWin32::recreateWindow(RECT window_rect, DWORD dw_ex_style, DWORD dw sWindowHandleForMessageBox = mWindowHandle; } +// wglCreateContextAttribsARB / wglMakeCurrent / wglDeleteContext all mutate +// per-driver state keyed on the window's single (CS_OWNDC) HDC. Called +// concurrently from multiple threads - which happens when the compositor's +// producer threads spin up together - wglMakeCurrent sporadically fails +// (returns FALSE, leaves no current context), which then no-ops glGenTextures +// and cascades into texture-allocation failures. Serialize all context +// create/bind/destroy so those calls never overlap on the shared HDC. This is +// startup-only (each thread binds its context once and keeps it), so it costs +// nothing per frame. +static std::mutex sGLContextMutex; + void* LLWindowWin32::createSharedContext() { + std::lock_guard lock(sGLContextMutex); + mMaxGLVersion = llclamp(mMaxGLVersion, 3.f, 4.6f); S32 version_major = llfloor(mMaxGLVersion); @@ -1986,26 +2000,24 @@ void* LLWindowWin32::createSharedContext() void LLWindowWin32::makeContextCurrent(void* contextPtr) { - // TEMP ctxdiag: confirm whether wglMakeCurrent actually succeeds per - // thread. All contexts share the single mhDC; an HDC can only be current - // on one thread at a time, so concurrent producers may be failing here - // silently (no current context -> glGenTextures no-ops). - BOOL ok = wglMakeCurrent(mhDC, (HGLRC) contextPtr); - DWORD err = ok ? 0 : GetLastError(); - HGLRC after = wglGetCurrentContext(); - HDC afterdc = wglGetCurrentDC(); - LL_INFOS("ctxdiag") << "makeContextCurrent thread=" << LLThread::currentID() - << " req_rc=" << contextPtr - << " mhDC=" << (void*)mhDC - << " ok=" << (S32)ok - << " GetLastError=" << (U32)err - << " cur_rc=" << (void*)after - << " cur_dc=" << (void*)afterdc << LL_ENDL; + // Serialize against other threads' context create/bind/destroy - see + // sGLContextMutex. Without this, concurrent wglMakeCurrent calls on the + // shared HDC sporadically fail and leave the thread with no current + // context. + std::lock_guard lock(sGLContextMutex); + + if (!wglMakeCurrent(mhDC, (HGLRC) contextPtr)) + { + LL_WARNS() << "wglMakeCurrent failed (GetLastError=" << (U32)GetLastError() + << ") for context " << contextPtr << " on thread " + << LLThread::currentID() << LL_ENDL; + } LL_PROFILER_GPU_CONTEXT; } void LLWindowWin32::destroySharedContext(void* contextPtr) { + std::lock_guard lock(sGLContextMutex); wglDeleteContext((HGLRC)contextPtr); } diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index d770dd97310..bbbc5a5a07e 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -3758,9 +3758,9 @@ bool LLAppViewer::initWindow() mCompositor.attachToWindow(gViewerWindow->getWindow(), w, h); mCompositor.addCompositable(this); - // Shader objects are shared across the context share group, so - // the compositor can use this program from the OS main thread. - mCompositor.setBlitShader(&gCompositorBlitProgram); + // The blit shader is handed to the compositor by + // LLViewerShaderMgr::loadShadersInterface once it's linked (and + // taken away / re-handed around every shader reload). // Seed the vsync mode; the compositor applies the interval on // its own context at the first present. diff --git a/indra/newview/llscenemonitor.cpp b/indra/newview/llscenemonitor.cpp index 48c4eca3237..3888ac39fea 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -313,15 +313,21 @@ void LLSceneMonitor::capture() gGL.getTexUnit(0)->bind(&cur_target); - // Read from the most recently presented swap chain image - the - // frame the user is actually seeing. The compositor owns the - // swap chain now. - LLSwapChain& src = const_cast(LLAppViewer::instance()->getCompositor().getSwapChain()); + // Read from our own back buffer - the world frame this thread just + // rendered. The swap chain surface belongs to the compositor's + // context on the OS main thread; reading GL_BACK from here while it + // concurrently composites and swaps that same surface is undefined. + // The back buffer holds the same scene (pre-UI), which is what the + // scene-load diff wants anyway. + LLRenderTarget& src = LLAppViewer::instance()->getBackBuffer(); src.bindForRead(); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, cur_target.getWidth(), cur_target.getHeight()); //copy the content - src.unbindRead(); + // Restore the read binding to the current draw target (the same + // restore LLSwapChain::unbindRead does). + glBindFramebuffer(GL_READ_FRAMEBUFFER, LLRenderTarget::sCurFBO); + glReadBuffer(LLRenderTarget::sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); mDiffState = NEED_DIFF; } diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 3b23af58ae1..a38e362ee81 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -29,6 +29,8 @@ #include +#include "llappviewer.h" +#include "llcompositor.h" #include "llfeaturemanager.h" #include "llviewershadermgr.h" #include "llviewercontrol.h" @@ -3231,12 +3233,31 @@ bool LLViewerShaderMgr::loadShadersInterface() if (success) { + // The compositor draws with this program from the OS main thread + // (shader objects are shared across the context share group). Take + // it away before re-linking: setBlitShader blocks until any + // in-flight composite finishes, so we never delete the program out + // from under a present. + LLCompositor& compositor = LLAppViewer::instance()->getCompositor(); + compositor.setBlitShader(nullptr); + gCompositorBlitProgram.mName = "Compositor Blit Shader"; gCompositorBlitProgram.mShaderFiles.clear(); gCompositorBlitProgram.mShaderFiles.push_back(make_pair("interface/compositorblitV.glsl", GL_VERTEX_SHADER)); gCompositorBlitProgram.mShaderFiles.push_back(make_pair("interface/compositorblitF.glsl", GL_FRAGMENT_SHADER)); gCompositorBlitProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gCompositorBlitProgram.createShader(); + if (!success) + { + // The compositor cannot present a single frame without this + // shader - there is no fallback blit path, and a viewer that + // can't present just hangs on its first publish. Fail hard and + // loud instead. + LL_ERRS() << "Unable to load compositor blit shader, verify graphics driver installed and current." << LL_ENDL; + } + + // Linked and ready - hand it back to the compositor. + compositor.setBlitShader(&gCompositorBlitProgram); } if (success) diff --git a/indra/newview/llviewerthread.cpp b/indra/newview/llviewerthread.cpp index 392fdf9a0b9..0d7bd2898bb 100644 --- a/indra/newview/llviewerthread.cpp +++ b/indra/newview/llviewerthread.cpp @@ -46,7 +46,14 @@ LLViewerThread::LLViewerThread(LLWindow* window) // The shared context has to be created on the OS main thread, // which is where this constructor runs. mContext = mWindow->createSharedContext(); - llassert(mContext != nullptr); + if (!mContext) + { + // An llassert alone compiles out in Release, and running without a + // context doesn't fail loudly on its own: wglMakeCurrent(hdc, NULL) + // succeeds, so the whole render thread would just no-op its GL with + // no diagnostic pointing here. Die with the cause named instead. + LL_ERRS() << "Failed to create the viewer render context, verify graphics driver installed and current." << LL_ENDL; + } } LLViewerThread::~LLViewerThread() From bdb6a25e1751ebe334ca9d977aa1fa38fd5af7bc Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 14 Jul 2026 00:40:45 -0400 Subject: [PATCH 9/9] Delete llpipeline-library-feasibility.md --- doc/llpipeline-library-feasibility.md | 334 -------------------------- 1 file changed, 334 deletions(-) delete mode 100644 doc/llpipeline-library-feasibility.md diff --git a/doc/llpipeline-library-feasibility.md b/doc/llpipeline-library-feasibility.md deleted file mode 100644 index a201fa317d4..00000000000 --- a/doc/llpipeline-library-feasibility.md +++ /dev/null @@ -1,334 +0,0 @@ -# Feasibility Study: Extracting LLPipeline / the Render System into a Library - -**Status:** Draft for review -**Scope:** Architectural assessment only — no implementation -**Sibling document:** `doc/compositor-thread-feasibility.md` (the threading split; this -study assumes that work as orthogonal but compatible) - ---- - -## 1. Executive Summary - -The stated goal has three parts: - -1. `newview` / `secondlife-bin` becomes **headless by default**. -2. A **common renderer interface** exists so different rendering backends can be - selected at runtime (driver version, hardware, API availability, …). -3. OpenGL state is **removed from `secondlife-bin` and isolated** into a new - `llpipeline` library. - -**These are three different efforts of three very different sizes, and the study's -central finding is that they should not be pursued as one move.** Two of the three -are tractable and are *already in motion*. The third — literally extracting -`LLPipeline` and the draw-pool system into a standalone library — is the hard one, -and its feasibility reduces to a single question: - -> **Can we afford to insert an abstract drawable/scene interface that cuts the -> `LLDrawable ↔ LLViewerObject` knot?** - -That knot is the crux. `LLDrawable` does not *abstract* a scene object — it *wraps* -one: `lldrawable.h:41` includes `llviewerobject.h` (and `:43` includes -`llappviewer.h`). The draw pools downcast drawables to concrete `LLVO*` types -(terrain→`LLVOSurfacePatch`, water→`LLVOWater`, avatar→`LLVOAvatar`, -tree→`LLVOTree`). `LLSpatialPartition` branches on `getVObj()->getPCode()`. So any -"`llpipeline` library" that preserves that knot must drag `LLViewerObject`, -`LLVOVolume`, `LLVOAvatar`, `LLWorld`, `LLViewerRegion`, `LLViewerTexture`, … with -it — i.e. *most of `newview`* — which defeats the headless-bin goal, because the -app logic still needs its object model. **Without the seam: not feasible as -stated. With the seam: feasible, but a multi-month structural refactor.** - -**Verdict, by goal:** - -| Goal | Feasibility | Where the work lives | -|---|---|---| -| Backend-agnostic renderer interface | **Feasible, in progress** | inside `llrender` (extend `LLSwapChain`/`LLGPUResource`) | -| Headless by default | **Feasible, modest** | gate GL init in `llappviewer`; null backend | -| Extract `LLPipeline` + draw pools into a library | **Not feasible as stated; feasible only after a scene-interface refactor** | a new `llrender`-adjacent lib, *after* the knot is cut | - -**Recommendation:** pursue the backend interface and headless gating now — they sit -in `llrender`, which is already a clean lower-level library, and they deliver most -of the practical value the goal is reaching for. Treat the literal `LLPipeline` -extraction as a *later, optional* phase, explicitly gated on first building the -abstract drawable/scene seam. This is not a redirect away from the goal — it is the -path the in-flight `LLGPUResource` / `LLResourceLease` / `LLSwapChain` work is -already on. - ---- - -## 2. What "extract LLPipeline" actually entails - -`LLPipeline` is not a self-contained renderer. It is the **orchestrator of the -viewer's scene-to-screen process**, and it is fused to the viewer's domain model on -both sides: - -``` - INBOUND (~100 files reach in) OUTBOUND (pipeline reaches out) - UI / floaters / tools / settings gAgent, LLWorld, LLViewerRegion, - LLVO* scene objects ─── gPipeline ───► LLViewerCamera, LLSelectMgr, - startup / app / display │ LLEnvironment, LLViewerShaderMgr, - ▼ the entire LLViewerObject / LLVO* - draw pools, LLSpatialPartition, hierarchy, LLFace, ... - LLDrawable, LLFace -``` - -`pipeline.cpp` is ~11.5k lines and `#include`s ~90 headers, the large majority of -them viewer-domain: `llagent.h`, `llselectmgr.h`, `llworld.h`, `llviewerregion.h`, -`llviewerobject.h`, the whole `llvo*` family, plus UI (`llfloatertools.h`, -`llpanelface.h`, `llui.h`, `lltoolmgr.h`). This is the defining fact: **the pipeline -is woven through the viewer's object, world, agent, selection, and UI systems**, not -isolated behind them. - -### 2.1 Inbound coupling - -Roughly **~100 files** in `newview` reference `gPipeline` / `LLPipeline::`. The -traffic falls into three bands: - -- **Clean public API (fine across a boundary).** The dominant calls are drawable - lifecycle: `markRebuild`, `markMoved`, `markTextured`, plus render-type control - (`hasRenderType` / `toggleRenderType`) and `createObject`. These are proper - methods and would survive a library boundary unchanged. This band is the bulk of - the call *volume*. -- **Leaky static flags (a real problem).** External code reads — and in places - *writes* — pipeline statics directly: `sRenderingHUDs`, `sImpostorRender`, - `sShadowRender`, `sUnderWaterRender`, `sReflectionRender`, `sRenderDeferred`, - `sRenderTransparentWater`, and more. Some writers are UI/feature code (e.g. the - 360-capture floater toggling `sRenderAttachedLights` / `sUseOcclusion`; - settings-handlers in `llviewercontrol.cpp`). These are an informal shared-state - contract, not an API. -- **Direct member reach-in (the leakiest).** External code touches pipeline member - objects and render targets directly: `mReflectionMapManager`, `mHeroProbeManager`, - internal `LLRenderTarget`s (water reflection, exposure/luminance/scene maps used by - the GLTF material preview), and even increments instrumentation counters - (`mTextureMatrixOps`). The GLTF material-preview path chains calls through several - internal render targets to do its own post-processing. - -The clean band is library-ready. The other two bands are an **encapsulation debt** -that must be paid down (accessors / a render-control facade / a state -snapshot-restore API) *before* a boundary can be drawn — independent of any backend -question. - -> Note on precision: the counts above are deliberately given as magnitudes. They -> come from grep-style occurrence counting, which is fine for ranking the coupling -> but is not a measurement to base a schedule on. - -### 2.2 Outbound coupling — the knot - -The harder direction. The render system's core data structures *are* scene-graph -structures: - -- **`LLDrawable` wraps `LLViewerObject`.** `lldrawable.h:41` `#include - "llviewerobject.h"`; the constructor takes `LLViewerObject*`; accessors like - `getRegion()`, `getTextureEntry()`, `getVOVolume()`, `isAvatar()` forward straight - to the wrapped object. It is a companion struct, not an abstraction. -- **`LLFace` binds geometry to a viewer object.** Constructed from - `LLViewerObject*`; `getWorldMatrix()` / `getTextureEntry()` call through to it; it - downcasts to `LLVOVolume` / `LLVOAvatar` for volume face data. -- **Draw pools are specialized per `LLVO*` subclass.** Terrain reads - `LLVOSurfacePatch` and `gAgent.getRegion()`; water casts faces to `LLVOWater` and - reads water settings; the avatar pool pulls in `llvoavatar.h` + `llagent.h`; tree - reads `getRegion()->mRenderMatrix`. -- **`LLSpatialPartition` choreographs LOD/visibility by object type**, branching on - `getVObj()->getPCode()` and downcasting to `LLVOVolume` / `LLVOAvatar` / - `LLControlAvatar`. - -The **one genuine abstraction seam already present** is `LLDrawInfo` (the per-batch -descriptor in `llspatialpartition.h`: vertex buffer + texture + model matrix + -shader + counts). Everything at `LLDrawInfo` level and below is backend-shaped data; -everything above it (`LLDrawable`, `LLFace`, `LLSpatialPartition`, the pools' -*setup*) is scene-graph logic. But `LLDrawInfo` is a thin slice — the issuing of -draw calls — not the bulk of the pipeline. - -`LLVertexBuffer` already lives in `llrender`, so the lowest layer is in the right -place. The problem is everything between `LLDrawInfo` and `LLViewerObject`. - ---- - -## 3. The backend-agnostic renderer interface - -This is the genuinely valuable and *separable* piece — and the part already moving. - -### 3.1 Where things stand - -`llrender` is **already a clean lower-level library**: it links `llcommon`, -`llimage`, `llmath`, `llwindow`, etc., and pulls in **no `newview` headers**. Where -it needs viewer services it inverts the dependency — e.g. `LLTextureManagerBridge` -(`gTextureManagerBridgep`) lets `llrender` call back into the viewer's texture -manager without compile-time coupling. `llrender.h`'s own header comment states the -intent outright: *"to define an interface for a multiple rendering API -abstraction."* And the in-flight files in this branch — `llgpuresource.h/.cpp`, -`llresourcelease.h`, `llswapchain.h/.cpp` — are the first courses of exactly that -wall, with `LLImageGL` as the first `LLGPUResource` adopter. - -So the right home for the renderer interface is **inside / beneath `llrender`**, not -a new `llpipeline` library. The backend question is about *how draw commands and GPU -resources are realized*, which is `llrender`'s job; it is not about *what scene to -draw*, which is `LLPipeline`'s job. - -### 3.2 The size of the backend job - -OpenGL is **pervasively assumed**, not funneled through one thin layer. There is -partial indirection — enum-translation tables (`LLRender` enums → `GL_*`), RAII -state wrappers (`LLGLEnable`, `LLGLDepthTest`, `LLGLSPipeline`), and the immediate- -mode `gGL` emulator that buffers vertices and caches matrix/texture/light state. But -beneath that: - -- Raw `glXxx` calls are scattered across `llrendertarget.cpp` (FBO management), - `llvertexbuffer.cpp` (VBOs), `llglslshader.cpp` (program/uniform calls), - `llrender.cpp` (texture units, blend, cull), and `llimagegl.cpp` (textures). -- GL types leak into public signatures (`GLuint`, `GLenum`, `GLint` in - `LLImageGL` / `LLVertexBuffer` / `LLRender` APIs). -- **Shaders are GLSL-specific end to end.** `LLGLSLShader` calls - `glAttachShader`/`glLinkProgram`/`glGetUniformLocation`/`glUniform*` directly; - `LLViewerShaderMgr` (in `newview`) loads `.glsl` files and bakes permutations in as - preprocessor `#define`s. A non-GL backend needs a different shader story - (SPIR-V / cross-compilation), and this is the single largest sub-cost. - -This is **systematic refactoring, not a rewrite**: route the scattered `glXxx` -through a backend interface, replace GL types in public APIs with opaque resource -handles (the `LLGPUResource` direction), and add a shader-compilation abstraction. A -useful completeness test is to stand up a **null backend** first (see §4) and a -single alternative backend second to validate the interface — before claiming it is -backend-agnostic. - ---- - -## 4. Headless by default - -This is the smallest and most independently shippable of the three goals — and a -prerequisite that falls out naturally from the backend interface (a "null" backend -*is* headless). - -**Current state is shallow.** `gHeadlessClient` is read in `llappviewer.cpp:3270`, -but the GL bring-up immediately after runs **unconditionally**: `gPipeline.init()` -(`:3397`), `gViewerWindow->initGLDefaults()` (`:3401`), and -`mSwapChain.attachToWindow()` (`:3406`) have no headless guard. The *only* thing -`gHeadlessClient` actually gates is the per-frame `display()` call -(`llappviewer.cpp:1488`; early-out at `llviewerdisplay.cpp:551`). So today "headless" -means "still build the whole GL pipeline, just don't draw each frame." - -**True headless-by-default** therefore requires gating *initialization*, not just -the frame loop: - -- A null renderer backend selected when no GL context is available/desired, so - `gPipeline.init()`, shader compilation, and swap-chain attach become no-ops or - route to stubs. -- Confirming that feature/GPU detection (`llfeaturemanager` via `gGLManager`) and - `LLViewerShaderMgr` tolerate the null backend. - -Critically, this is achievable **without moving `LLPipeline` anywhere** — it is a -gating + null-backend exercise on top of §3. It is the cleanest near-term win and a -good forcing function for the backend interface's completeness. - ---- - -## 5. Why the "one new llpipeline library" framing misleads - -The goal pictures: `secondlife-bin` = headless app logic with no GL, and a new -`llpipeline` library = all rendering + all GL, selectable by backend. The trouble is -that **"all rendering" in this codebase is not separable from "the scene/world/ -objects."** Moving `LLPipeline` + draw pools + `LLDrawable` + `LLSpatialPartition` -into a library drags `LLViewerObject`, the `LLVO*` hierarchy, `LLWorld`, -`LLViewerRegion`, `LLFace`, and `LLViewerTexture` along — and those depend back on -the pipeline (the ~100 inbound files), producing a **circular dependency** that a -static library boundary cannot express. You would not end up with "thin headless bin -+ render lib"; you would end up with "two libraries that each need the other," i.e. -the monolith with a seam drawn through its middle. - -The backend/GL-isolation value the goal is reaching for does **not** require that -move. GL isolation belongs in `llrender` (§3); headless belongs in app-init gating -(§4). The `LLPipeline`-as-library move only becomes coherent *after* the -`LLDrawable ↔ LLViewerObject` knot is cut — and at that point the library boundary -is the abstract scene interface, not "`LLPipeline`." - ---- - -## 6. If the literal extraction is still wanted: the seam - -Should the team decide the `LLPipeline` library is a hard requirement, the -unavoidable enabling work is an **abstract drawable/scene interface** that the render -system consumes *instead of* `LLViewerObject`: - -1. Define an abstract render-item interface (geometry, transform, material, LOD, - visibility) that exposes what pools and the spatial partition need, with **no - `LLViewerObject` knowledge**. `LLDrawInfo` is the model for the batch level; the - harder part is abstracting what `LLDrawable`/`LLFace` expose. -2. Make `LLViewerObject` / `LLVO*` *implement* (or feed) that interface; invert the - downcasts in the pools and spatial partition so they call the interface, not - concrete `LLVO*` types. -3. Pay down the §2.1 inbound debt: replace direct static-flag and member reach-ins - with a render-control facade and an explicit save/restore state API. -4. Only then move `LLPipeline` + pools + the now-abstract drawable/partition into a - library that depends on the interface, leaving `LLViewerObject` and the world - model in `newview`. - -Steps 1–3 are independently valuable (they improve the monolith even if the library -is never built) and are where the multi-month cost lives. Step 4 is comparatively -mechanical once 1–3 are done. - ---- - -## 7. Risk register - -| # | Risk | Severity | Note | -|---|------|----------|------| -| 1 | Treating the three goals as one move | **High** | Forces the impossible-as-stated extraction to gate the tractable wins; decouple them | -| 2 | `LLDrawable`/`LLFace` wrap `LLViewerObject` directly | **High** | The structural blocker; only an abstract scene interface resolves it | -| 3 | Circular dependency between a render lib and the object model | **High** | A static-lib boundary cannot express it; §5 | -| 4 | Leaky pipeline statics / member reach-ins (~2 of 3 coupling bands) | Medium | Encapsulation debt; payable independently of backend work | -| 5 | GLSL baked through `LLGLSLShader` + `LLViewerShaderMgr` | Medium-High | Largest sub-cost of the backend interface; needs a shader-IR story | -| 6 | "Headless" today only gates `display()`, not init | Medium | Verified; true headless needs init gating + null backend (§4) | -| 7 | Pool specialization per `LLVO*` subtype | Medium | Terrain/water/avatar/tree pools encode object-type logic, not generic geometry | -| 8 | Backend interface declared "done" without a second backend | Medium | Stand up null + one real alternate backend to prove completeness | - ---- - -## 8. Recommended path - -**Phase A — Backend interface + GL isolation, inside `llrender` (in progress).** -Continue the `LLGPUResource` / `LLResourceLease` / `LLSwapChain` work: opaque GPU -resource handles, a backend seam under `LLRender`/`LLRenderTarget`/`LLVertexBuffer`, -GL types out of public APIs. This isolates GL *below an interface* without moving -`LLPipeline`. - -**Phase B — Null backend + headless-by-default.** Add a null backend and gate -`gPipeline.init()` / shader compile / swap-chain attach on backend availability -(§4). Ship headless-by-default as a standalone capability. Doubles as the -completeness test for Phase A. - -**Phase C — Shader abstraction.** Decouple `LLGLSLShader` / `LLViewerShaderMgr` from -GLSL specifics (compilation + permutation strategy). Largest single backend sub-cost; -required before any non-GL backend is real. - -**Phase D — Encapsulation debt paydown.** Replace pipeline static-flag and -render-target reach-ins (§2.1 bands 2–3) with accessors / a render-control facade / -state snapshot API. Improves the monolith regardless of later steps. - -**Phase E (optional, gated) — The scene interface and the `llpipeline` library.** -Only if a true library split is still required: build the abstract drawable/scene -interface (§6), invert the pool/partition downcasts, then lift `LLPipeline` + pools -+ abstract drawable/partition into a library. This is the multi-month structural -effort; do not start it until A–D have proven the seams. - -A–D deliver essentially all of the stated *intent* — selectable backends, GL -isolated out of the bin's logic, headless by default — and are where current work -already points. E is the only part that is "extract `LLPipeline`" literally, and it -should be entered deliberately, with eyes open, as a scene-graph refactor rather than -a packaging change. - ---- - -## 9. Recommendation - -**Decouple the goal into its three real efforts and pursue them in dependency -order.** The backend interface and GL isolation (Phase A) and headless-by-default -(Phase B) are feasible, high-value, and already underway in `llrender` — that is the -correct home for them. The literal extraction of `LLPipeline` and the draw pools into -a library is **not feasible as stated**, because the render system's core types wrap -the viewer's object model rather than abstracting it; it becomes feasible only after -an abstract drawable/scene interface cuts the `LLDrawable ↔ LLViewerObject` knot, and -that interface — not "`LLPipeline`" — is the real library boundary. - -Frame the program as: *isolate the backend now, go headless now, and treat the -pipeline library as a later option that the scene-interface work unlocks.* That keeps -every near-term step shippable and avoids letting the hardest, least-certain piece -block the value that is already within reach.