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/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 fcd287bbb39..c4a6ac3634b 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -28,6 +28,10 @@ set(llrender_SOURCE_FILES llrendernavprim.cpp llrendersphere.cpp llrendertarget.cpp + llswapchain.cpp + llcompositor.cpp + lltestsquarecompositable.cpp + llgpuresource.cpp llshadermgr.cpp lltexture.cpp lltexturemanagerbridge.cpp @@ -59,6 +63,12 @@ set(llrender_HEADER_FILES llrender2dutils.h llrendernavprim.h llrendersphere.h + llswapchain.h + llcompositor.h + llcompositable.h + lltestsquarecompositable.h + llgpuresource.h + llresourcelease.h llshadermgr.h lltexture.h lltexturemanagerbridge.h diff --git a/indra/llrender/llcompositable.h b/indra/llrender/llcompositable.h new file mode 100644 index 00000000000..64a2e84904e --- /dev/null +++ b/indra/llrender/llcompositable.h @@ -0,0 +1,103 @@ +/** + * @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; + + // 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. + 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); } + +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() + { + 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 + } + +private: + std::atomic mReady{false}; + std::atomic mFrameMs{0.f}; + 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..ffeff20066d --- /dev/null +++ b/indra/llrender/llcompositor.cpp @@ -0,0 +1,397 @@ +/** + * @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 "llimagegl.h" +#include "llgl.h" +#include "llglstates.h" +#include "llglslshader.h" +#include "llwindow.h" + +#include +#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; + + // 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); + + // 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. + + const GLint dst_w = (GLint)mSwapChain.getWidth(); + const GLint dst_h = (GLint)mSwapChain.getHeight(); + + const F64 present_start = LLTimer::getTotalSeconds(); + + // 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(); + + { + // 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) + { + 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) + { + // 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(); + } + } + + if (can_draw) + { + 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(); + mSwapChain.present(); + + 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; + } + + // 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 new file mode 100644 index 00000000000..dbc3d97dcee --- /dev/null +++ b/indra/llrender/llcompositor.h @@ -0,0 +1,193 @@ +/** + * @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 +#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.isAttached(); } + + // 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(); + + // 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); } + + // 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. + 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. 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 + // 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; + 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. + // 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. 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/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/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 a268ea07bb4..c358f64b8ed 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; @@ -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); + U32 name = texture->getTexture(index); + gGL.getTexUnit(uniform)->bindManual(texture->getUsage(), name, has_mips); } gGL.getTexUnit(uniform)->setTextureFilteringOption(mode); 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/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..e63925c8e22 --- /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 (mLeaseMutex->try_lock()) + { + mLeaseMutex->unlock(); + } + else + { + llassert(false); // a lease holder outlived this resource + } +} + +// - LLSharedLease ---------------------------------------------------------- + +LLSharedLease::LLSharedLease(LLGPUResource* res) + : mResource(res) +{ + if (mResource) + { + mResource->mLeaseMutex->lock_shared(); + } +} + +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->mLeaseMutex->unlock_shared(); + mResource = nullptr; + } +} + +// - LLUniqueLease ---------------------------------------------------------- + +LLUniqueLease::LLUniqueLease(LLGPUResource* res) + : mResource(res) +{ + if (mResource) + { + mResource->mLeaseMutex->lock(); + } +} + +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->mLeaseMutex->unlock(); + mResource = nullptr; + } +} diff --git a/indra/llrender/llgpuresource.h b/indra/llrender/llgpuresource.h new file mode 100644 index 00000000000..e6b823bf119 --- /dev/null +++ b/indra/llrender/llgpuresource.h @@ -0,0 +1,120 @@ +/** + * @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 +#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) - 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. +class LLGPUResource +{ +public: + LLGPUResource() : mLeaseMutex(std::make_unique()) {} + virtual ~LLGPUResource(); + + LLGPUResource(const LLGPUResource&) = delete; + LLGPUResource& operator=(const LLGPUResource&) = delete; + + // 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. 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; + + // 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: + // 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 +// it lives. Use .get() to read the value. +// +// Safe: +// glBindTexture(target, img->getTexName().get()); +// auto name = img->getTexName(); ... name.get() ... +// +// 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: + 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..c310f708d7f 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() @@ -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) @@ -578,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"); } @@ -684,11 +687,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 +714,7 @@ bool LLImageGL::updateBindStats() const F32 LLImageGL::getTimePassedSinceLastBound() { + LLSharedLease lease = getSharedLease(); return sLastFrameTime - mLastBindTime ; } @@ -1054,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 } @@ -1506,14 +1512,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) { @@ -1630,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) { @@ -1718,12 +1725,8 @@ 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 deletes the old name and installs the new one. + syncTexName(new_texname); } } @@ -1740,42 +1743,19 @@ 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()); + + // 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"); - 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(); + placeFrameCompleteFence(); } ref(); @@ -1784,6 +1764,8 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) [=, this]() { LL_PROFILE_ZONE_NAMED("cglt - delete callback"); + // syncTexName takes the unique lease, which waits on the + // fence we placed above. syncTexName(new_tex_name); unref(); }); @@ -1791,9 +1773,88 @@ void LLImageGL::syncToMainThread(LLGLuint new_tex_name) LL_PROFILER_GPU_COLLECT; } +// ---- Cross-context GPU sync ------------------------------------------------ + +// 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() +{ + 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) + { + mCrossSync = std::make_shared(); + } + else if (!b) + { + mCrossSync.reset(); + } + // Cross-context textures take the CPU lease too (field accessors); + // single-context textures pay neither. + setLeaseEnabled(b); +} + +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::waitFrameCompleteFence() +{ + if (!mCrossSync) return; + std::shared_ptr fence; + { + 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::placeReadCompleteFence() +{ + 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; + { + std::lock_guard lock(mCrossSync->fenceMutex); + fence = mCrossSync->readCompleteFence; + } + if (fence) + { + glWaitSync((GLsync)fence.get(), 0, GL_TIMEOUT_IGNORED); + } +} + void LLImageGL::syncTexName(LLGLuint texname) { + // 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) @@ -1804,6 +1865,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 +2006,7 @@ void LLImageGL::destroyGLTexture() { checkActiveThread(); + LLUniqueLease lease = getUniqueLease(); if (mTexName != 0) { if(mTextureMemory != S64Bytes(0)) @@ -2002,6 +2070,7 @@ void LLImageGL::setFilteringOption(LLTexUnit::eTextureFilterOptions option) bool LLImageGL::getIsResident(bool test_now) { + LLSharedLease lease = getSharedLease(); if (test_now) { if (mTexName != 0) @@ -2019,6 +2088,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 +2100,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 +2112,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 +2126,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 +2148,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 +2397,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 +2463,29 @@ 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 +{ + // 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(); bool res = true; if (mPickMask) diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e7..1393aa1fc1f 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -37,8 +37,13 @@ #include "llunits.h" #include "llthreadsafequeue.h" #include "llrender.h" +#include "llgpuresource.h" #include "threadpool.h" #include "workqueue.h" +#include +#include +#include +#include #include #define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL @@ -57,7 +62,7 @@ namespace LLImageGLMemory } //============================================================================ -class LLImageGL : public LLRefCount +class LLImageGL : public LLThreadSafeRefCount, public LLGPUResource { friend class LLTexUnit; public: @@ -114,6 +119,7 @@ class LLImageGL : public LLRefCount void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); + public: virtual void dump(); // debugging info to LL_INFOS() @@ -168,8 +174,35 @@ 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 holds a shared lease for the caller's scope. + // 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; @@ -251,6 +284,18 @@ class LLImageGL : public LLRefCount U16 mHeight; S8 mCurrentDiscardLevel; + // 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; protected: @@ -273,7 +318,7 @@ class LLImageGL : public LLRefCount LLGLenum mFormatType; bool mFormatSwapBytes;// if true, use glPixelStorei(GL_UNPACK_SWAP_BYTES, 1) - bool mExternalTexture; + bool mExternalTexture = false; // STATICS public: @@ -321,7 +366,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..791ced8ef82 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -196,22 +196,43 @@ 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 - the calls below take their own. 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 +248,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) + // 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) { - 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 +319,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; + // 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(); + + 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 +379,6 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 } stop_glerror(); - return true; } @@ -343,28 +394,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; diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 0b0d69812f0..af4c7960c4c 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -29,8 +29,9 @@ #include "llrendertarget.h" #include "llrender.h" #include "llgl.h" +#include "llimagegl.h" -LLRenderTarget* LLRenderTarget::sBoundTarget = NULL; +thread_local LLRenderTarget* LLRenderTarget::sBoundTarget = NULL; U32 LLRenderTarget::sBytesAllocated = 0; void check_framebuffer_status() @@ -51,22 +52,25 @@ void check_framebuffer_status() } bool LLRenderTarget::sUseFBO = false; -U32 LLRenderTarget::sCurFBO = 0; +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), 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() @@ -74,33 +78,113 @@ 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; +} + +// --------------------------------------------------------------------------- +// 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) { - //for accounting, get the number of pixels added/subtracted S32 pix_diff = (resx*resy)-(mResX*mResY); - mResX = resx; - mResY = resy; - llassert(mInternalFormat.size() == mTex.size()); for (U32 i = 0; i < mTex.size(); ++i) - { //resize color attachments - 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(); + + // 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 (mDepth) + 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()) { - gGL.getTexUnit(0)->bindManual(mUsage, mDepth); + 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); 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) { @@ -122,7 +206,6 @@ 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)); } @@ -137,11 +220,11 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT 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); } @@ -152,10 +235,10 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, LLT 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.isNull()); + llassert(mTex.empty()); llassert(!isBoundInStack()); if (mFBO == 0) @@ -167,12 +250,18 @@ 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); + // 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, @@ -188,8 +277,8 @@ 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); glBindFramebuffer(GL_FRAMEBUFFER, mFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, LLTexUnit::getInternalType(mUsage), 0, 0); @@ -222,35 +311,26 @@ bool LLRenderTarget::addColorAttachment(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; 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(); } @@ -271,14 +351,14 @@ bool LLRenderTarget::addColorAttachment(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) @@ -287,20 +367,16 @@ bool LLRenderTarget::addColorAttachment(U32 color_fmt) flush(); } - return true; } bool LLRenderTarget::allocateDepth() { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; - 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; @@ -323,7 +399,7 @@ void LLRenderTarget::shareDepthBuffer(LLRenderTarget& target) 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; } @@ -333,61 +409,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::setNeedsCrossContextSync(bool b) +{ + // 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()) + { + mTex[0]->setNeedsCrossContextSync(b); + } +} + +bool LLRenderTarget::needsCrossContextSync() const +{ + return !mTex.empty() && mTex[0].notNull() && mTex[0]->needsCrossContextSync(); +} + +LLImageGL* LLRenderTarget::getColorAttachmentImage() const +{ + // 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()) + { + return mTex[0].get(); + } + return nullptr; +} + void LLRenderTarget::release() { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; llassert(!isBoundInStack()); - if (mDepth) - { - LLImageGL::deleteTextures(1, &mDepth); - - mDepth = 0; + mIsSwapChainImage = false; - 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) - { //detach shared depth buffer + 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); } - // Detach any extra color buffers (e.g. SRGB spec buffers) - // - 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) @@ -400,13 +510,6 @@ void LLRenderTarget::release() mFBO = 0; } - if (mTex.size() > 0) - { - sBytesAllocated -= mResX*mResY*4; - LLImageGL::deleteTextures(1, &mTex[0]); - } - - mTex.clear(); mInternalFormat.clear(); mResX = mResY = 0; @@ -415,20 +518,19 @@ void LLRenderTarget::release() void LLRenderTarget::bindTarget() { LL_PROFILE_GPU_ZONE("bindTarget"); - llassert(mFBO); llassert(!isBoundInStack()); + llassert(mFBO); 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); } @@ -439,9 +541,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; @@ -451,11 +568,11 @@ void LLRenderTarget::clear(U32 mask_in) { LL_PROFILE_GPU_ZONE("clear"); llassert(mFBO); + U32 mask = GL_COLOR_BUFFER_BIT; if (mUseDepth) { mask |= GL_DEPTH_BUFFER_BIT; - } if (mFBO) { @@ -475,13 +592,13 @@ void LLRenderTarget::clear(U32 mask_in) U32 LLRenderTarget::getTexture(U32 attachment) const { - 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 0; } - return mTex[attachment]; + return mTex[attachment]->getTexNameRaw(); } U32 LLRenderTarget::getNumTextures() const @@ -491,7 +608,10 @@ U32 LLRenderTarget::getNumTextures() const 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); + // 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); } @@ -499,9 +619,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,23 +636,52 @@ 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. Pop the bookkeeping and leave + // the framebuffer bound for the next 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() +{ + // Doesn't read instance state - no lease needed. + glBindFramebuffer(GL_READ_FRAMEBUFFER, sCurFBO); + glReadBuffer(sCurFBO ? GL_COLOR_ATTACHMENT0 : GL_BACK); } bool LLRenderTarget::isComplete() const { - return !mTex.empty() || mDepth; + return !mTex.empty() || mDepth.notNull(); } void LLRenderTarget::getViewport(S32* viewport) @@ -545,6 +694,8 @@ void LLRenderTarget::getViewport(S32* viewport) bool LLRenderTarget::isBoundInStack() const { + // 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) { @@ -556,18 +707,14 @@ bool LLRenderTarget::isBoundInStack() const void LLRenderTarget::swapFBORefs(LLRenderTarget& other) { - // Must be initialized 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); @@ -582,3 +729,27 @@ void LLRenderTarget::swapFBORefs(LLRenderTarget& other) std::swap(mFBO, other.mFBO); std::swap(mTex, other.mTex); } + +U32 LLRenderTarget::getDepth() const +{ + // 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 +{ + // Just a snapshot - callers that need the value to stay valid should + // hold a shared lease themselves. + return mFBO; +} + +void LLRenderTarget::markAsSwapChainImage(bool yes) +{ + mIsSwapChainImage = yes; +} + +bool LLRenderTarget::isSwapChainImage() const +{ + return mIsSwapChainImage; +} diff --git a/indra/llrender/llrendertarget.h b/indra/llrender/llrendertarget.h index cd3290cf663..35d76158f07 100644 --- a/indra/llrender/llrendertarget.h +++ b/indra/llrender/llrendertarget.h @@ -31,6 +31,10 @@ #include "llgl.h" #include "llrender.h" +#include "llgpuresource.h" + +#include +#include /* Wrapper around OpenGL framebuffer objects for use in render-to-texture @@ -58,20 +62,45 @@ */ -class LLRenderTarget +// 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: // Whether or not to use FBO implementation static bool sUseFBO; static U32 sBytesAllocated; - static U32 sCurFBO; - static U32 sCurResX; - static U32 sCurResY; + // 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 thread_local bool sFlushRequiresParent; LLRenderTarget(); ~LLRenderTarget(); + // Movable so std::vector can reallocate. Only safe to + // move when no leases are held - see LLGPUResource. + LLRenderTarget(LLRenderTarget&&) noexcept; + 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 @@ -82,6 +111,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); + 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 + // 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 @@ -142,15 +190,38 @@ class LLRenderTarget //get Y resolution U32 getHeight() const { return mResY; } - LLTexUnit::eTextureType getUsage(void) const { return mUsage; } + LLTexUnit::eTextureType getUsage() const { return mUsage; } + // 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(void) const { return mDepth; } + U32 getDepth() const; + + // 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: 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 //should be used 1:1 with bindTarget @@ -172,22 +243,53 @@ class LLRenderTarget // *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; 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; 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; + +private: + // 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/llresourcelease.h b/indra/llrender/llresourcelease.h new file mode 100644 index 00000000000..b450a8626a8 --- /dev/null +++ b/indra/llrender/llresourcelease.h @@ -0,0 +1,78 @@ +/** + * @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 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: + 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 new file mode 100644 index 00000000000..663c83fd5f5 --- /dev/null +++ b/indra/llrender/llswapchain.cpp @@ -0,0 +1,119 @@ +/** + * @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 "llrendertarget.h" +#include "llwindow.h" + + +LLSwapChain::~LLSwapChain() +{ + release(); +} + +void LLSwapChain::attachToWindow(LLWindow* window, U32 width, U32 height) +{ + llassert(window != nullptr); + llassert(width > 0 && height > 0); + + mWindow = window; + mWidth = width; + mHeight = height; + + // 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 dimensions + } + + mWidth = width; + mHeight = height; + // GL: the default framebuffer follows the window; nothing to reallocate. +} + +void LLSwapChain::acquireNextImage() +{ + // GL: no real acquire - the driver owns the back-buffer rotation under + // SwapBuffers. Vk/XR backends do the WSI acquire here. +} + +void LLSwapChain::bindForRender() +{ + // 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(mWindow != nullptr); + + // 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::bindForRead() +{ + // 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() +{ + // Drop the strict-flush requirement so any late-shutdown top-level + // flushes don't trip the assert. + LLRenderTarget::sFlushRequiresParent = false; + + mWindow = nullptr; + mWidth = 0; + mHeight = 0; +} diff --git a/indra/llrender/llswapchain.h b/indra/llrender/llswapchain.h new file mode 100644 index 00000000000..0a96e9c89c0 --- /dev/null +++ b/indra/llrender/llswapchain.h @@ -0,0 +1,97 @@ +/** + * @file llswapchain.h + * @brief Backend-agnostic presentation surface for the window. + * + * $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" + +class LLWindow; + +// 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): 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): 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: + LLSwapChain() = default; + ~LLSwapChain(); + + LLSwapChain(const LLSwapChain&) = delete; + LLSwapChain& operator=(const LLSwapChain&) = delete; + + // GL: remember the window so present() can swapBuffers; record dimensions. + void attachToWindow(LLWindow* window, U32 width, U32 height); + + // 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); + + // 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(); + + // Bind the acquired image as the render target for compositing. GL: binds + // the default framebuffer (FBO 0) with the full-window viewport. + void bindForRender(); + + // Present the composited image. GL: swapBuffers(). Vk/XR: the real present. + void present(); + + // 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 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; } + +private: + 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 new file mode 100644 index 00000000000..2ae050a9632 --- /dev/null +++ b/indra/llrender/lltestsquarecompositable.cpp @@ -0,0 +1,214 @@ +/** + * @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 "llimagegl.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) +{ + mCompositor = &compositor; + start(); +} + +void LLTestSquareCompositable::disconnect() +{ + if (!isStopped()) + { + setQuitting(); + if (mCompositor) mCompositor->wakeSyncWaiters(); // break our waitForPresent + 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); + } +} + +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); + + // 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. + paint(); + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->placeFrameCompleteFence(); // also flushes + } + else + { + glFlush(); + } + 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 + + // wakeSyncWaiters (on disconnect) break the wait promptly. + U64 tick = mCompositor->waitForPresent(mLastTick + 1, [this]{ return isQuitting(); }); + if (tick <= mLastTick || isQuitting()) + { + 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. + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->waitReadCompleteFence(); + } + + mStep = (mStep + 3) % mSize; + paint(); + + // Publish: the compositor waits on this fence before sampling. + // placeFrameCompleteFence flushes so the other context sees it. + if (LLImageGL* sync = mRT.getColorAttachmentImage()) + { + sync->placeFrameCompleteFence(); + } + else + { + glFlush(); + } + produceFrame(); + } + } + + // 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() +{ + // 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; + } + } + + 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 new file mode 100644 index 00000000000..75a7eb76a12 --- /dev/null +++ b/indra/llrender/lltestsquarecompositable.h @@ -0,0 +1,102 @@ +/** + * @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" + +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 to the compositor's present clock via +// waitForPresent. +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; + + // Store the compositor (for the present clock) 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 + // 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(); + + 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; + + 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/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/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..11b60c1cda6 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -207,6 +207,26 @@ 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; } + + // 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/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 77023b6ca6d..067db112e73 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 @@ -61,6 +62,7 @@ #include #include #include +#include #include #include // std::pair @@ -105,13 +107,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; @@ -1926,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); @@ -1984,12 +2000,24 @@ void* LLWindowWin32::createSharedContext() void LLWindowWin32::makeContextCurrent(void* contextPtr) { - wglMakeCurrent(mhDC, (HGLRC) contextPtr); + // 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); } @@ -2011,6 +2039,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 +2194,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 +2205,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; + // 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); - // Inform the application of the new mouse position (needed for per-frame - // hover/picking to function). - mCallbacks->handleMouseMove(this, position.convert(), (MASK)0); - - // 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 +2224,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; } @@ -3016,6 +3174,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 @@ -3026,9 +3186,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); }); @@ -3038,6 +3195,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); @@ -3050,8 +3209,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); }); } @@ -3261,15 +3418,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..48916f5b904 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 @@ -7314,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/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/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/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 263c1f20540..bbbc5a5a07e 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" @@ -87,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" @@ -245,6 +247,8 @@ #include "gltfscenemanager.h" #include "workqueue.h" + +#include using namespace LL; // Include for security api initialization @@ -375,6 +379,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; @@ -577,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 } @@ -737,6 +749,553 @@ 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); + + // Front buffer exists now: the compositor may sample the world layer. + setReady(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) + { + // 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(); + } + else + { + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } + } + } + + // 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) + { + // 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) + { + // 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(); + } + 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: 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) + { + // 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 + // 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. + if (LLImageGL* sync = getBackBuffer().getColorAttachmentImage()) + { + sync->waitReadCompleteFence(); + } + + 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"); + + // 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(); + 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 + // back buffer, or if the snapshot machinery cleared + // gDisplaySwapBuffers to keep a readback-only frame off the screen. + if (mBackBufferRendered && gDisplaySwapBuffers) + { + markFrameRendered(); + } + gDisplaySwapBuffers = true; +} + class LLUITranslationBridge : public LLTranslationBridge { public: @@ -751,6 +1310,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 +1771,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 +1830,19 @@ 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 and macOS; other platforms stay single threaded. +#if LL_WINDOWS || LL_DARWIN + 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,333 +1917,104 @@ 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()) - { - LLWorld::createInstance(); - } - LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); - LLSD newFrame; + if (!LLApp::isExiting()) { - 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()) + // 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()) { - { - 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"); - gPipeline.mReflectionMapManager.update(); - LLFloaterSnapshot::update(); // take snapshots - LLFloaterSimpleSnapshot::update(); - gGLActive = false; - } - - if (LLViewerStatsRecorder::instanceExists()) - { - LLViewerStatsRecorder::instance().idle(); - } - } + gViewerWindow->getWindow()->drainOSMainQueue(); } + if (!gHeadlessClient && gViewerWindow) { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df pauseMainloopTimeout"); - pingMainloopTimeout("Main:Sleep"); - - pauseMainloopTimeout(); - } - - // 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); - } + LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df Display"); + gGLActive = true; - 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("df gMeshRepo"); - gMeshRepo.update() ; - } - - 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(); - } + // 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(); - { - LL_PROFILE_ZONE_NAMED_CATEGORY_APP("df resumeMainloopTimeout"); - resumeMainloopTimeout(); - } - pingMainloopTimeout("Main:End"); + gGLActive = false; } } - - if (LLApp::isExiting()) + else { - pingMainloopTimeout("Main:qSnapshot"); - // Save snapshot for next time, if we made it through initialization - if (STATE_STARTED == LLStartUp::getStartupState()) - { - saveFinalSnapshot(); - } - - pingMainloopTimeout("Main:TerminateVoice"); - if (LLVoiceClient::instanceExists()) + // 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) { - LLVoiceClient::getInstance()->terminate(); + viewerThreadShutdown(); } - - 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::getDisplayRefreshRate() const +{ + // Platform-detected rate of the display the window is on. + if (gViewerWindow && gViewerWindow->getWindow()) + { + return gViewerWindow->getWindow()->getRefreshRate(); + } + return 0; +} + +void LLAppViewer::setVSyncMode(U32 mode) +{ + mode = llclamp(mode, 0u, 4u); + + // 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) + { + // 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) + { + LLPerfStats::vsync_max_fps = (U32)(refresh / mode); + } + } +} + S32 LLAppViewer::updateTextureThreads(F32 max_time) { size_t work_pending = 0; @@ -1701,6 +2063,27 @@ 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) + { + // 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(); + + // 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; @@ -1709,16 +2092,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 @@ -2034,6 +2410,12 @@ bool LLAppViewer::cleanup() LL_INFOS() << "Shutting down OpenGL" << LL_ENDL; + // 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. + releaseRenderTargets(); + mCompositor.release(); + // Shut down OpenGL if (gViewerWindow) { @@ -2124,8 +2506,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()) @@ -2311,7 +2691,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 @@ -3369,6 +3749,28 @@ bool LLAppViewer::initWindow() stop_glerror(); gViewerWindow->initGLDefaults(); + // 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); + + // 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. + 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 ); @@ -5141,6 +5543,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 6ecafae0363..a6194dad2c3 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -49,8 +49,15 @@ #include "llsys.h" // for LLOSInfo #include "lltimer.h" #include "llappcorehttp.h" +#include "llcompositor.h" +#include "llcompositable.h" +#include "llrendertarget.h" +#include "llthreadsafequeue.h" #include "threadpool_fwd.h" +#include +#include + #include class LLCommandLineParser; @@ -83,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(); @@ -100,9 +109,9 @@ class LLAppViewer : public LLApp // // 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 @@ -149,6 +158,78 @@ class LLAppViewer : public LLApp std::string getSecondLifeTitle() const; // The Second Life title. std::string getWindowTitle() const; // The window display name. + // 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); + + // 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. Viewer thread only. + void setBackBufferRendered() + { + mBackBufferRendered = true; + } + + // 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. // sendSimpleLogoutRequest does not create a marker file. @@ -302,7 +383,7 @@ class LLAppViewer : public LLApp virtual bool meetsRequirementsForMaximizedStart(); // Used on first login to decide to launch maximized - virtual void sendOutOfDiskSpaceNotification(); + virtual void sendOutOfDiskSpaceNotification() override; protected: @@ -373,6 +454,27 @@ class LLAppViewer : public LLApp S32 mNumSessions; std::string mSerialNumber; + 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 + 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 + + // 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() + // 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 6c151351ff9..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(); ) { @@ -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/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/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/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 7498c2d524f..3888ac39fea 100644 --- a/indra/newview/llscenemonitor.cpp +++ b/indra/newview/llscenemonitor.cpp @@ -311,16 +311,23 @@ 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 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 - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBindFramebuffer(GL_FRAMEBUFFER, old_FBO); + // 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/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/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/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 0c93b247510..a75fa56c317 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; } @@ -483,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) { @@ -855,7 +840,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); @@ -863,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 9dfa9a0efd2..9ccbe78062f 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; @@ -168,6 +168,21 @@ void display_startup() LLGLState::checkStates(); + // 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 { LLViewerDynamicTexture::updateAllInstances(); @@ -193,8 +208,13 @@ void display_startup() LLGLState::checkStates(); - if (gViewerWindow && gViewerWindow->getWindow()) - gViewerWindow->getWindow()->swapBuffers(); + if (LLRenderTarget::getCurrentBoundTarget() == &startup_rt) + { + startup_rt.flush(); + } + // Just set the flag; the actual publish happens at the end of + // renderViewerFrame. + LLAppViewer::instance()->setBackBufferRendered(); glClear(GL_DEPTH_BUFFER_BIT); } @@ -417,13 +437,14 @@ 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(); + // 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; @@ -695,6 +716,32 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) // Actually push all of our triangles to the screen. // + // 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()) + { + 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. 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) + { + LLAppViewer::instance()->getBackBuffer().bindTarget(); + } + else + { + llassert(gSnapshot); + } + // do render-to-texture stuff here if (gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_DYNAMIC_TEXTURES)) { @@ -772,7 +819,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) @@ -1554,14 +1601,33 @@ 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"); - if (gDisplaySwapBuffers) + + // 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(); + + 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 { - gViewerWindow->getWindow()->swapBuffers(); + // A snapshot's scratch target is the bottom of the stack here. + // Outside a snapshot this means an upstream flush was missed. + llassert(gSnapshot); } - gDisplaySwapBuffers = true; } 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..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" @@ -81,6 +83,7 @@ LLGLSLShader gOcclusionProgram; LLGLSLShader gSkinnedOcclusionProgram; LLGLSLShader gOcclusionCubeProgram; LLGLSLShader gGlowCombineProgram; +LLGLSLShader gCompositorBlitProgram; LLGLSLShader gReflectionMipProgram; LLGLSLShader gGaussianProgram; LLGLSLShader gRadianceGenProgram; @@ -3228,6 +3231,35 @@ bool LLViewerShaderMgr::loadShadersInterface() success = gPathfindingNoNormalsProgram.createShader(); } + 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) { 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 0f23596c9a8..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,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 && + // 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() )) { if (mRawImage.isNull()) diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7dd32074cfe..d12d02118ac 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 @@ -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,6 +1175,18 @@ 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 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) + { + world_rt.bindTarget(); + world_bound = true; + } + // just in case we downres textures, bind downresmap and copy program gPipeline.mDownResMap.bindTarget(); gCopyProgram.bind(); @@ -1211,6 +1223,12 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) gCopyProgram.unbind(); gPipeline.mDownResMap.flush(); + + if (world_bound && + LLRenderTarget::getCurrentBoundTarget() == &world_rt) + { + world_rt.flush(); + } } return create_timer.getElapsedTimeF32(); diff --git a/indra/newview/llviewerthread.cpp b/indra/newview/llviewerthread.cpp new file mode 100644 index 00000000000..0d7bd2898bb --- /dev/null +++ b/indra/newview/llviewerthread.cpp @@ -0,0 +1,134 @@ +/** + * @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" + +#if LL_DARWIN +#include "llwindowmacosx-objc.h" +#endif + +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(); + 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() +{ + // 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()) + { +#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." + << 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. +#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(); + + // 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..ec5befa218a --- /dev/null +++ b/indra/newview/llviewerthread.h @@ -0,0 +1,63 @@ +/** + * @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, 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. 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: + 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 ce8aed8d6f0..61f521c678e 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) @@ -1978,7 +2027,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, @@ -2599,6 +2648,15 @@ void LLViewerWindow::reshape(S32 width, S32 height) mWindowRectRaw.mRight = mWindowRectRaw.mLeft + width; mWindowRectRaw.mTop = mWindowRectRaw.mBottom + 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 ); LLViewerCamera * camera = LLViewerCamera::getInstance(); // simpleton, might not exist @@ -5304,6 +5362,19 @@ bool LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei swap(); } + // 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++) { 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: + + + + + + + +