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))