From ded106da6a2730fcd65779b2a7beb484146fb295 Mon Sep 17 00:00:00 2001 From: Bernard Laberge Date: Thu, 30 Jul 2026 16:56:14 -0400 Subject: [PATCH] perf: SG-43585: Performance improvement of pyevaluate and pyexec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the Qt5->Qt6 migration, JS<->Python web-panel bridge round-trips (pyevaluate / pyexec) became slow and grew unbounded whenever the handler did RV work such as clearSession() / addSourceVerbose(). The bridge is synchronous on the Qt main (GUI) thread, so any redundant work RV does on that thread directly delays delivery of queued bridge calls. Three sources of avoidable GUI-thread work are addressed: - Redundant edit-mode churn. clearSession() sets the default view twice with force=true; each before/after view-change event toggled the same view edit mode off and back on (up to 4x per clear), rebuilding menus and event tables for no net change. - Settle-window repaint storm. A static askForRedraw() started a 2-second window that kept isUpdating() true, so the ~120 Hz heartbeat kept repainting for ~2 s after the single needed frame had been drawn. In Qt5 each QGLWidget swapped its own back buffer cheaply; in Qt6 every redraw forces a full-window QOpenGLWidget recomposite + present. - Per-presented-frame full-window composite. The viewport now renders as a native QOpenGLWindow (new GLWindow) embedded via QWidget::createWindowContainer instead of a QOpenGLWidget, so the top-level QMainWindow is no longer forced onto the OpenGL RHI backend and the viewport presents on its own surface. Docked QWebEngineView panels render on the platform-default backend and QT_QUICK_BACKEND= software is no longer needed. GLView becomes a plain QWidget host delegating to GLWindow; QTGLVideoDevice gains a window-backed path. Also fixed, exposed by the surface change: - DesktopVideoDevice::open no longer dereferences the control device's (now-null) QOpenGLWidget; ScreenView shares a QOpenGLContext via new backing-agnostic QTGLVideoDevice::glShareContext()/glSurfaceFormat(). - Qt::AA_ShareOpenGLContexts is set (documented QtWebEngine requirement) so second-output and upload-worker GL surfaces share with the viewport, and a null-context dereference in newSharedContextWorkerDevice that crashed with Multithread GPU Upload enabled is removed. - The UI blocking overlay is reworked from a raster child widget into a frameless translucent top-level window so it can dim and block input over the native viewport. - crashpad's MSVC toolset is pinned to the parent build (fixes LNK2019). - Session initialization is guarded on a current GL context: constructing an RvSession queries GL_SHADING_LANGUAGE_VERSION, which aborts with no current context. A QOpenGLWindow has no context until first exposed, so the constructor's retry now runs only when GLWindow::initializeGL() already bailed out early (Windows), leaving macOS to initialize from initializeGL() itself. Without this RV aborted at launch on macOS. Validated on Windows (release): flat bridge latency under playback spam, all web panels render docked, and viewport/events/drag-drop/annotations/ presentation/blocking-overlay all work. Validated on macOS: launch and session init/teardown. macOS feature and HDPI passes still pending. Co-authored-by: Cédrik Fuoco Signed-off-by: Cédrik Fuoco Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Bernard Laberge fix: SG-43585: initialize the session outside the GL callback RvDocument::initializeSession() ran from GLWindow::initializeGL(), so creating the RvSession -- and with it loading every package, which runs Mu and Python -- depended on when Qt happened to deliver the viewport window's first expose. That is not something the platforms agree on, and it broke two of them differently. Linux (X11): QWidget::show() is asynchronous, so no expose had arrived by the time RvApplication::newSessionFromFiles() reached doc->session() a few lines later. That returned null and RV segfaulted at startup in Session::queryAndStoreGLInfo() with this == nullptr, before any web panel or script was involved. Confirmed under gdb, and by the crash simply moving to the next use of the null session (rebuildSessionFromFiles) when that call was skipped. macOS: a QOpenGLWindow has no GL context until it is first exposed, so the session was constructed with no current context and RV aborted in IPCore::Shader::initGLSLVersion() when glGetString(GL_SHADING_LANGUAGE_VERSION) returned null. Windows appeared to escape both because initializeGL() fires synchronously while the window is being constructed there. Have the application drive it instead: newSessionFromFiles() calls initializeSession() immediately after show(), and initializeSession() makes the viewport context current itself before constructing the RvSession. There is a single RvDocument creation site, so this is the whole ordering. initializeGL() no longer calls back into the document, which also retires the m_sessionInitPending retry that worked around the same fragility from the other end. Validated on Linux: launches cleanly, and a docked QWebEngineView works. On macOS: launches cleanly and media loads. Not yet exercised on Windows, where this changes startup ordering as well. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Bernard Laberge docs: SG-43585: record why macOS keeps the software Qt Quick backend The QT_QUICK_BACKEND comments no longer described reality. Both files still explained the setting as a Qt 5.12.1 workaround for QWebEngineView conflicting with the QGLWidget viewport, which has not been the reason since the viewport became a native QOpenGLWindow. src/bin/apps/rv/main.cpp (Windows, Linux): drop the block outright. The call was already commented out, so all that remained was a commented-out line plus a rationale for a decision the code no longer makes. Linux has since confirmed the setting is not needed there -- it launches and runs docked web panels without it. src/bin/nsapps/RV/main.cpp (macOS): the setting stays, and the comment now says why, because it is load bearing rather than vestigial. The viewport window is embedded with QWidget::createWindowContainer, making it a QObject child of the top level window's QWidgetWindow. With the hardware Qt Quick backend, adding a QWebEngineView makes Qt destroy and recreate that native subtree; the viewport window is deleted with it rather than reparented, and QWindowContainer then dereferences the window it just lost, so RV segfaults inside Qt. Measured on macOS with the four line reproducer now quoted in the comment: software backend survives 2/2, hardware backend crashes 2/2. The cost is that macOS web panels composite in software, so macOS gets the cheap viewport repaint half of this work but not the hardware web panel half. Noted in the comment so the asymmetry with Windows and Linux reads as deliberate. Comment-only; no compiled behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Bernard Laberge --- cmake/dependencies/crashpad.cmake | 10 + .../rv-reference-manual-chapter-four.md | 4 +- src/bin/apps/rv/main.cpp | 13 +- src/bin/nsapps/RV/main.cpp | 31 +- src/lib/app/RvCommon/CMakeLists.txt | 1 + src/lib/app/RvCommon/DesktopVideoDevice.cpp | 12 +- src/lib/app/RvCommon/GLView.cpp | 811 ++---------------- src/lib/app/RvCommon/GLWindow.cpp | 372 ++++++++ src/lib/app/RvCommon/QTGLVideoDevice.cpp | 110 ++- src/lib/app/RvCommon/RvApplication.cpp | 10 + .../RvCommon/RvCommon/DesktopVideoDevice.h | 11 +- src/lib/app/RvCommon/RvCommon/GLView.h | 94 +- src/lib/app/RvCommon/RvCommon/GLWindow.h | 102 +++ .../app/RvCommon/RvCommon/QTGLVideoDevice.h | 24 + src/lib/app/RvCommon/RvCommon/RvDocument.h | 6 + src/lib/app/RvCommon/RvDocument.cpp | 68 +- src/lib/app/mu_rvui/mode_manager.mu | 36 +- src/lib/app/py_rvui/rv/qtutils.py | 8 +- src/lib/ip/IPCore/Session.cpp | 37 +- 19 files changed, 890 insertions(+), 870 deletions(-) create mode 100644 src/lib/app/RvCommon/GLWindow.cpp create mode 100644 src/lib/app/RvCommon/RvCommon/GLWindow.h diff --git a/cmake/dependencies/crashpad.cmake b/cmake/dependencies/crashpad.cmake index e4b3992a8..83d14e2ca 100644 --- a/cmake/dependencies/crashpad.cmake +++ b/cmake/dependencies/crashpad.cmake @@ -72,6 +72,16 @@ SET(_crashpad_handler LIST(APPEND _configure_options "-DCMAKE_POSITION_INDEPENDENT_CODE=ON") LIST(APPEND _configure_options "-DCRASHPAD_BUILD_TESTS=OFF") +# Pin the MSVC toolset to match the parent build. Without this, cmake auto-selects the newest installed toolset (e.g. 14.44) while the main build uses a pinned +# version (e.g. 14.40). The mismatch causes LNK2019 for __std_find_end_1 and similar CRT symbols added only in newer toolsets. Only applies to Visual Studio +# generators; the crashpad build dir must be clean. +IF(CMAKE_GENERATOR MATCHES "Visual Studio" + AND CMAKE_GENERATOR_TOOLSET +) + LIST(APPEND _configure_options "-T") + LIST(APPEND _configure_options "${CMAKE_GENERATOR_TOOLSET}") +ENDIF() + # Disable deprecation warnings on macOS — mini_chromium uses deprecated Security APIs IF(RV_TARGET_DARWIN) LIST(APPEND _configure_options "-DCMAKE_CXX_FLAGS=-Wno-error=deprecated-declarations -Wno-deprecated-declarations") diff --git a/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-four.md b/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-four.md index 72cdfebe5..7680c069d 100644 --- a/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-four.md +++ b/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-four.md @@ -236,7 +236,9 @@ import rv.qtutils # Gets the current RV session windows as a PySide QMainWindow. rvSessionWindow = rv.qtutils.sessionWindow() -# Gets the current RV session GL view as a PySide QGLWidget. +# Gets the current RV session view host as a PySide QWidget. Note: the GL +# viewport is now a native child window embedded in this host (via +# createWindowContainer), so this is a plain QWidget, not a QOpenGLWidget. rvSessionGLView = rv.qtutils.sessionGLView() # Gets the current RV session top tool bar as a PySide QToolBar. diff --git a/src/bin/apps/rv/main.cpp b/src/bin/apps/rv/main.cpp index 32b10b467..448310496 100644 --- a/src/bin/apps/rv/main.cpp +++ b/src/bin/apps/rv/main.cpp @@ -353,11 +353,6 @@ int utf8Main(int argc, char* argv[]) TwkFB::ThreadPool::initialize(); - // Qt 5.12.1 specific - // Disable Qt Quick hardware rendering because QwebEngineView conflicts with - // QGLWidget - setEnvVar("QT_QUICK_BACKEND", "software"); - #if defined(PLATFORM_LINUX) // Work around for Wacom Tablet issue on linux // Note: This is a Qt 5.12.4 regression @@ -369,6 +364,14 @@ int utf8Main(int argc, char* argv[]) // (RV Preferences/Rendering/Multithread GPU Upload) QApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + // Share GL resources across every QOpenGLContext in the process. This is a + // documented QtWebEngine requirement, and it lets RV's auxiliary GL + // surfaces -- the second-output ScreenView and the multithreaded-upload + // worker device -- share textures/FBOs with the main viewport context + // without an explicit, ordering-sensitive setShareContext() call. Must be + // set before the QApplication is constructed. + QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + TwkUtil::MemPool::initialize(); string altPrefsPath; diff --git a/src/bin/nsapps/RV/main.cpp b/src/bin/nsapps/RV/main.cpp index 9ac24f5b9..c276951cc 100644 --- a/src/bin/nsapps/RV/main.cpp +++ b/src/bin/nsapps/RV/main.cpp @@ -284,9 +284,34 @@ int main(int argc, char* argv[]) setPlatformSpecificLocale(); - // Qt 5.12.1 specific - // Disable Qt Quick hardware rendering because QwebEngineView conflicts with - // QGLWidget + // + // Keep Qt Quick on the software backend. Do not remove this on macOS. + // + // It was originally added because QWebEngineView conflicted with the + // QGLWidget viewport, and src/bin/apps/rv/main.cpp has since dropped it for + // Windows and Linux -- but on macOS it is now load bearing for a different + // reason. The viewport is a native QOpenGLWindow embedded via + // QWidget::createWindowContainer, which makes it a QObject child of the top + // level window's QWidgetWindow. With the hardware Qt Quick backend, adding a + // QWebEngineView to the widget tree makes Qt destroy and recreate that + // native subtree; the viewport window is deleted with it rather than + // reparented, and QWindowContainer then dereferences the window it just + // lost. RV segfaults inside Qt, before it can react. + // + // Reproducible in four lines from any docked panel -- with the software + // backend this survives, with the hardware backend it crashes: + // + // w = rv.qtutils.sessionWindow() + // d = QtWidgets.QDockWidget('t', w) + // v = QtWebEngineWidgets.QWebEngineView(d) + // d.setWidget(v); w.addDockWidget(QtCore.Qt.RightDockWidgetArea, d) + // v.setHtml('hi') + // + // The cost of keeping it is that web panels composite in software on macOS, + // so macOS gets the cheap-viewport-repaint half of SG-43585 but not the + // hardware web panel half. Removing it needs the viewport to stop being + // owned by the top level window's native subtree, not just a comment change. + // setenv("QT_QUICK_BACKEND", "software", 0 /* changeFlag : Do not change the existing value */); // Prevent usage of native sibling widgets on Mac. This attribute can be diff --git a/src/lib/app/RvCommon/CMakeLists.txt b/src/lib/app/RvCommon/CMakeLists.txt index 64e324549..2c525727f 100644 --- a/src/lib/app/RvCommon/CMakeLists.txt +++ b/src/lib/app/RvCommon/CMakeLists.txt @@ -48,6 +48,7 @@ SET(_sources RvApplication.cpp DiagnosticsView.cpp GLView.cpp + GLWindow.cpp TwkQTAction.cpp MediaDirModel.cpp RvNetworkDialog.cpp diff --git a/src/lib/app/RvCommon/DesktopVideoDevice.cpp b/src/lib/app/RvCommon/DesktopVideoDevice.cpp index 2e2740c5c..3b159e710 100644 --- a/src/lib/app/RvCommon/DesktopVideoDevice.cpp +++ b/src/lib/app/RvCommon/DesktopVideoDevice.cpp @@ -157,10 +157,10 @@ namespace Rv { TWK_GLDEBUG; - QSurfaceFormat fmt = shareDevice()->widget()->format(); + QSurfaceFormat fmt = shareDevice()->glSurfaceFormat(); fmt.setSwapInterval(m_vsync ? 1 : 0); - ScreenView* vw = new ScreenView(fmt, 0, shareDevice()->widget(), Qt::Window); + ScreenView* vw = new ScreenView(fmt, 0, shareDevice()->glShareContext(), Qt::Window); setViewWidget(vw); QTGLVideoDevice* vd = new QTGLVideoDevice(0, "local view", vw); @@ -747,11 +747,11 @@ namespace Rv void DesktopVideoDevice::sortVideoFormatsByWidth() { sort(m_videoFormats.begin(), m_videoFormats.end(), widthSort); } - DesktopVideoDevice::ScreenView::ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLWidget* glViewShare, + DesktopVideoDevice::ScreenView::ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLContext* glShareContext, Qt::WindowFlags flags) : QOpenGLWidget(parent, flags) { - m_glViewShare = glViewShare; + m_glShareContext = glShareContext; setFormat(fmt); // Important: set PartialUpdate, because otherwise @@ -764,9 +764,9 @@ namespace Rv { QOpenGLWidget::initializeGL(); - if (m_glViewShare && context() && context()->isValid()) + if (m_glShareContext && context() && context()->isValid()) { - context()->setShareContext(m_glViewShare->context()); + context()->setShareContext(m_glShareContext); } } diff --git a/src/lib/app/RvCommon/GLView.cpp b/src/lib/app/RvCommon/GLView.cpp index 617f29581..dc9aec0d4 100644 --- a/src/lib/app/RvCommon/GLView.cpp +++ b/src/lib/app/RvCommon/GLView.cpp @@ -13,189 +13,106 @@ #endif #include +#include #include -#include -#include #include #include +#include +#include +#include #include -#include -#include -#include -#include - -#include +#include namespace Rv { - using namespace boost; using namespace std; - using namespace TwkApp; - using namespace IPCore; - - namespace - { - - class SyncBufferThreadData - { - public: - explicit SyncBufferThreadData(const VideoDevice* device) - : m_device(device) - , m_done(false) - , m_doSync(false) - , m_running(false) - , m_mutex() - , m_cond() - { - } - - void run() - { - while (!m_done) - { - boost::mutex::scoped_lock lock(m_mutex); - m_running = true; - m_doSync = false; - - m_cond.wait(lock); - - if (m_device && m_doSync && !m_done) - m_device->syncBuffers(); - - m_doSync = false; - } - } - - void notify(bool finish = false) - { - { - boost::mutex::scoped_lock lock(m_mutex); - if (finish) - m_done = true; - else - m_doSync = true; - if (!m_running) - return; - } - - m_cond.notify_one(); - } - - const VideoDevice* device() const { return m_device; } - - void setDevice(const VideoDevice* d) { m_device = d; } - - private: - SyncBufferThreadData(SyncBufferThreadData&) {} - - void operator=(SyncBufferThreadData&) {} - - private: - bool m_done; - bool m_doSync; - bool m_running; - boost::mutex m_mutex; - boost::condition_variable m_cond; - const VideoDevice* m_device; - }; - - class ThreadTrampoline - { - public: - ThreadTrampoline(GLView* view) - : m_view(view) - { - } - - void operator()() - { - SyncBufferThreadData* closure = reinterpret_cast(m_view->syncClosure()); - closure->run(); - } - - private: - GLView* m_view; - }; - - } // namespace GLView::GLView(QWidget* parent, QOpenGLContext* sharedContext, RvDocument* doc, bool stereo, bool vsync, bool doubleBuffer, int red, int green, int blue, int alpha, bool noResize) - : QOpenGLWidget(parent) - , m_sharedContext(sharedContext) + : QWidget(parent) , m_doc(doc) - , m_red(red) - , m_green(green) - , m_blue(blue) - , m_alpha(alpha) - , m_lastKey(0) - , m_lastKeyType(QEvent::None) - , m_userActive(true) - , m_renderCount(0) - , m_firstPaintCompleted(false) + , m_container(0) + , m_videoDevice(0) , m_csize(1024, 576) , m_msize(128, 128) - , m_postFirstNonEmptyRender(noResize) - , m_stopProcessingEvents(false) - , m_syncThreadData(0) + , m_sharedContext(sharedContext) { - setFormat(rvGLFormat(stereo, vsync, doubleBuffer, red, green, blue, alpha)); + // + // Native GL viewport window (renders + presents on its own surface). + // + m_glWindow = new GLWindow(sharedContext, doc, stereo, vsync, doubleBuffer, red, green, blue, alpha, noResize); + + // + // Embed the native window in the widget tree. The container is a + // normal QWidget; there is no QOpenGLWidget in the window, so the + // top-level window is not forced onto the OpenGL RHI backend. + // + m_container = QWidget::createWindowContainer(m_glWindow, this); + m_container->setFocusPolicy(Qt::StrongFocus); + + // + // Create the native platform surface up-front. Unlike a QOpenGLWidget + // (which renders offscreen to an FBO), a QOpenGLWindow has no GL + // context until its window surface exists; RV performs GL setup + // (makeCurrent + capability queries) during startup before the window + // is shown, so the surface must exist by then. + // + m_glWindow->create(); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_container); + + // + // The device drives the GL surface (the window) for rendering, and + // uses the container QWidget for event / coordinate translation + // (height-based y-flip, mapToGlobal, mouse grab). + // ostringstream str; str << UI_APPLICATION_NAME " Main Window" << "/" << m_doc; - m_videoDevice = new QTGLVideoDevice(0, str.str(), this); + m_videoDevice = new QTGLVideoDevice(0, str.str(), m_glWindow, m_container); + m_glWindow->setVideoDevice(m_videoDevice); setObjectName((m_doc->session()) ? m_doc->session()->name().c_str() : "no session"); + setFocusProxy(m_container); + } - m_activityTimer.start(); - setMouseTracking(true); - setAcceptDrops(true); - setFocusPolicy(Qt::StrongFocus); + GLView::~GLView() { delete m_videoDevice; } - m_eventProcessingTimer.setSingleShot(true); - connect(&m_eventProcessingTimer, SIGNAL(timeout()), this, SLOT(eventProcessingTimeout())); - } + QOpenGLContext* GLView::context() const { return m_glWindow->context(); } - GLView::~GLView() + QSurfaceFormat GLView::format() const { return m_glWindow->format(); } + + void GLView::makeCurrent() { - // delete m_frameBuffer; - delete m_videoDevice; + // Route through the device, which guards on context validity. Unlike a + // QOpenGLWidget (which can render to its FBO before being shown), a + // QOpenGLWindow has no GL context until it is created/exposed, so a + // direct makeCurrent() here can deref a null context during startup. + if (m_videoDevice) + m_videoDevice->makeCurrent(); + } - if (m_syncThreadData) - { - SyncBufferThreadData* closure = reinterpret_cast(m_syncThreadData); - closure->notify(true); - m_swapThread.join(); + bool GLView::isValid() const { return m_glWindow != nullptr; } - delete closure; - } - } + void GLView::absolutePosition(int& x, int& y) const { m_glWindow->absolutePosition(x, y); } + + void GLView::stopProcessingEvents() { m_glWindow->stopProcessingEvents(); } - void GLView::stopProcessingEvents() { m_stopProcessingEvents = true; } + bool GLView::firstPaintCompleted() const { return m_glWindow->firstPaintCompleted(); } QSize GLView::sizeHint() const { return m_csize; } QSize GLView::minimumSizeHint() const { return m_msize; } - void GLView::absolutePosition(int& x, int& y) const - { - x = 0; - y = 0; - - QPoint p(0, 0); - QPoint gp = mapToGlobal(p); + QImage GLView::readPixels(int x, int y, int w, int h) { return m_glWindow->readPixels(x, y, w, h); } - x = gp.x(); - y = gp.y(); - } + float GLView::devicePixelRatio() const { return videoDevice() ? videoDevice()->devicePixelRatio() : 1.0f; } QSurfaceFormat GLView::rvGLFormat(bool stereo, bool vsync, bool doubleBuffer, int red, int green, int blue, int alpha) { - const Rv::Options& opts = Rv::Options::sharedOptions(); - // NOTE_QT6: QGLFormat into QSurfaceFormat - // NOTE_QT6: setStencil, setDepth does not exist anymore. Trying to use - // setDepthBufferSize and setStencilBufferSize. QSurfaceFormat fmt; fmt.setDepthBufferSize(24); fmt.setSwapBehavior(doubleBuffer ? QSurfaceFormat::DoubleBuffer : QSurfaceFormat::SingleBuffer); @@ -208,9 +125,6 @@ namespace Rv fmt.setMajorVersion(2); fmt.setMinorVersion(1); - // fmt.setProfile(QSurfaceFormat::CoreProfile); - // fmt.setProfile(QSurfaceFormat::CompatibilityProfile); - // // The default value for these buffer sizes is -1, but it is // illegal to set to that value so test for positive red, not @@ -232,603 +146,4 @@ namespace Rv return fmt; } - void GLView::initializeGL() - { - // - // At this point the format is known. Can't do this in the constructor - // - - // QUESTION_QT6: Should we use isValid from QOpenGLWidget or directly - // using from QOpenGLContext? NOTE_QT6: Returns true if the widget and - // OpenGL resources, like the context, have been successfully - // initialized. - // Note that the return value is always false until the widget - // is shown. - // NOTE_QT6: QOpenGLContext: Returns if this context is valid, i.e. has - // been successfully created. - if (context()->isValid()) - { - initializeGLExtensions(); - initializeOpenGLFunctions(); - - if (m_sharedContext) - { - context()->setShareContext(m_sharedContext); - } - - if (m_doc) - { - m_doc->initializeSession(); - } - - // NOTE_QT6: QGLFormat is deprecated. Using QSurfaceFormat now. - QSurfaceFormat f = context()->format(); - -#ifndef PLATFORM_DARWIN - // - // Doesn't work on OS X - // - if (f.redBufferSize() != m_red && m_red != 0) - { - // QMessageBox box(this); - // box.setWindowTitle(tr("Ouput Display Format")); - - ostringstream str; - - str << "WARNING: asked for" - << " " << m_red << " " << m_green << " " << m_blue << " " << m_alpha << " RGBA color but got" - << " " << f.redBufferSize() << " " << f.greenBufferSize() << " " << f.blueBufferSize() << " " - << (f.alphaBufferSize() <= 0 ? 0 : f.alphaBufferSize()) << " RGBA instead"; - - cout << str.str() << endl; - - // box.setText(str.str().c_str()); - // box.setDetailedText("You can change the default display color - // depth and target " - // "from the preferences under - // Rendering->Display Output Format.\n" - // "Choosing \"OpenGL Default Format\" will - // tell RV to ask for the " "default - // prefered format for this display."); - // box.setWindowModality(Qt::WindowModal); - // QPushButton* b1 = box.addButton(tr("Continue"), - // QMessageBox::AcceptRole); box.setIcon(QMessageBox::Critical); - // box.exec(); - } -#endif - if (f.stencilBufferSize() == 0) - { - cout << "WARNING: no stencil buffer available" << endl; - } - } - else - { - cout << "WARNING: invalid GL context" << endl; - } - } - - void GLView::resizeGL(int w, int h) - { - if (m_doc) - m_doc->viewSizeChanged(w, h); -#ifdef PLATFORM_WINDOWS - SetWindowRgn(reinterpret_cast(this->winId()), 0, false); -#endif - } - - bool GLView::validateReadPixels(int x, int y, int w, int h) - { - int r = x + w; - int t = y + h; - - // are the extents of the read region out of bounds? - if (x < 0 || y < 0 || r > width() * devicePixelRatio() || t > height() * devicePixelRatio()) - return false; - - return true; - } - - QImage GLView::readPixels(int x, int y, int w, int h) - { - // If out of bounds, return an empty image. - if (validateReadPixels(x, y, w, h) == false) - { - QImage image(0, 0, QImage::Format_RGBA8888); - return image; - } - - makeCurrent(); - - QImage image(w, h, QImage::Format_RGBA8888); - glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.bits()); - - return image; - } - - void GLView::debugSaveFramebuffer() - { - // Create a QImage with the same size as the FBO - QImage image(width(), height(), QImage::Format_RGBA8888); - glReadPixels(0, 0, width(), height(), GL_RGBA, GL_UNSIGNED_BYTE, image.bits()); - - // image.save("/home/>//fbo.png"); - } - - void GLView::paintGL() - { - TWK_GLDEBUG; - - IPCore::Session* session = m_doc->session(); - bool debug = IPCore::debugProfile && session; - - if (!m_postFirstNonEmptyRender && session && session->postFirstNonEmptyRender()) - { - m_postFirstNonEmptyRender = true; - - if (!session->isFullScreen()) - { - m_doc->resizeToFit(false, false); - m_doc->center(); - TWK_GLDEBUG; - } - } - - if (debug) - { - Session::ProfilingRecord& trecord = session->beginProfilingSample(); - trecord.renderStart = session->profilingElapsedTime(); - } - - // should be no longer necessary, moved it to resize() -#if 0 - // - // This is necessary to stop the Windows Desktop Window Manager - // (new in Vista/win7) fromc caching portions of rv's glview - // and holding them in the display even when rv redraws. the - // effect being that parts of previous displays will be "left - // behind" and not updated even when rv plays. especially when - // going to/from fullscreen. - // -#ifdef PLATFORM_WINDOWS - SetWindowRgn (this->winId(), 0, false); -#endif -#endif - - if (m_doc && session && m_videoDevice) - { - // m_frameBuffer->makeCurrent(); - TWK_GLDEBUG; - m_videoDevice->makeCurrent(); - TWK_GLDEBUG; - - if (m_userActive && m_activityTimer.elapsed() > 1.0) - { - if (m_doc->mainPopup() && !m_doc->mainPopup()->isVisible() && hasFocus()) - { - TwkApp::ActivityChangeEvent aevent("user-inactive", m_videoDevice); - m_videoDevice->sendEvent(aevent); - TWK_GLDEBUG; - m_userActive = false; - } - } - - // - // Make sure the video device knows where it is on screen. - // - int x = 0, y = 0; - absolutePosition(x, y); - m_videoDevice->setAbsolutePosition(x, y); - - TWK_GLDEBUG; - session->render(); - TWK_GLDEBUG; - - m_firstPaintCompleted = true; - - // Starting with Qt 5.12.1, the resulting texture is later - // composited over white which creates undesirable artifacts. As a - // work around, we set the resulting alpha channel to 1 to make sure - // that the resulting texture is fully opaque. - - // NOTE_QT: That code seems to fix an issue on MacOS only. (Not on - // Linux, not test on Windows) - // The issue I see on MacOS that the background is brown - // istead of black with the default background. - // - // Even on Qt6/QOpenGLWidget, we need to call - // glBindFramebuffer(); it otherwise complains and fails - // on glClear() on macOS - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, QOpenGLContext::currentContext()->defaultFramebufferObject()); - TWK_GLDEBUG; - - glPushAttrib(GL_COLOR_BUFFER_BIT); - TWK_GLDEBUG; - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); - TWK_GLDEBUG; - glClearColor(0.f, 0.f, 0.f, 1.0f); - TWK_GLDEBUG; - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - TWK_GLDEBUG; - glPopAttrib(); - TWK_GLDEBUG; - } - else - { - glClearColor(0.f, 0.f, 0.f, 1.0f); - TWK_GLDEBUG; - glClear(GL_COLOR_BUFFER_BIT); - TWK_GLDEBUG; - } - - if (m_stopProcessingEvents) - return; - - // - // We're done with drawing and about to (possibly) wait for - // vsync before the buffer swaps, but if the driver is layzy, - // it may not performs some requested gl actions until after - // the wait on vsync, in which case we get tearing. - // - // A glFlush here should make these gl actions happen during - // the wait. - // - // This could be a problem if further drawing is done to this - // buffer by Qt, for example if it's doing it's graphics area - // widget drawing. Seems fine for now. - // - // Update: We want to actually wait until the gfx operations are - // complete. glFlush just flushes the command buffer to - // hardware, glFinish blocks until the hardware is finished - // processing the commands. - // - - // DONT - // glFlush(); - // glFinish(); - - // - // Do the swap, the vsync code is in the swapBuffers() code in - // Qt. It should block here waiting for the refresh. The debug - // code here is accumulating profiling information in the session - // ProfilingRecord struct. This can be dumped on exit to figure out - // what's going on. - // - - // Note for Qt6 . QOpenGLWidget implementation: - // - // With the new QOpenGLWidget (Qt6 branch), all of the rendering of - // each widget is done in its own FBOs (aka: off-screen buffers), as - // opposed to the old Qt5/QGLWidget branch where the rendering was - // done in each GGLWidget's backbuffer (aka: GL_BACK, an on-screen - // framebuffer surface). - // - // With QOpenGLWidget, it is now the WainWindow's responsibility - // (more technically, the MainWindow's rendering backend, which - // happens to be Qt's OpenGL rendering backend when QOpenGLWidget are - // present in the children tree) to gather and composite all of the - // off-screen buffers (regardless of which image type they are, eg: - // cpu-memory images, gpu/opengl images, etc) and finally to call - // swapBuffers to show the final contents of the mainWindow. - // - // As a result, I'm not sure there's a point -- at all -- - // in calling swapBuffers on the GLView anywhere amnymore. From old - // comments in the previous version of this tile, it appears that - // calling swapBuffers was done to force a quicker visual update, or - // to minimize visual tearing of some sort. This would have worked - // with the old QGLWidget (becayuse each QGLWidget had its own - // context, and its rendering target was directly the GL_BACK - // framebuffer, but, again, with QOpenGLWidget, the rendering target - // is no longer GL_BACK, it is an FBO that is meant to be used at - // the end of the application's drawing / visual update pipeline. - - if (debug) - { - Session::ProfilingRecord& trecord = session->currentProfilingSample(); - trecord.renderEnd = session->profilingElapsedTime(); - trecord.swapStart = trecord.renderEnd; - - if (session->outputVideoDevice() != videoDevice()) - { - session->outputVideoDevice()->syncBuffers(); - } - else - { - m_videoDevice->widget()->context()->swapBuffers(m_videoDevice->widget()->context()->surface()); - } - - trecord.swapEnd = session->profilingElapsedTime(); - session->endProfilingSample(); - } - else - { - if (session->outputVideoDevice() != videoDevice()) - { - session->outputVideoDevice()->syncBuffers(); - } - } - - session->addSyncSample(); - - session->postRender(); - - m_eventProcessingTimer.start(); - - TWK_GLDEBUG; - } - - void GLView::eventProcessingTimeout() { m_doc->session()->userGenericEvent("per-render-event-processing", ""); } - - bool GLView::event(QEvent* event) - { - // qDebug() << "Event type: " << event->type(); - - bool keyevent = false; - Rv::Session* session = m_doc->session(); - - if (m_stopProcessingEvents) - { - event->accept(); - return true; - } - - // - // We want to exclude the "click-through" clicks on - // click-to-focus systems (like osX). Turns out the event - // sequence in these cases is exactly the same as a - // tab-to-focus, then click sequence. So the only way we have - // to identify the "click-through" is by how quickly the click - // (button push) follows the windowActivate event. - // - - if (event->type() == QEvent::WindowActivate) - m_activationTimer.start(); - -#if 0 - // - // This doesn't work. Nothing I've tried will make a stylus - // press raise and activate the window when we are handling - // native tablet events. It works fine when we are treating - // stylus events as mouse events. - // - if (event->type() == QEvent::TabletPress && !isActiveWindow()) - { - cerr << "activating window" << endl; - QEvent activateEvent(QEvent::WindowActivate); - //m_frameBuffer->translator().sendQTEvent(new QEvent(QEvent::WindowActivate)); - session->setEventVideoDevice(videoDevice()); - m_videoDevice->translator().sendQTEvent(new QEvent(QEvent::WindowActivate)); - event->accept(); - /* - activateWindow(); - raise(); - event->accept(); - */ - return true; - } -#endif - - float activationTime = 0.0; - if (m_activationTimer.isRunning()) - { - if (event->type() == QEvent::MouseButtonPress) - { - // - // Pass this time through with the event, so that - // event handling code can decide witehr or not to ignore - // this 'click-through' event. - // - activationTime = m_activationTimer.elapsed(); - m_activationTimer.stop(); - } - if (event->type() == QEvent::MouseMove) - m_activationTimer.stop(); - } - - if (event->type() != QEvent::Paint) - { - m_activityTimer.stop(); - m_activityTimer.start(); - - if (!m_userActive) - { - TwkApp::ActivityChangeEvent aevent("user-active", m_videoDevice); - - // - // m_userActive set first will prevent recursive nightmare. In - // Qt 4.4 an event may recursively produce more - // events. For example, changing the cursor in 4.4 will - // cause a Paint event immediately (not after the - // previous event returns). - // - - m_userActive = true; - m_videoDevice->sendEvent(aevent); - } - } - - if (QKeyEvent* kevent = dynamic_cast(event)) - { - keyevent = true; - - // - // Get around really annoying event bugs on Qt/Mac - // - - if (m_lastKey == kevent->key() - && (m_lastKeyType == QEvent::ShortcutOverride && (kevent->type() == QEvent::KeyPress) || (m_lastKeyType == kevent->type()))) - { - // - // Qt 4.3.3 (4.5 too) will give both override and press events - // even if key is not in menu. Filter that here. - // - // Remember the new key/type, otherwise we filter out - // any number of ShortcutOverride/Press pairs, and - // auto-repeat doesn't work. - // - m_lastKey = kevent->key(); - m_lastKeyType = kevent->type(); - - event->accept(); - return true; - } - - m_lastKeyType = kevent->type(); - m_lastKey = kevent->key(); - } - - switch (event->type()) - { - case QEvent::FocusIn: - m_videoDevice->translator().resetModifiers(); - case QEvent::Enter: - setFocus(Qt::MouseFocusReason); - break; - default: - break; - } - if (event->type() == QEvent::Resize) - { - QResizeEvent* e = static_cast(event); - - // QT5 BUG -- results in invalid drawable - if (!isVisible()) - return true; - - if (e->oldSize().width() != -1 && e->oldSize().height() != -1) - { - ostringstream contents; - contents << e->oldSize().width() << " " << e->oldSize().height() << "|" << e->size().width() << " " << e->size().height(); - - if (m_doc && session) - { - session->userGenericEvent("view-resized", contents.str()); - } - } - return QOpenGLWidget::event(event); - } - - if (session && session->outputVideoDevice() - && session->outputVideoDevice()->displayMode() == TwkApp::VideoDevice::MirrorDisplayMode) - { - if (const TwkApp::VideoDevice* cdv = session->controlVideoDevice()) - { - const TwkApp::VideoDevice* odv = session->outputVideoDevice(); - - if (odv && cdv != odv && cdv == videoDevice()) - { - const float w = width(); - const float h = height(); - const float ow = odv->width(); - const float oh = odv->height(); - - const float aspect = w / h; - const float oaspect = ow / oh; - - m_videoDevice->translator().setRelativeDomain(ow, oh); - - if (aspect >= oaspect) - { - const float yscale = oh / h; - const float yoffset = 0.0; - const float xscale = yscale; - const float xoffset = -(w * yscale - ow) / 2.0; - m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); - } - else - { - const float xscale = ow / w; - const float xoffset = 0.0; - const float yscale = xscale; - const float yoffset = -(xscale * h - oh) / 2.0; - m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); - } - } - else - { - m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); - m_videoDevice->translator().setRelativeDomain(width(), height()); - } - } - else - { - m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); - m_videoDevice->translator().setRelativeDomain(width(), height()); - } - } - else - { - m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); - m_videoDevice->translator().setRelativeDomain(width(), height()); - } - - if (session) - session->setEventVideoDevice(videoDevice()); - - if (m_videoDevice->translator().sendQTEvent(event, activationTime)) - { - event->accept(); - return true; - } - else - { - bool result = QOpenGLWidget::event(event); - - return result; - } - } - - bool GLView::eventFilter(QObject* object, QEvent* event) - { - if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease || event->type() == QEvent::Shortcut - || event->type() == QEvent::ShortcutOverride) - { - - // - // Get around really annoying event bugs on Qt/Mac - // (copied from GLView::event... same bug applies -lo) - // - // Qt 4.3.3 (4.5 too) will give both override and press events even - // if key is not in menu. Filter that here. - // - // Remember the new key/type, otherwise we filter out - // any number of ShortcutOverride/Press pairs, and - // auto-repeat doesn't work. - // - - if (QKeyEvent* kevent = dynamic_cast(event)) - { - if (m_lastKey == kevent->key() - && (m_lastKeyType == QEvent::ShortcutOverride && (kevent->type() == QEvent::KeyPress) - || (m_lastKeyType == kevent->type()))) - { - m_lastKey = kevent->key(); - m_lastKeyType = kevent->type(); - - event->accept(); - return true; - } - - m_lastKeyType = kevent->type(); - m_lastKey = kevent->key(); - } - - // if (m_frameBuffer->translator().sendQTEvent(event)) - Session* session = m_doc->session(); - session->setEventVideoDevice(videoDevice()); - if (m_videoDevice->translator().sendQTEvent(event)) - { - - event->accept(); - return true; - } - - event->accept(); - return true; - } - - return false; - } - - float GLView::devicePixelRatio() const { return videoDevice() ? videoDevice()->devicePixelRatio() : 1.0f; } - } // namespace Rv diff --git a/src/lib/app/RvCommon/GLWindow.cpp b/src/lib/app/RvCommon/GLWindow.cpp new file mode 100644 index 000000000..6e536d42f --- /dev/null +++ b/src/lib/app/RvCommon/GLWindow.cpp @@ -0,0 +1,372 @@ +//****************************************************************************** +// Copyright (c) 2007 Tweak Inc. +// All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +//****************************************************************************** + +#ifdef PLATFORM_WINDOWS +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Rv +{ + using namespace std; + using namespace TwkApp; + using namespace IPCore; + + GLWindow::GLWindow(QOpenGLContext* sharedContext, RvDocument* doc, bool stereo, bool vsync, bool doubleBuffer, int red, int green, + int blue, int alpha, bool noResize) + : QOpenGLWindow(QOpenGLWindow::NoPartialUpdate) + , m_doc(doc) + , m_red(red) + , m_green(green) + , m_blue(blue) + , m_alpha(alpha) + , m_lastKey(0) + , m_lastKeyType(QEvent::None) + , m_userActive(true) + , m_firstPaintCompleted(false) + , m_postFirstNonEmptyRender(noResize) + , m_stopProcessingEvents(false) + , m_sharedContext(sharedContext) + { + setFormat(GLView::rvGLFormat(stereo, vsync, doubleBuffer, red, green, blue, alpha)); + + m_videoDevice = nullptr; // set later by the hosting GLView + + m_activityTimer.start(); + + m_eventProcessingTimer.setSingleShot(true); + connect(&m_eventProcessingTimer, SIGNAL(timeout()), this, SLOT(eventProcessingTimeout())); + } + + GLWindow::~GLWindow() {} + + void GLWindow::stopProcessingEvents() { m_stopProcessingEvents = true; } + + void GLWindow::eventProcessingTimeout() { m_doc->session()->userGenericEvent("per-render-event-processing", ""); } + + float GLWindow::devicePixelRatioF() const { return static_cast(devicePixelRatio()); } + + void GLWindow::absolutePosition(int& x, int& y) const + { + QPoint gp = mapToGlobal(QPoint(0, 0)); + x = gp.x(); + y = gp.y(); + } + + void GLWindow::initializeGL() + { + if (context()->isValid()) + { + initializeGLExtensions(); + initializeOpenGLFunctions(); + + if (m_sharedContext) + { + context()->setShareContext(m_sharedContext); + } + + // + // NOTE: session initialization is deliberately NOT driven from + // here. Loading packages creates web panels, and adding a + // QWebEngineView makes Qt tear down the main window's native + // subtree -- destroying this very window while this method is + // still on the stack, so every later member access is a + // use-after-free. RvApplication::newSessionFromFiles() calls + // RvDocument::initializeSession() after show() instead, with no + // GL callback in the call chain. + // + + QSurfaceFormat f = context()->format(); + +#ifndef PLATFORM_DARWIN + if (f.redBufferSize() != m_red && m_red != 0) + { + ostringstream str; + str << "WARNING: asked for" + << " " << m_red << " " << m_green << " " << m_blue << " " << m_alpha << " RGBA color but got" + << " " << f.redBufferSize() << " " << f.greenBufferSize() << " " << f.blueBufferSize() << " " + << (f.alphaBufferSize() <= 0 ? 0 : f.alphaBufferSize()) << " RGBA instead"; + cout << str.str() << endl; + } +#endif + if (f.stencilBufferSize() == 0) + { + cout << "WARNING: no stencil buffer available" << endl; + } + } + else + { + cout << "WARNING: invalid GL context" << endl; + } + } + + void GLWindow::resizeGL(int w, int h) + { + if (m_doc) + m_doc->viewSizeChanged(w, h); + } + + QImage GLWindow::readPixels(int x, int y, int w, int h) + { + const int pw = width() * devicePixelRatio(); + const int ph = height() * devicePixelRatio(); + + // If out of bounds, return an empty image. + if (x < 0 || y < 0 || (x + w) > pw || (y + h) > ph) + return QImage(0, 0, QImage::Format_RGBA8888); + + if (m_videoDevice) + m_videoDevice->makeCurrent(); + else + makeCurrent(); + + QImage image(w, h, QImage::Format_RGBA8888); + glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.bits()); + + return image; + } + + void GLWindow::paintGL() + { + TWK_GLDEBUG; + + IPCore::Session* session = m_doc->session(); + + if (!m_postFirstNonEmptyRender && session && session->postFirstNonEmptyRender()) + { + m_postFirstNonEmptyRender = true; + + if (!session->isFullScreen()) + { + m_doc->resizeToFit(false, false); + m_doc->center(); + TWK_GLDEBUG; + } + } + + if (m_doc && session && m_videoDevice) + { + m_videoDevice->makeCurrent(); + TWK_GLDEBUG; + + if (m_userActive && m_activityTimer.elapsed() > 1.0) + { + if (m_doc->mainPopup() && !m_doc->mainPopup()->isVisible()) + { + TwkApp::ActivityChangeEvent aevent("user-inactive", m_videoDevice); + m_videoDevice->sendEvent(aevent); + TWK_GLDEBUG; + m_userActive = false; + } + } + + // + // Make sure the video device knows where it is on screen. + // + int x = 0, y = 0; + absolutePosition(x, y); + m_videoDevice->setAbsolutePosition(x, y); + + TWK_GLDEBUG; + session->render(); + TWK_GLDEBUG; + + m_firstPaintCompleted = true; + + // Force the resulting alpha channel to 1 so the surface is fully + // opaque (matches the former GLView behavior). + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, QOpenGLContext::currentContext()->defaultFramebufferObject()); + TWK_GLDEBUG; + glPushAttrib(GL_COLOR_BUFFER_BIT); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); + glClearColor(0.f, 0.f, 0.f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glPopAttrib(); + TWK_GLDEBUG; + } + else + { + glClearColor(0.f, 0.f, 0.f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + TWK_GLDEBUG; + } + + if (m_stopProcessingEvents) + return; + + // If a separate output device is presenting, sync it. The control + // (window) surface presents itself: QOpenGLWindow swaps automatically + // after paintGL returns. + if (session->outputVideoDevice() != m_videoDevice) + { + session->outputVideoDevice()->syncBuffers(); + } + + session->addSyncSample(); + session->postRender(); + + m_eventProcessingTimer.start(); + + TWK_GLDEBUG; + } + + bool GLWindow::event(QEvent* event) + { + // The device (and its translator) is wired by the hosting GLView just + // after construction; ignore any events that arrive before then. + if (!m_videoDevice) + return QOpenGLWindow::event(event); + + bool keyevent = false; + Rv::Session* session = m_doc->session(); + + if (m_stopProcessingEvents) + { + event->accept(); + return true; + } + + if (event->type() == QEvent::WindowActivate) + m_activationTimer.start(); + + float activationTime = 0.0; + if (m_activationTimer.isRunning()) + { + if (event->type() == QEvent::MouseButtonPress) + { + activationTime = m_activationTimer.elapsed(); + m_activationTimer.stop(); + } + if (event->type() == QEvent::MouseMove) + m_activationTimer.stop(); + } + + if (event->type() != QEvent::Paint && event->type() != QEvent::UpdateRequest) + { + m_activityTimer.stop(); + m_activityTimer.start(); + + if (!m_userActive) + { + TwkApp::ActivityChangeEvent aevent("user-active", m_videoDevice); + m_userActive = true; + m_videoDevice->sendEvent(aevent); + } + } + + if (QKeyEvent* kevent = dynamic_cast(event)) + { + keyevent = true; + + if (m_lastKey == kevent->key() + && (m_lastKeyType == QEvent::ShortcutOverride && (kevent->type() == QEvent::KeyPress) || (m_lastKeyType == kevent->type()))) + { + m_lastKey = kevent->key(); + m_lastKeyType = kevent->type(); + event->accept(); + return true; + } + + m_lastKeyType = kevent->type(); + m_lastKey = kevent->key(); + } + + switch (event->type()) + { + case QEvent::FocusIn: + m_videoDevice->translator().resetModifiers(); + case QEvent::Enter: + requestActivate(); + break; + default: + break; + } + + if (session && session->outputVideoDevice() + && session->outputVideoDevice()->displayMode() == TwkApp::VideoDevice::MirrorDisplayMode) + { + if (const TwkApp::VideoDevice* cdv = session->controlVideoDevice()) + { + const TwkApp::VideoDevice* odv = session->outputVideoDevice(); + + if (odv && cdv != odv && cdv == m_videoDevice) + { + const float w = width(); + const float h = height(); + const float ow = odv->width(); + const float oh = odv->height(); + + const float aspect = w / h; + const float oaspect = ow / oh; + + m_videoDevice->translator().setRelativeDomain(ow, oh); + + if (aspect >= oaspect) + { + const float yscale = oh / h; + const float xscale = yscale; + const float xoffset = -(w * yscale - ow) / 2.0; + m_videoDevice->translator().setScaleAndOffset(xoffset, 0.0, xscale, yscale); + } + else + { + const float xscale = ow / w; + const float yscale = xscale; + const float yoffset = -(xscale * h - oh) / 2.0; + m_videoDevice->translator().setScaleAndOffset(0.0, yoffset, xscale, yscale); + } + } + else + { + m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); + m_videoDevice->translator().setRelativeDomain(width(), height()); + } + } + else + { + m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); + m_videoDevice->translator().setRelativeDomain(width(), height()); + } + } + else + { + m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0, 1.0); + m_videoDevice->translator().setRelativeDomain(width(), height()); + } + + if (session) + session->setEventVideoDevice(m_videoDevice); + + if (m_videoDevice->translator().sendQTEvent(event, activationTime)) + { + event->accept(); + return true; + } + + return QOpenGLWindow::event(event); + } + +} // namespace Rv diff --git a/src/lib/app/RvCommon/QTGLVideoDevice.cpp b/src/lib/app/RvCommon/QTGLVideoDevice.cpp index 50fd6e5e4..ba4fd3eed 100644 --- a/src/lib/app/RvCommon/QTGLVideoDevice.cpp +++ b/src/lib/app/RvCommon/QTGLVideoDevice.cpp @@ -47,6 +47,18 @@ namespace Rv { } + QTGLVideoDevice::QTGLVideoDevice(VideoModule* m, const string& name, QOpenGLWindow* window, QWidget* eventWidget) + : GLVideoDevice(m, name, ImageOutput | ProvidesSync | SubWindow) + , m_view(0) + , m_window(window) + , m_translator(new QTTranslator(this, eventWidget)) + , m_x(0) + , m_y(0) + , m_refresh(-1.0) + { + assert(window); + } + QTGLVideoDevice::QTGLVideoDevice(const string& name, QOpenGLWidget* view) : GLVideoDevice(NULL, name, NoCapabilities) , m_view(view) @@ -68,16 +80,36 @@ namespace Rv GLVideoDevice* QTGLVideoDevice::newSharedContextWorkerDevice() const { - // NOTE_QT: QOpenGLWidget does not take a share parameter anymore. Try - // to share with setShareContext. - QOpenGLWidget* openGLWidget = new QOpenGLWidget(m_view->parentWidget()); - openGLWidget->context()->setShareContext(m_view->context()); + // A freshly constructed QOpenGLWidget has no context() yet -- it is + // created lazily on first show/makeCurrent -- so the previous + // openGLWidget->context()->setShareContext(...) here dereferenced a null + // context and crashed (reachable via ImageRenderer::createGLContexts() + // when "Multithread GPU Upload" is enabled). Explicit sharing is also + // unnecessary: Qt::AA_ShareOpenGLContexts (set in main.cpp) makes all GL + // contexts in the process share resources, so this worker context shares + // with the control viewport automatically. + QOpenGLWidget* openGLWidget = new QOpenGLWidget(m_view ? m_view->parentWidget() : nullptr); return new QTGLVideoDevice(name() + "-workerContextDevice", openGLWidget); } void QTGLVideoDevice::makeCurrent() const { - if (m_view->context() && m_view->context()->isValid()) + if (m_window) + { + // QOpenGLWindow creates its GL context lazily on the first + // makeCurrent(), provided the platform window (surface) exists. + if (m_window->handle()) + { + m_window->makeCurrent(); + TWK_GLDEBUG; + + GLint surfaceFBO = m_window->defaultFramebufferObject(); + if (surfaceFBO != 0) + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, surfaceFBO); + TWK_GLDEBUG; + } + } + else if (m_view && m_view->context() && m_view->context()->isValid()) { m_view->makeCurrent(); TWK_GLDEBUG; @@ -94,6 +126,8 @@ namespace Rv GLuint QTGLVideoDevice::fboID() const { + if (m_window) + return m_window->defaultFramebufferObject(); if (m_view) return m_view->defaultFramebufferObject(); @@ -129,14 +163,27 @@ namespace Rv { if (!isWorkerDevice()) { - QSize s = m_view->size(); - m_view->update(); + if (m_window) + m_window->update(); + else if (m_view) + m_view->update(); } } void QTGLVideoDevice::redrawImmediately() const { - if (!isWorkerDevice()) + if (isWorkerDevice()) + return; + + if (m_window) + { + if (m_window->isVisible()) + m_window->update(); + else + redraw(); + return; + } + { if (m_view->isVisible()) { @@ -166,12 +213,16 @@ namespace Rv VideoDevice::Resolution QTGLVideoDevice::resolution() const { - return Resolution(m_view->width() * devicePixelRatio(), m_view->height() * devicePixelRatio(), 1.0f, 1.0f); + const int w = m_window ? m_window->width() : m_view->width(); + const int h = m_window ? m_window->height() : m_view->height(); + return Resolution(w * devicePixelRatio(), h * devicePixelRatio(), 1.0f, 1.0f); } VideoDevice::Resolution QTGLVideoDevice::internalResolution() const { - return Resolution(m_view->width(), m_view->height(), 1.0f, 1.0f); + const int w = m_window ? m_window->width() : m_view->width(); + const int h = m_window ? m_window->height() : m_view->height(); + return Resolution(w, h, 1.0f, 1.0f); } VideoDevice::Offset QTGLVideoDevice::offset() const { return Offset(m_x, m_y); } @@ -180,45 +231,52 @@ namespace Rv VideoDevice::VideoFormat QTGLVideoDevice::format() const { - return VideoFormat(m_view->width() * devicePixelRatio(), m_view->height() * devicePixelRatio(), 1.0, 1.0, - (m_refresh != -1.0) ? m_refresh : 0.0, hardwareIdentification()); + const int w = m_window ? m_window->width() : m_view->width(); + const int h = m_window ? m_window->height() : m_view->height(); + return VideoFormat(w * devicePixelRatio(), h * devicePixelRatio(), 1.0, 1.0, (m_refresh != -1.0) ? m_refresh : 0.0, + hardwareIdentification()); } void QTGLVideoDevice::open(const StringVector& args) { - if (!isWorkerDevice()) + if (isWorkerDevice()) + return; + if (m_window) + m_window->show(); + else m_view->show(); } void QTGLVideoDevice::close() { - if (!isWorkerDevice()) + if (isWorkerDevice()) + return; + if (m_window) + m_window->hide(); + else m_view->hide(); } bool QTGLVideoDevice::isOpen() const { if (isWorkerDevice()) - { return false; - } - else - { - return m_view->isVisible(); - } + return m_window ? m_window->isVisible() : m_view->isVisible(); } - size_t QTGLVideoDevice::width() const { return m_view->width() * devicePixelRatio(); } + size_t QTGLVideoDevice::width() const { return (m_window ? m_window->width() : m_view->width()) * devicePixelRatio(); } - size_t QTGLVideoDevice::height() const { return m_view->height() * devicePixelRatio(); } + size_t QTGLVideoDevice::height() const { return (m_window ? m_window->height() : m_view->height()) * devicePixelRatio(); } void QTGLVideoDevice::syncBuffers() const { - if (!isWorkerDevice()) - { - makeCurrent(); + if (isWorkerDevice()) + return; + makeCurrent(); + if (m_window) + m_window->context()->swapBuffers(m_window); + else m_view->context()->swapBuffers(m_view->context()->surface()); - } } void QTGLVideoDevice::setAbsolutePosition(int x, int y) diff --git a/src/lib/app/RvCommon/RvApplication.cpp b/src/lib/app/RvCommon/RvApplication.cpp index 4920fae65..9e569c7db 100644 --- a/src/lib/app/RvCommon/RvApplication.cpp +++ b/src/lib/app/RvCommon/RvApplication.cpp @@ -734,6 +734,16 @@ namespace Rv doc->raise(); #endif + // + // Create the session now that the viewport window exists and its GL + // context has been created by show(). This must happen here rather + // than from GLWindow::initializeGL(): loading packages creates web + // panels, and adding a QWebEngineView makes Qt destroy the main + // window's native subtree -- including the viewport window -- which + // is fatal if a GLWindow method is still on the call stack. + // + doc->initializeSession(); + // doc->ensurePolished(); Rv::RvSession* s = doc->session(); diff --git a/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h b/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h index 394804930..dc78b2f2c 100644 --- a/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h +++ b/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace Rv @@ -57,13 +58,19 @@ namespace Rv class ScreenView : public QOpenGLWidget { public: - ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLWidget* glViewShare, Qt::WindowFlags flags); + // + // glShareContext is the control view's GL context to share with + // (so blits/FBOs are usable across the two surfaces). It comes + // from QTGLVideoDevice::glShareContext() and is backing-agnostic: + // the control view may be a QOpenGLWidget or a QOpenGLWindow. + // + ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLContext* glShareContext, Qt::WindowFlags flags); void initializeGL() override; void paintGL() override; private: - QOpenGLWidget* m_glViewShare = nullptr; + QOpenGLContext* m_glShareContext = nullptr; }; public: diff --git a/src/lib/app/RvCommon/RvCommon/GLView.h b/src/lib/app/RvCommon/RvCommon/GLView.h index 4cda0171e..40f1a0dbb 100644 --- a/src/lib/app/RvCommon/RvCommon/GLView.h +++ b/src/lib/app/RvCommon/RvCommon/GLView.h @@ -8,55 +8,74 @@ #ifndef __rv_qt__GLView__h__ #define __rv_qt__GLView__h__ #include -#include -#include +#include #include -#include -#include -#include -#include -#include +#include +#include + +class QOpenGLContext; namespace Rv { class RvDocument; class QTGLVideoDevice; - - class GLView - : public QOpenGLWidget - , protected QOpenGLFunctions + class GLWindow; + + // + // GLView + // + // Host QWidget that embeds the native GL viewport (GLWindow) via + // QWidget::createWindowContainer(). GLView keeps the public API the rest + // of RV expects -- view()->context()/format()/makeCurrent()/videoDevice() + // etc. -- by delegating to the GLWindow. + // + // Crucially, GLView is no longer a QOpenGLWidget. That keeps the top-level + // QMainWindow off the OpenGL RHI backend (so docked QWebEngineView panels + // render on the platform default backend), and lets the viewport present + // on its own native surface instead of via the full-window widget + // composite. + // + + class GLView : public QWidget { Q_OBJECT public: - typedef TwkUtil::Timer Timer; - GLView(QWidget* parent, QOpenGLContext* sharedContext, RvDocument* doc, bool stereo = false, bool vsync = true, bool doubleBuffer = true, int red = 0, int green = 0, int blue = 0, int alpha = 0, bool noResize = true); - ~GLView(); + ~GLView() override; static QSurfaceFormat rvGLFormat(bool stereo = false, bool vsync = true, bool doubleBuffer = true, int red = 8, int green = 8, int blue = 8, int alpha = 8); - void absolutePosition(int& x, int& y) const; + GLWindow* glWindow() const { return m_glWindow; } QTGLVideoDevice* videoDevice() const { return m_videoDevice; } - void stopProcessingEvents(); + // + // Delegated QOpenGLWidget-compatible API still used across RV. + // + QOpenGLContext* context() const; + QSurfaceFormat format() const; + void makeCurrent(); + + // Compatibility shim for the former QOpenGLWidget::isValid(): the + // native GL window is created up-front, so the viewport is considered + // valid once the window exists. + bool isValid() const; - virtual bool event(QEvent*); - virtual bool eventFilter(QObject* object, QEvent* event); + void absolutePosition(int& x, int& y) const; + + void stopProcessingEvents(); - bool firstPaintCompleted() const { return m_firstPaintCompleted; }; + bool firstPaintCompleted() const; void setContentSize(int w, int h) { m_csize = QSize(w, h); } void setMinimumContentSize(int w, int h) { m_msize = QSize(w, h); } - virtual QSize sizeHint() const; - virtual QSize minimumSizeHint() const; - - void* syncClosure() const { return m_syncThreadData; } + QSize sizeHint() const override; + QSize minimumSizeHint() const override; QImage readPixels(int x, int y, int w, int h); @@ -64,38 +83,13 @@ namespace Rv // For reference: https://doc.qt.io/qt-6/highdpi.html float devicePixelRatio() const; - public slots: - void eventProcessingTimeout(); - - protected: - void initializeGL(); - void resizeGL(int w, int h); - void paintGL(); - bool validateReadPixels(int x, int y, int w, int h); - void debugSaveFramebuffer(); - private: RvDocument* m_doc; - boost::thread m_swapThread; + GLWindow* m_glWindow; + QWidget* m_container; QTGLVideoDevice* m_videoDevice; - unsigned int m_lastKey; - QEvent::Type m_lastKeyType; - Timer m_activityTimer; - Timer m_renderTimer; - QTimer m_eventProcessingTimer; - bool m_userActive; - size_t m_renderCount; - Timer m_activationTimer; - bool m_firstPaintCompleted; QSize m_csize; QSize m_msize; - int m_red; - int m_green; - int m_blue; - int m_alpha; - bool m_postFirstNonEmptyRender; - bool m_stopProcessingEvents; - void* m_syncThreadData; QOpenGLContext* m_sharedContext; }; diff --git a/src/lib/app/RvCommon/RvCommon/GLWindow.h b/src/lib/app/RvCommon/RvCommon/GLWindow.h new file mode 100644 index 000000000..ef24551cf --- /dev/null +++ b/src/lib/app/RvCommon/RvCommon/GLWindow.h @@ -0,0 +1,102 @@ +//****************************************************************************** +// Copyright (c) 2007 Tweak Inc. +// All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +//****************************************************************************** +#ifndef __rv_qt__GLWindow__h__ +#define __rv_qt__GLWindow__h__ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Rv +{ + class RvDocument; + class QTGLVideoDevice; + + // + // GLWindow + // + // The RV viewport rendered as a *native* GL surface (QOpenGLWindow) + // instead of a composited QOpenGLWidget. Because it is a real window and + // not a QOpenGLWidget in the main window's widget tree, the top-level + // QMainWindow is no longer forced to the OpenGL RHI backend -- so docked + // QWebEngineView panels render on the platform default backend -- and the + // viewport presents on its own surface instead of via the ~90 ms + // full-window widget composite. + // + // It is embedded in the widget hierarchy by GLView via + // QWidget::createWindowContainer(). Rendering and event routing are ported + // from the former QOpenGLWidget-based GLView. + // + + class GLWindow + : public QOpenGLWindow + , protected QOpenGLFunctions + { + Q_OBJECT + + public: + typedef TwkUtil::Timer Timer; + + GLWindow(QOpenGLContext* sharedContext, RvDocument* doc, bool stereo = false, bool vsync = true, bool doubleBuffer = true, + int red = 0, int green = 0, int blue = 0, int alpha = 0, bool noResize = true); + ~GLWindow() override; + + QTGLVideoDevice* videoDevice() const { return m_videoDevice; } + + // The device is created and owned by the hosting GLView (it needs the + // container QWidget for event/coordinate translation), then handed + // here. GLWindow does not take ownership. + void setVideoDevice(QTGLVideoDevice* d) { m_videoDevice = d; } + + void stopProcessingEvents(); + + bool event(QEvent*) override; + + bool firstPaintCompleted() const { return m_firstPaintCompleted; } + + // Absolute (global) top-left position of the surface in pixels. + void absolutePosition(int& x, int& y) const; + + float devicePixelRatioF() const; + + QImage readPixels(int x, int y, int w, int h); + + public slots: + void eventProcessingTimeout(); + + protected: + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + private: + RvDocument* m_doc; + QTGLVideoDevice* m_videoDevice; + unsigned int m_lastKey; + QEvent::Type m_lastKeyType; + Timer m_activityTimer; + QTimer m_eventProcessingTimer; + bool m_userActive; + Timer m_activationTimer; + bool m_firstPaintCompleted; + int m_red; + int m_green; + int m_blue; + int m_alpha; + bool m_postFirstNonEmptyRender; + bool m_stopProcessingEvents; + QOpenGLContext* m_sharedContext; + }; + +} // namespace Rv + +#endif // __rv_qt__GLWindow__h__ diff --git a/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h b/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h index 6bc414926..91cab3c10 100644 --- a/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h +++ b/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -29,6 +32,13 @@ namespace Rv { public: QTGLVideoDevice(TwkApp::VideoModule*, const std::string& name, QOpenGLWidget* view); + // + // Native-window backed variant. The GL surface is a QOpenGLWindow + // (embedded in the widget tree via createWindowContainer); eventWidget + // is the container QWidget used by the QTTranslator for coordinate + // mapping (height/mapToGlobal) and mouse grab. + // + QTGLVideoDevice(TwkApp::VideoModule*, const std::string& name, QOpenGLWindow* window, QWidget* eventWidget); QTGLVideoDevice(TwkApp::VideoModule*, const std::string& name); virtual ~QTGLVideoDevice(); @@ -36,6 +46,19 @@ namespace Rv QOpenGLWidget* widget() const { return m_view; } + QOpenGLWindow* window() const { return m_window; } + + // + // Backing-agnostic accessors for the shareable GL context and its + // surface format. The control view may be backed by either a + // QOpenGLWidget (m_view) or, in the native-window port, a + // QOpenGLWindow (m_window). Second-output devices (DesktopVideoDevice) + // share the control context and need these without caring which. + // + QOpenGLContext* glShareContext() const { return m_window ? m_window->context() : (m_view ? m_view->context() : nullptr); } + + QSurfaceFormat glSurfaceFormat() const { return m_window ? m_window->format() : (m_view ? m_view->format() : QSurfaceFormat()); } + virtual void makeCurrent() const; const QTTranslator& translator() const { return *m_translator; } @@ -85,6 +108,7 @@ namespace Rv float m_refresh; float m_devicePixelRatio{1.0f}; QOpenGLWidget* m_view; + QOpenGLWindow* m_window{nullptr}; QTTranslator* m_translator; }; diff --git a/src/lib/app/RvCommon/RvCommon/RvDocument.h b/src/lib/app/RvCommon/RvCommon/RvDocument.h index 91634cc62..2744d8106 100644 --- a/src/lib/app/RvCommon/RvCommon/RvDocument.h +++ b/src/lib/app/RvCommon/RvCommon/RvDocument.h @@ -148,6 +148,12 @@ namespace Rv void mergeMenu(const TwkApp::Menu*, bool shortcuts = true); void convert(QMenu*, const TwkApp::Menu*, bool shortcuts); + // Position the UI blocking overlay over the main window's client area. + // The overlay is a frameless top-level window (not a child widget) so + // it can cover the native GL viewport window, which renders above + // sibling raster widgets and so cannot be dimmed by a child overlay. + void positionBlockingOverlay(); + void closeEvent(QCloseEvent*) override; void changeEvent(QEvent*) override; bool event(QEvent*) override; diff --git a/src/lib/app/RvCommon/RvDocument.cpp b/src/lib/app/RvCommon/RvDocument.cpp index 773864e6d..a80cfb375 100644 --- a/src/lib/app/RvCommon/RvDocument.cpp +++ b/src/lib/app/RvCommon/RvDocument.cpp @@ -282,16 +282,29 @@ namespace Rv } // - // Create UI blocking overlay - transparent widget that captures all input + // Create UI blocking overlay - a transparent window that captures all + // input and dims the UI. // - m_blockingOverlay = new QWidget(this); + // It is a frameless top-level window (owned by this document) rather + // than a child widget. The viewport is a native QOpenGLWindow, which + // renders above any sibling raster child widget regardless of + // raise()/stacking order, so a child overlay could never dim or block + // the viewport. A top-level window sits above the main window and its + // native child, so it covers the viewport too. + // + m_blockingOverlay = new QWidget(this, Qt::FramelessWindowHint | Qt::Tool); m_blockingOverlay->setObjectName("UIBlockingOverlay"); - // Semi-transparent dark overlay for visual feedback - m_blockingOverlay->setStyleSheet("QWidget#UIBlockingOverlay { background-color: rgba(0, 0, 0, 100); }"); + // Semi-transparent dark overlay for visual feedback. A top-level window + // has no per-pixel alpha unless WA_TranslucentBackground is set, and an + // rgba stylesheet fill does not reliably paint on a plain translucent + // top-level QWidget. Instead fill solid black and make the whole window + // see-through with window opacity, which is deterministic on all + // platforms and composites correctly above the native GL viewport. + m_blockingOverlay->setStyleSheet("QWidget#UIBlockingOverlay { background-color: rgb(0, 0, 0); }"); + m_blockingOverlay->setWindowOpacity(0.4); - // Ensure it stays on top and captures input - m_blockingOverlay->raise(); + // Capture input (mouse + keyboard) while shown m_blockingOverlay->setAttribute(Qt::WA_TransparentForMouseEvents, false); m_blockingOverlay->setFocusPolicy(Qt::StrongFocus); @@ -299,10 +312,35 @@ namespace Rv m_blockingOverlay->hide(); } + void RvDocument::positionBlockingOverlay() + { + if (!m_blockingOverlay) + return; + + // Cover the main window's client area in global coordinates (the + // overlay is a top-level window, so it needs screen coordinates). + m_blockingOverlay->setGeometry(QRect(mapToGlobal(QPoint(0, 0)), size())); + m_blockingOverlay->raise(); + } + void RvDocument::initializeSession() { + // + // Called by RvApplication::newSessionFromFiles() once the document has + // been shown, so the viewport window exists and its GL context has + // been created. Constructing an RvSession queries + // GL_SHADING_LANGUAGE_VERSION and aborts without a current context, so + // make the viewport context current first. + // + if (!m_glView) + { + return; + } + if (!m_session) { + m_glView->makeCurrent(); + m_session = new RvSession; // m_session->setFrameBuffer(fb); @@ -1977,18 +2015,19 @@ namespace Rv { if (m_session) m_session->askForRedraw(); + + // Keep the top-level overlay aligned with the window when visible. + if (m_blockingOverlay && m_blockingOverlay->isVisible()) + positionBlockingOverlay(); } void RvDocument::resizeEvent(QResizeEvent* event) { QMainWindow::resizeEvent(event); - // Keep overlay covering entire window when visible + // Keep overlay covering entire client area when visible if (m_blockingOverlay && m_blockingOverlay->isVisible()) - { - m_blockingOverlay->setGeometry(0, 0, width(), height()); - m_blockingOverlay->raise(); - } + positionBlockingOverlay(); } void RvDocument::setUIBlocked(bool blocked) @@ -2000,12 +2039,13 @@ namespace Rv if (blocked) { - // Cover entire window - m_blockingOverlay->setGeometry(0, 0, width(), height()); - m_blockingOverlay->raise(); // Bring to front, above everything + // Cover entire client area (top-level window, global coords) + positionBlockingOverlay(); m_blockingOverlay->show(); + m_blockingOverlay->raise(); // Bring to front, above everything // Grab focus to also block keyboard input + m_blockingOverlay->activateWindow(); m_blockingOverlay->setFocus(Qt::OtherFocusReason); } else diff --git a/src/lib/app/mu_rvui/mode_manager.mu b/src/lib/app/mu_rvui/mode_manager.mu index 001e736c1..cc8528662 100644 --- a/src/lib/app/mu_rvui/mode_manager.mu +++ b/src/lib/app/mu_rvui/mode_manager.mu @@ -755,8 +755,40 @@ class: ModeManagerMode : MinorMode let vnode = viewNode(), vtype = nodeType(vnode), - name = if vtype.substr(0,2) == "RV" then vtype.substr(2,0) + "_edit_mode" else "", - entry = findModeEntry(name); + name = if vtype.substr(0,2) == "RV" then vtype.substr(2,0) + "_edit_mode" else ""; + + // + // Avoid redundant deactivate->reactivate churn when the view-edit-mode + // is not actually changing. Toggling an edit mode is expensive (it + // rebuilds menus/event tables), and clearSession re-sets the same + // default view twice with force=true -- so without this the same edit + // mode is toggled off then on up to four times per clear for no net + // change. On the 'before' event, event.contents() holds the node we + // are switching TO while viewNode() is still the OLD view; if both map + // to the same edit mode, skip the deactivation. The matching 'after' + // event then finds the mode already active and no-ops in activateEntry. + // + if (!on) + { + try + { + let toName = event.contents(); + if (toName != "") + { + let toType = nodeType(toName), + toEditMode = if toType.substr(0,2) == "RV" + then toType.substr(2,0) + "_edit_mode" + else ""; + if (toEditMode == name) + { + return; + } + } + } + catch (exception exc) { let ignore = exc; } + } + + let entry = findModeEntry(name); if (entry neq nil) { diff --git a/src/lib/app/py_rvui/rv/qtutils.py b/src/lib/app/py_rvui/rv/qtutils.py index f1fcfc97d..ec72da90c 100644 --- a/src/lib/app/py_rvui/rv/qtutils.py +++ b/src/lib/app/py_rvui/rv/qtutils.py @@ -36,12 +36,16 @@ def sessionWindow(): def sessionGLView(): """ - Returns the QOpenGLWidget for the current RV session GL view. + Returns the QWidget hosting the current RV session GL view. + + Note: as of the native-GL-window change (Direction C) the session view is a + plain QWidget host (it embeds the GL surface via createWindowContainer), no + longer a QOpenGLWidget. """ rvPyLongPtr = rv.commands.sessionGLView() if rvPyLongPtr is not None: - return wrapInstance(rvPyLongPtr, QOpenGLWidget) + return wrapInstance(rvPyLongPtr, QWidget) else: return None diff --git a/src/lib/ip/IPCore/Session.cpp b/src/lib/ip/IPCore/Session.cpp index 6419e3845..666bf2e21 100644 --- a/src/lib/ip/IPCore/Session.cpp +++ b/src/lib/ip/IPCore/Session.cpp @@ -2261,20 +2261,18 @@ namespace IPCore void Session::askForRedraw(bool force) { if (m_beingDeleted) + { return; + } m_wantsRedraw = true; - if (!isPlaying()) - { - if (!m_stopTimer.isRunning()) - { - m_stopTimer.stop(); - m_stopTimer.start(); - App()->startPlay(this); - send(startPlayMessage()); - } - } + // + // Non-playback redraws only need a single frame update via d->redraw() + // below. Starting the 2s m_stopTimer/startPlay settle window kept + // isUpdating() true and starved Qt6 QWebChannel delivery during spam + // clicks, without providing further redraw work once wantsRedraw clears. + // if (!isRendering() || force) { @@ -3223,7 +3221,24 @@ namespace IPCore // This is the "heartbeat" entry point in the session // - if (force || isUpdating() || m_rangeDirty) + if (m_stopTimer.isRunning() && !isPlaying()) + { + if (!m_wantsRedraw && !isEvalRunning() && !isBuffering() && !currentStateIsError()) + { + stopCompletely(); + } + else if (m_stopTimer.elapsed() > 2.0 && !isEvalRunning() && !isBuffering() && !currentStateIsError()) + { + stopCompletely(); + } + } + + // + // During non-playback settle, only redraw when something actually + // requested it. The old isUpdating() guard repainted at heartbeat + // rate for ~2s even with wantsRedraw=false, starving Qt6 QWebChannel. + // + if (force || isPlaying() || m_wantsRedraw || m_rangeDirty) { if (multipleVideoDevices() && outputVideoDevice()->willBlockOnTransfer() && (outputVideoDevice()->capabilities() & TwkApp::VideoDevice::ASyncReadBack))