diff --git a/src/bin/apps/rv/main.cpp b/src/bin/apps/rv/main.cpp index 8b2a760fc..ef9bb5c11 100644 --- a/src/bin/apps/rv/main.cpp +++ b/src/bin/apps/rv/main.cpp @@ -376,25 +376,10 @@ int utf8Main(int argc, char* argv[]) // without an explicit, ordering-sensitive setShareContext() call. Must be // set before the QApplication is constructed. // - // The Vulkan presentation path depends on this as well: there is no GLView - // to chain from, so the offscreen presentation context joins this global - // group instead (see QTVulkanVideoDevice::ensureGLContext). Otherwise FTGL - // font-atlas glyph uploads land in a context where the atlas texture has no - // storage. + // The Vulkan path also relies on it: QTVulkanVideoDevice::ensureGLContext() + // has no GLView to share with, so it joins this global group. QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - // Render Qt Quick through OpenGL, matching the graphics API RV's windows - // composite with. - // - // Qt Quick's RHI backend defaults to Direct3D 11 on Windows. RV's top-level - // windows composite with OpenGL (GLView realizes the window up front, before - // any render-to-texture widget joins the tree), and a QQuickWidget cannot - // obtain a QRhi from a window using a different API. Without this, anything - // Quick-based inside an RV window -- most visibly a QWebEngineView, whose - // page is rendered by a QQuickWidget -- silently draws nothing and logs "The - // top-level window is not using the expected graphics API for composition" - // followed by "Attempted to render scene with no rhi". - // #ifdef PLATFORM_WINDOWS // Put Qt Quick on the same graphics API RV's windows composite with. // diff --git a/src/lib/app/RvApp/Options.cpp b/src/lib/app/RvApp/Options.cpp index ed1696b24..9c6751733 100644 --- a/src/lib/app/RvApp/Options.cpp +++ b/src/lib/app/RvApp/Options.cpp @@ -71,7 +71,6 @@ namespace Rv else if (name == "threads") stl_ext::thread_group::debug_all(true); else if (name == "gpu") - // Enables GLView format baseline, ImageRenderer GL reporting, and Vulkan presentation/interop diagnostics. ImageRenderer::debugGpu(true); else if (name == "audio") { diff --git a/src/lib/app/RvCommon/CMakeLists.txt b/src/lib/app/RvCommon/CMakeLists.txt index 163f84cb7..36babe6a4 100644 --- a/src/lib/app/RvCommon/CMakeLists.txt +++ b/src/lib/app/RvCommon/CMakeLists.txt @@ -89,7 +89,18 @@ IF(RV_TARGET_LINUX MESSAGE(FATAL_ERROR "Vulkan::Vulkan target is missing; cmake/dependencies/vulkan.cmake should provide it on Linux and Windows") ENDIF() MESSAGE(STATUS "Vulkan: using managed Vulkan ${RV_DEPS_VULKAN_VERSION}") - LIST(APPEND _sources VulkanView.cpp QTVulkanVideoDevice.cpp RvCommon/VulkanView.h RvCommon/QTVulkanVideoDevice.h) + LIST( + APPEND + _sources + VulkanView.cpp + VulkanWindow.cpp + QTVulkanVideoDevice.cpp + VulkanDesktopVideoDevice.cpp + RvCommon/VulkanView.h + RvCommon/VulkanWindow.h + RvCommon/QTVulkanVideoDevice.h + RvCommon/VulkanDesktopVideoDevice.h + ) ENDIF() FILE(GLOB _ui_sources ui/*.ui) @@ -164,7 +175,7 @@ IF(NOT (RV_TARGET_LINUX OR RV_TARGET_WINDOWS) ) - LIST(REMOVE_ITEM _files_to_moc "RvCommon/VulkanView.h" "RvCommon/QTVulkanVideoDevice.h") + LIST(REMOVE_ITEM _files_to_moc "RvCommon/VulkanView.h" "RvCommon/VulkanWindow.h" "RvCommon/QTVulkanVideoDevice.h" "RvCommon/VulkanDesktopVideoDevice.h") ENDIF() FOREACH( diff --git a/src/lib/app/RvCommon/DesktopVideoDevice.cpp b/src/lib/app/RvCommon/DesktopVideoDevice.cpp index 3b159e710..f0086539b 100644 --- a/src/lib/app/RvCommon/DesktopVideoDevice.cpp +++ b/src/lib/app/RvCommon/DesktopVideoDevice.cpp @@ -24,8 +24,19 @@ #include #include +#include + +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) +#include +#include +#endif + #include #include +#include + +#include +#include // #define DEBUG_NO_FULLSCREEN @@ -74,21 +85,20 @@ namespace Rv void DesktopVideoDevice::redraw() const { - if (m_view) + if (m_view && m_view->glWindow()) { ScopedLock lock(m_mutex); - QSize s = m_view->size(); - m_view->update(); + m_view->glWindow()->update(); } } void DesktopVideoDevice::redrawImmediately() const { - if (m_view && m_view->isVisible()) + if (m_view && m_view->glWindow() && m_view->isVisible()) { ScopedLock lock(m_mutex); TWK_GLDEBUG; - m_view->update(); + m_view->glWindow()->update(); TWK_GLDEBUG; } else @@ -113,29 +123,29 @@ namespace Rv // later. GLint svFboId = m_viewDevice->fboID(); if (svFboId == 0) + { + // Report the stall once, not once per frame. + if (!m_transferStalled) + { + m_transferStalled = true; + cerr << "WARNING: DesktopVideoDevice: '" << name() << "' present stalled: the GL surface has no backing FBO yet" + << " (fboID()==0), so nothing can be composited into it. The output will hold its last frame until the" + << " surface paints." << endl; + } return; + } + + m_transferStalled = false; // Switch to the ScreenView's OpenGL context. m_viewDevice->makeCurrent(); // calls screenview's makeCurrent, sets the // font current, etc etc. - // Next, because we can't blit from FBOs belonging to different - // contexts, check to see if we already have an clone of the source FBO - // associated to the view's context. - GLFBO* svSourceFbo = m_fboMap[sourceFbo]; + // FBOs can't be blitted across contexts, so use this context's clone. + GLFBO* svSourceFbo = cloneForSource(sourceFbo); if (!svSourceFbo) { - // We don't yet have a clone of the source FBO living in the context - // of the ScreenView. Therefore, we now create this new clone FBO - // with the dimensions/format as the source. - // Afterwards, associate the source FBO's color attachment to the - // clone's color attachment, because color attachments *can* be - // shared for the blit operation - svSourceFbo = new GLFBO(sourceFbo->width(), sourceFbo->height(), sourceFbo->primaryColorFormat()); - - svSourceFbo->attachColorTexture(sourceFbo->colorTarget(0), - sourceFbo->colorID(0)); // PB: What's colorId ? - m_fboMap[sourceFbo] = svSourceFbo; + return; } // Finally, create a temporary GLFBO with the ID of the ScreenView's FBO @@ -157,13 +167,30 @@ namespace Rv { TWK_GLDEBUG; - QSurfaceFormat fmt = shareDevice()->glSurfaceFormat(); + // + // No share device on the Vulkan main-view path; AA_ShareOpenGLContexts + // still puts every context in one share group. + // + const QTGLVideoDevice* share = shareDevice(); + + // + // QOpenGLWindow creates its context lazily, so realize the share + // device's context (e.g. right after a backend swap) before copying + // its format and share context. + // + if (share) + { + share->makeCurrent(); + } + + QSurfaceFormat fmt = share ? share->glSurfaceFormat() : QSurfaceFormat::defaultFormat(); fmt.setSwapInterval(m_vsync ? 1 : 0); - ScreenView* vw = new ScreenView(fmt, 0, shareDevice()->glShareContext(), Qt::Window); + ScreenView* vw = new ScreenView(fmt, 0, share ? share->glShareContext() : nullptr, Qt::Window); setViewWidget(vw); - QTGLVideoDevice* vd = new QTGLVideoDevice(0, "local view", vw); + // GL surface is the embedded window; the container handles events, as in GLView. + QTGLVideoDevice* vd = new QTGLVideoDevice(0, "local view", vw->glWindow(), vw); setViewDevice(vd); QRect g = screenGeometry(); @@ -180,23 +207,34 @@ namespace Rv viewWidget()->setGeometry(g); viewWidget()->show(); - // QCoreApplication::processEvents(); // force the window to - // show. m_share->makeCurrent(); + // + // No makeCurrent() here: a PartialUpdateBlit window's backing FBO only + // exists after the first paint, and binding it earlier crashes in Qt. + // TWK_GLDEBUG; } void DesktopVideoDevice::close() { - delete m_view; + releaseFBOClones(); + + // The device first: its font textures live in the view's context. delete m_viewDevice; + delete m_view; delete m_translator; m_view = 0; m_viewDevice = 0; m_translator = 0; + + // The current context was just destroyed; restore the main view's for later GL teardown. + if (m_share) + { + m_share->makeCurrent(); + } } - void DesktopVideoDevice::setViewWidget(QOpenGLWidget* widget) + void DesktopVideoDevice::setViewWidget(ScreenView* widget) { m_view = widget; m_translator = new QTTranslator(this, m_view); @@ -204,8 +242,26 @@ namespace Rv void DesktopVideoDevice::makeCurrent() const { - if (m_view) - m_view->makeCurrent(); + if (!m_viewDevice) + { + return; + } + + // + // Binding before the first paint crashes in Qt (no backing FBO yet). + // A destroyed surface is allowed through: makeCurrent() then uses the + // offscreen teardown surface, which releaseFBOClones() relies on. + // + const QTGLVideoDevice* glViewDevice = dynamic_cast(m_viewDevice); + const QOpenGLWindow* surfaceWindow = glViewDevice ? glViewDevice->window() : nullptr; + const bool surfaceGone = surfaceWindow && !surfaceWindow->handle(); + + if (!surfaceGone && m_viewDevice->fboID() == 0) + { + return; + } + + m_viewDevice->makeCurrent(); } void DesktopVideoDevice::setupModelviewAndProjection(float w, float h, GLPipeline* glPipeline) const @@ -285,26 +341,24 @@ namespace Rv // ScopedLock lock(m_mutex); + + // Same readiness gate as transfer(). + if (m_viewDevice->fboID() == 0) + { + return; + } + m_viewDevice->makeCurrent(); TWK_GLDEBUG; const float w = m_viewDevice->width(); const float h = m_viewDevice->height(); - GLFBO* local_fbo1 = m_fboMap[fbo1]; - GLFBO* local_fbo2 = m_fboMap[fbo2]; - - if (!local_fbo1) - { - local_fbo1 = new GLFBO(fbo1->width(), fbo1->height(), fbo1->primaryColorFormat()); - local_fbo1->attachColorTexture(fbo1->colorTarget(0), fbo1->colorID(0)); - m_fboMap[fbo1] = local_fbo1; - } + GLFBO* local_fbo1 = cloneForSource(fbo1); + GLFBO* local_fbo2 = cloneForSource(fbo2); - if (!local_fbo2) + if (!local_fbo1 || !local_fbo2) { - local_fbo2 = new GLFBO(fbo2->width(), fbo2->height(), fbo2->primaryColorFormat()); - local_fbo2->attachColorTexture(fbo2->colorTarget(0), fbo2->colorID(0)); - m_fboMap[fbo2] = local_fbo2; + return; } const GLFBO *leftFBO = local_fbo1, *rightFBO = local_fbo2; @@ -519,10 +573,98 @@ namespace Rv bool DesktopVideoDevice::isDualStereo() const { return isStereo(); } - void DesktopVideoDevice::unbind() const + TwkGLF::GLFBO* DesktopVideoDevice::cloneForSource(const GLFBO* sourceFbo) const + { + if (!sourceFbo) + { + return nullptr; + } + + FBOMap::iterator i = m_fboMap.find(sourceFbo); + + if (i != m_fboMap.end()) + { + GLFBO* cached = i->second; + + // The renderer reallocates FBOs, so the same address can be a different FBO. + const bool stillMatches = cached && cached->colorID(0) == sourceFbo->colorID(0) + && cached->colorTarget(0) == sourceFbo->colorTarget(0) && cached->width() == sourceFbo->width() + && cached->height() == sourceFbo->height(); + + if (stillMatches) + { + return cached; + } + + delete cached; + m_fboMap.erase(i); + } + + // + // A dead source texture leaves the clone incomplete; never cache one, + // or every later frame would blit from it. + // + GLFBO* clone = new GLFBO(sourceFbo->width(), sourceFbo->height(), sourceFbo->primaryColorFormat()); + + clone->attachColorTexture(sourceFbo->colorTarget(0), sourceFbo->colorID(0)); + + if (!clone->isComplete()) + { + // Report once per source texture; this runs every frame. + const GLuint badTex = sourceFbo->colorID(0); + if (m_reportedBadSourceTex != badTex) + { + m_reportedBadSourceTex = badTex; + + cerr << "WARNING: DesktopVideoDevice: '" << name() << "' could not mirror the renderer's " << sourceFbo->width() << "x" + << sourceFbo->height() << " FBO (source colour texture " << badTex << " is not usable in this context); skipping" + << " until it changes." << endl; + + // Distinguish a dead texture (glIsTexture) from a share-group mismatch. + const QOpenGLContext* cur = QOpenGLContext::currentContext(); + const QTGLVideoDevice* share = shareDevice(); + const QOpenGLContext* shareCtx = share ? share->glShareContext() : nullptr; + + cerr << "WARNING: DesktopVideoDevice: glIsTexture(" << badTex << ")=" << (glIsTexture(badTex) ? "true" : "false") + << " srcTarget=0x" << hex << sourceFbo->colorTarget(0) << " srcFormat=0x" << sourceFbo->primaryColorFormat() << dec + << " (GL_TEXTURE_2D=0x" << hex << GL_TEXTURE_2D << " GL_TEXTURE_RECTANGLE_ARB=0x" << GL_TEXTURE_RECTANGLE_ARB << dec + << ")" << endl; + cerr << "WARNING: DesktopVideoDevice: currentContext=" << static_cast(cur) + << " shareGroup=" << static_cast(cur ? cur->shareGroup() : nullptr) + << " rendererShareContext=" << static_cast(shareCtx) + << " shareGroup=" << static_cast(shareCtx ? shareCtx->shareGroup() : nullptr) + << " globalShare=" << static_cast(QOpenGLContext::globalShareContext()) << endl; + } + + delete clone; + return nullptr; + } + + m_reportedBadSourceTex = 0; + + m_fboMap[sourceFbo] = clone; + + return clone; + } + + void DesktopVideoDevice::releaseFBOClones() const { ScopedLock lock(m_mutex); + if (m_fboMap.empty()) + { + return; + } + + // Through the virtual so each subclass binds its own surface. + makeCurrent(); + + if (!QOpenGLContext::currentContext()) + { + cerr << "WARNING: DesktopVideoDevice: '" << name() << "' released " << m_fboMap.size() + << " FBO clone(s) WITHOUT a current context -- the GL handles leaked." << endl; + } + for (FBOMap::iterator i = m_fboMap.begin(); i != m_fboMap.end(); ++i) { delete i->second; @@ -531,6 +673,8 @@ namespace Rv m_fboMap.clear(); } + void DesktopVideoDevice::unbind() const { releaseFBOClones(); } + size_t DesktopVideoDevice::numVideoFormats() const { return m_videoFormats.size(); } DesktopVideoDevice::VideoFormat DesktopVideoDevice::videoFormatAtIndex(size_t i) const @@ -747,35 +891,64 @@ namespace Rv void DesktopVideoDevice::sortVideoFormatsByWidth() { sort(m_videoFormats.begin(), m_videoFormats.end(), widthSort); } - DesktopVideoDevice::ScreenView::ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLContext* glShareContext, - Qt::WindowFlags flags) - : QOpenGLWidget(parent, flags) + DesktopVideoDevice::ScreenWindow::ScreenWindow(const QSurfaceFormat& fmt, QOpenGLContext* glShareContext) + : QOpenGLWindow(glShareContext, QOpenGLWindow::PartialUpdateBlit) + , m_glShareContext(glShareContext) { - m_glShareContext = glShareContext; setFormat(fmt); - - // Important: set PartialUpdate, because otherwise - // before every call to paintGL Qt will call glClear(), - // thereby erasing the FBO we just transferred pixels to. - setUpdateBehavior(QOpenGLWidget::PartialUpdate); } - void DesktopVideoDevice::ScreenView::initializeGL() + void DesktopVideoDevice::ScreenWindow::initializeGL() { - QOpenGLWidget::initializeGL(); + QOpenGLWindow::initializeGL(); - if (m_glShareContext && context() && context()->isValid()) + // + // Verify the share group; a mismatch only shows as a black display. + // setShareContext() is useless here: the context is already created. + // + const QOpenGLContext* ours = context(); + const QOpenGLContext* global = QOpenGLContext::globalShareContext(); + const QOpenGLContext* wanted = m_glShareContext ? m_glShareContext : global; + + if (!wanted) + { + cerr << "ERROR: DesktopVideoDevice::ScreenWindow: no context to share with;" + << " Qt::AA_ShareOpenGLContexts must be set before the QApplication is created." << endl; + } + else if (ours && ours->shareGroup() != wanted->shareGroup()) { - context()->setShareContext(m_glShareContext); + cerr << "ERROR: DesktopVideoDevice::ScreenWindow: GL context did not join the renderer's share group" + << " (ours=" << static_cast(ours->shareGroup()) + << " wanted=" << static_cast(wanted->shareGroup()) + << "); the renderer's textures cannot be reached from this context, so this" + << " presentation output would render black." << endl; } } - void DesktopVideoDevice::ScreenView::paintGL() + void DesktopVideoDevice::ScreenWindow::paintGL() { - // This method is explicitely empty because this widget's FBO is + // This method is explicitely empty because this window's FBO is // written to by the transfer/transfer2() method } + DesktopVideoDevice::ScreenView::ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLContext* glShareContext, + Qt::WindowFlags flags) + : QWidget(parent, flags) + { + m_glWindow = new ScreenWindow(fmt, glShareContext); + + // A plain QWidget container keeps this top-level off the OpenGL RHI backing store. + m_container = QWidget::createWindowContainer(m_glWindow, this); + + // A QOpenGLWindow has no GL context until its platform surface exists. + m_glWindow->create(); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_container); + } + //---------------------------------------------------------------------- bool DesktopVideoDevice::isOpen() const { return m_view != 0; } @@ -844,33 +1017,37 @@ namespace Rv // XXX The following steps are not Unicode safe // + m_colorProfile = ColorProfile(); + // Get the context for this screen const QList screens = QGuiApplication::screens(); // Ensure the screen index is valid. if (m_screen < 0 || m_screen >= screens.size()) { - m_colorProfile = ColorProfile(); return m_colorProfile; } - QScreen* targetScreen = screens[m_screen]; + const QScreen* targetScreen = screens[m_screen]; QWindow* windowOnTargetScreen = nullptr; - const QList windows = QGuiApplication::topLevelWindows(); - // Check all windows to find one on the target screen. - for (QWindow* window : windows) + // + // Only already-realized windows: winId() would create a platform window + // for QQuickWidget's offscreen QQuickWindow, which asserts in Qt + // ("Do not call create() on offscreenWindow", qquickwidget.cpp). + // + for (QWindow* window : QGuiApplication::topLevelWindows()) { - if (window->screen() == targetScreen) + if (window->handle() && window->screen() == targetScreen) { windowOnTargetScreen = window; + break; } } if (!windowOnTargetScreen) { // Return empty profile if no window is found on the screen. - m_colorProfile = ColorProfile(); return m_colorProfile; } @@ -880,46 +1057,72 @@ namespace Rv if (hdc) { // Look for the profile path - unsigned long pathLen; + DWORD pathLen = 0; GetICMProfile(hdc, &pathLen, NULL); - char* path = new char[pathLen]; - if (GetICMProfile(hdc, &pathLen, path)) + std::vector path(pathLen > 0 ? pathLen : 1); + + if (pathLen > 0 && GetICMProfile(hdc, &pathLen, path.data())) { // If we found a profile lets set the type, // url, and description m_colorProfile.type = ICCProfile; - unsigned long maxLen = 2084; - char* url = new char[maxLen]; - UrlCreateFromPath(path, url, &maxLen, NULL); - m_colorProfile.url = url; - - char desc[256]; - cmsHPROFILE profile = cmsOpenProfileFromFile(path, "r"); - cmsGetProfileInfoASCII(profile, cmsInfoDescription, "en", "US", desc, 256); - m_colorProfile.description = desc; + DWORD maxLen = 2084; + std::vector url(maxLen); + if (SUCCEEDED(UrlCreateFromPath(path.data(), url.data(), &maxLen, 0))) + { + m_colorProfile.url = url.data(); + } - delete url; + // Null when the reported profile path is gone or unreadable. + if (cmsHPROFILE profile = cmsOpenProfileFromFile(path.data(), "r")) + { + std::array desc{}; + cmsGetProfileInfoASCII(profile, cmsInfoDescription, "en", "US", desc.data(), desc.size()); + m_colorProfile.description = desc.data(); + cmsCloseProfile(profile); + } } - delete path; ReleaseDC(hwnd, hdc); } - else - { - m_colorProfile = ColorProfile(); - } return m_colorProfile; } #endif + bool DesktopVideoDevice::tenBitDisplayRequested() + { + const Options& opts = Options::sharedOptions(); + return opts.dispRedBits == 10 && opts.dispGreenBits == 10 && opts.dispBlueBits == 10 && opts.dispAlphaBits == 2; + } + + bool DesktopVideoDevice::shouldUseVulkanPresentation() + { +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + // Same rule as the main view in RvDocument. + return tenBitDisplayRequested() && VulkanView::supports10BitPresentation(); +#else + return false; +#endif + } + std::vector DesktopVideoDevice::createDesktopVideoDevices(TwkApp::VideoModule* module, const QTGLVideoDevice* shareDevice) + { + return createDesktopVideoDevices(module, shareDevice, shouldUseVulkanPresentation()); + } + + std::vector DesktopVideoDevice::createDesktopVideoDevices(TwkApp::VideoModule* module, const QTGLVideoDevice* shareDevice, + bool useVulkan) { std::vector devices; +#if !defined(PLATFORM_LINUX) && !defined(PLATFORM_WINDOWS) + (void)useVulkan; +#endif + const auto screens = QGuiApplication::screens(); for (int screen = 0; screen < screens.size(); screen++) { @@ -931,7 +1134,17 @@ namespace Rv name = QString("Screen %1").arg(screen); } - DesktopVideoDevice* sd = new DesktopVideoDevice(module, name.toUtf8().constData(), screen, shareDevice); + DesktopVideoDevice* sd = nullptr; +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + if (useVulkan) + { + sd = new VulkanDesktopVideoDevice(module, name.toUtf8().constData(), screen, shareDevice); + } + else +#endif + { + sd = new DesktopVideoDevice(module, name.toUtf8().constData(), screen, shareDevice); + } devices.push_back(sd); } diff --git a/src/lib/app/RvCommon/DesktopVideoModule.cpp b/src/lib/app/RvCommon/DesktopVideoModule.cpp index d5aa56bbd..29171c4ac 100644 --- a/src/lib/app/RvCommon/DesktopVideoModule.cpp +++ b/src/lib/app/RvCommon/DesktopVideoModule.cpp @@ -8,6 +8,9 @@ #include #include #include +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) +#include +#endif #include #include #include @@ -39,6 +42,45 @@ namespace Rv DesktopVideoModule::~DesktopVideoModule() {} + bool DesktopVideoModule::rebuildDevices(const QTGLVideoDevice* shareDevice, bool targetVulkan) + { +#if !defined(PLATFORM_LINUX) && !defined(PLATFORM_WINDOWS) + targetVulkan = false; +#endif + + bool currentVulkan = false; +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + for (TwkApp::VideoDevice* device : m_devices) + { + if (dynamic_cast(device)) + { + currentVulkan = true; + break; + } + } +#endif + + if (!m_devices.empty() && currentVulkan == targetVulkan) + { + return false; + } + + // close() releases the Vulkan swapchain or GL ScreenView before the delete. + for (TwkApp::VideoDevice* device : m_devices) + { + if (device->isOpen()) + { + device->close(); + } + delete device; + } + m_devices.clear(); + + m_devices = DesktopVideoDevice::createDesktopVideoDevices(this, shareDevice, targetVulkan); + + return true; + } + string DesktopVideoModule::name() const { return "Desktop"; } void DesktopVideoModule::open() {} diff --git a/src/lib/app/RvCommon/GLView.cpp b/src/lib/app/RvCommon/GLView.cpp index a3a7bdc1a..e0b320d01 100644 --- a/src/lib/app/RvCommon/GLView.cpp +++ b/src/lib/app/RvCommon/GLView.cpp @@ -18,16 +18,36 @@ #include #include #include +#include #include #include #include #include #include +#include namespace Rv { using namespace std; + std::string glDebugEnvOrUnset(const char* name) + { + const char* value = std::getenv(name); + return value ? value : ""; + } + + std::string glDebugFormatSummary(const QSurfaceFormat& f) + { + ostringstream out; + out << "rgba " << f.redBufferSize() << " " << f.greenBufferSize() << " " << f.blueBufferSize() << " " + << (f.alphaBufferSize() <= 0 ? 0 : f.alphaBufferSize()); + out << ", depth " << f.depthBufferSize() << ", stencil " << f.stencilBufferSize(); + out << ", swapInterval " << f.swapInterval(); + out << ", stereo " << (f.stereo() ? "true" : "false"); + out << ", major.minor " << f.majorVersion() << "." << f.minorVersion(); + return out.str(); + } + GLView::GLView(QWidget* parent, QOpenGLContext* sharedContext, RvDocument* doc, bool stereo, bool vsync, bool doubleBuffer, int red, int green, int blue, int alpha, bool noResize) : QWidget(parent) @@ -389,6 +409,11 @@ namespace Rv fmt.setSwapInterval(vsync ? 1 : 0); + if (IPCore::ImageRenderer::debugGpu()) + { + cout << "INFO: GLView requested QSurfaceFormat: " << glDebugFormatSummary(fmt) << endl; + } + return fmt; } diff --git a/src/lib/app/RvCommon/GLWindow.cpp b/src/lib/app/RvCommon/GLWindow.cpp index 8c9e0317a..89583b40d 100644 --- a/src/lib/app/RvCommon/GLWindow.cpp +++ b/src/lib/app/RvCommon/GLWindow.cpp @@ -18,50 +18,35 @@ #include #include #include -#include #include +#include #include #include #include #include -#include -#include #include #include #include +#include +#include #include #include -#include namespace Rv { + + // -debug gpu frame-time accumulators, mirroring VulkanWindow's. + static unsigned int s_glDiagFrames = 0; + static double s_glDiagRenderMs = 0.0; + static double s_glDiagOutPresentMs = 0.0; + // Time between paintGL() entries; includes the implicit swap after paintGL. + static double s_glDiagLoopMs = 0.0; + static TwkUtil::Timer s_glDiagLoopTimer; + using namespace std; using namespace TwkApp; using namespace IPCore; - namespace - { -#ifdef PLATFORM_LINUX - string envOrUnset(const char* name) - { - const char* value = std::getenv(name); - return value ? value : ""; - } -#endif - - string formatSummary(const QSurfaceFormat& f) - { - ostringstream out; - out << "rgba " << f.redBufferSize() << " " << f.greenBufferSize() << " " << f.blueBufferSize() << " " - << (f.alphaBufferSize() <= 0 ? 0 : f.alphaBufferSize()); - out << ", depth " << f.depthBufferSize() << ", stencil " << f.stencilBufferSize(); - out << ", swapInterval " << f.swapInterval(); - out << ", stereo " << (f.stereo() ? "true" : "false"); - out << ", major.minor " << f.majorVersion() << "." << f.minorVersion(); - return out.str(); - } - } // namespace - 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) @@ -80,13 +65,6 @@ namespace Rv { setFormat(GLView::rvGLFormat(stereo, vsync, doubleBuffer, red, green, blue, alpha)); -#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) - if (ImageRenderer::debugGpu()) - { - cout << "INFO: GLWindow requested QSurfaceFormat: " << formatSummary(format()) << endl; - } -#endif - m_videoDevice = nullptr; // set later by the hosting GLView m_activityTimer.start(); @@ -135,32 +113,33 @@ namespace Rv QSurfaceFormat f = context()->format(); -#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + // One-shot -debug gpu baseline: requested vs negotiated format, driver and display server. static bool baselineLogged = false; if (ImageRenderer::debugGpu() && !baselineLogged) { baselineLogged = true; - QScreen* screen = this->screen(); - if (!screen) - screen = QGuiApplication::primaryScreen(); + QScreen* scr = screen(); + if (!scr) + { + scr = QGuiApplication::primaryScreen(); + } const GLubyte* glVendor = glGetString(GL_VENDOR); const GLubyte* glRenderer = glGetString(GL_RENDERER); const GLubyte* glVersion = glGetString(GL_VERSION); const GLubyte* glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION); - const QSurfaceFormat requestedFormat = format(); cout << "INFO: GLWindow runtime baseline begin" << endl; cout << "INFO: Qt platform name: " << QGuiApplication::platformName().toStdString() << endl; cout << "INFO: Qt version: " << qVersion() << endl; - cout << "INFO: Constructor-requested color bits (GLWindow args): rgba " << m_red << " " << m_green << " " << m_blue << " " - << m_alpha << endl; - cout << "INFO: GLWindow::format() (post-negotiation): " << formatSummary(requestedFormat) << endl; - cout << "INFO: Actual QOpenGLContext format: " << formatSummary(f) << endl; - if (screen) + cout << "INFO: Constructor-requested color bits: rgba " << m_red << " " << m_green << " " << m_blue << " " << m_alpha + << endl; + cout << "INFO: QOpenGLWindow::format() (post-negotiation): " << glDebugFormatSummary(format()) << endl; + cout << "INFO: Actual QOpenGLContext format: " << glDebugFormatSummary(f) << endl; + if (scr) { - cout << "INFO: Screen name: " << screen->name().toStdString() << ", depth: " << screen->depth() << endl; + cout << "INFO: Screen name: " << scr->name().toStdString() << ", depth: " << scr->depth() << endl; } else { @@ -172,14 +151,13 @@ namespace Rv cout << "INFO: GL version: " << (glVersion ? reinterpret_cast(glVersion) : "") << endl; cout << "INFO: GLSL version: " << (glslVersion ? reinterpret_cast(glslVersion) : "") << endl; #ifdef PLATFORM_LINUX - cout << "INFO: Linux display env: XDG_SESSION_TYPE=" << envOrUnset("XDG_SESSION_TYPE") - << ", WAYLAND_DISPLAY=" << envOrUnset("WAYLAND_DISPLAY") << ", DISPLAY=" << envOrUnset("DISPLAY") - << ", XDG_CURRENT_DESKTOP=" << envOrUnset("XDG_CURRENT_DESKTOP") - << ", DESKTOP_SESSION=" << envOrUnset("DESKTOP_SESSION") << endl; + cout << "INFO: Linux display env: XDG_SESSION_TYPE=" << glDebugEnvOrUnset("XDG_SESSION_TYPE") + << ", WAYLAND_DISPLAY=" << glDebugEnvOrUnset("WAYLAND_DISPLAY") << ", DISPLAY=" << glDebugEnvOrUnset("DISPLAY") + << ", XDG_CURRENT_DESKTOP=" << glDebugEnvOrUnset("XDG_CURRENT_DESKTOP") + << ", DESKTOP_SESSION=" << glDebugEnvOrUnset("DESKTOP_SESSION") << endl; #endif cout << "INFO: GLWindow runtime baseline end" << endl; } -#endif #ifndef PLATFORM_DARWIN if (f.redBufferSize() != m_red && m_red != 0) @@ -247,6 +225,15 @@ namespace Rv } } + if (IPCore::ImageRenderer::debugGpu()) + { + if (s_glDiagLoopTimer.isRunning()) + { + s_glDiagLoopMs += s_glDiagLoopTimer.elapsed() * 1000.0; + } + s_glDiagLoopTimer.start(); + } + if (m_doc && session && m_videoDevice) { m_videoDevice->makeCurrent(); @@ -271,7 +258,19 @@ namespace Rv m_videoDevice->setAbsolutePosition(x, y); TWK_GLDEBUG; + const bool diagTiming = IPCore::ImageRenderer::debugGpu(); + Timer diagTimer; + if (diagTiming) + { + diagTimer.start(); + } + session->render(); + + if (diagTiming) + { + s_glDiagRenderMs += diagTimer.elapsed() * 1000.0; + } TWK_GLDEBUG; m_firstPaintCompleted = true; @@ -300,14 +299,45 @@ namespace Rv // If a separate output device is presenting, sync it. The control // (window) surface presents itself: QOpenGLWindow swaps automatically // after paintGL returns. + const bool diagPresent = IPCore::ImageRenderer::debugGpu(); + Timer diagPresentTimer; + if (session->outputVideoDevice() != m_videoDevice) { + if (diagPresent) + { + diagPresentTimer.start(); + } + session->outputVideoDevice()->syncBuffers(); + + if (diagPresent) + { + s_glDiagOutPresentMs += diagPresentTimer.elapsed() * 1000.0; + } } session->addSyncSample(); session->postRender(); + // No mainPresent term: QOpenGLWindow swaps after paintGL returns. + if (IPCore::ImageRenderer::debugGpu()) + { + if (++s_glDiagFrames >= 60) + { + const double n = double(s_glDiagFrames); + const double loopMs = s_glDiagLoopMs / n; + cout << "INFO: GLWindow frame avg over " << s_glDiagFrames << ": session->render()=" << (s_glDiagRenderMs / n) + << "ms outputPresent=" << (s_glDiagOutPresentMs / n) + << "ms total=" << ((s_glDiagRenderMs + s_glDiagOutPresentMs) / n) << "ms frameInterval=" << loopMs << "ms (" + << (loopMs > 0.0 ? 1000.0 / loopMs : 0.0) << " fps)" << endl; + s_glDiagFrames = 0; + s_glDiagRenderMs = 0.0; + s_glDiagOutPresentMs = 0.0; + s_glDiagLoopMs = 0.0; + } + } + m_eventProcessingTimer.start(); TWK_GLDEBUG; diff --git a/src/lib/app/RvCommon/MuUICommands.cpp b/src/lib/app/RvCommon/MuUICommands.cpp index f90e4018a..95ac15662 100644 --- a/src/lib/app/RvCommon/MuUICommands.cpp +++ b/src/lib/app/RvCommon/MuUICommands.cpp @@ -9,7 +9,11 @@ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif +#endif + #include + +#ifdef PLATFORM_WINDOWS #include #include #include @@ -38,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +69,7 @@ #include #include #include +#include #include #include // WINDOWS NEEDS THIS LAST // #include @@ -383,9 +389,6 @@ namespace Rv MuLangContext* c = static_cast(p->context()); Session* s = Session::currentSession(); RvDocument* doc = reinterpret_cast(s->opaquePointer()); - QWidget* w = doc->view(); - - GLView* glview = dynamic_cast(w); Mu::Vector4f v; v[0] = 0; @@ -393,7 +396,7 @@ namespace Rv v[2] = 0; v[3] = 0; - if (glview != NULL) + if (TwkGLF::GLVideoDevice* device = doc->viewVideoDevice()) { float x = NODE_ARG(0, float); float y = NODE_ARG(1, float); @@ -401,17 +404,15 @@ namespace Rv int ix = (int)(x + 0.5f); int iy = (int)(y + 0.5f); - QImage image = glview->readPixels(ix, iy, 1, 1); - - if ((image.width() > 0) && (image.height() > 0)) + if (ix >= 0 && iy >= 0 && static_cast(ix) < device->width() && static_cast(iy) < device->height()) { - QRgb rgba = image.pixel(0, 0); - QColor qc(rgba); - - v[0] = qc.redF(); - v[1] = qc.greenF(); - v[2] = qc.blueF(); - v[3] = qc.alphaF(); + device->makeCurrent(); + std::array rgba{}; + glReadPixels(ix, iy, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + v[0] = static_cast(rgba[0]) / 255.0f; + v[1] = static_cast(rgba[1]) / 255.0f; + v[2] = static_cast(rgba[2]) / 255.0f; + v[3] = static_cast(rgba[3]) / 255.0f; } } @@ -1731,10 +1732,6 @@ namespace Rv MuLangContext* c = static_cast(p->context()); Session* s = Session::currentSession(); RvDocument* doc = reinterpret_cast(s->opaquePointer()); - // Use the neutral view-widget accessor: doc->view() is the GL-only - // m_glView, which is null on the Vulkan/Metal presentation path. Wrapping - // a null QWidget* here makes the Mu side (e.g. the Session Manager event - // filter) dereference null and crash. QWidget* w = doc->viewWidget(); const QWidgetType* type = c->findSymbolOfTypeByQualifiedName(c->internName("qt.QWidget"), false); diff --git a/src/lib/app/RvCommon/QTGLVideoDevice.cpp b/src/lib/app/RvCommon/QTGLVideoDevice.cpp index 6e652a193..3fc90d160 100644 --- a/src/lib/app/RvCommon/QTGLVideoDevice.cpp +++ b/src/lib/app/RvCommon/QTGLVideoDevice.cpp @@ -19,8 +19,12 @@ #include #include +#include #include +#include +#include + namespace Rv { using namespace std; @@ -104,20 +108,26 @@ namespace Rv void QTGLVideoDevice::makeCurrent() const { - if (m_window) + // QOpenGLWindow creates its context lazily, and only if the platform surface exists. + if (m_window && m_window->handle()) { - // 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; + + // Needs a live context to copy the format from. + if (!m_teardownSurface && m_window->context()) { - m_window->makeCurrent(); - TWK_GLDEBUG; + m_teardownSurface = std::make_unique(); + m_teardownSurface->setFormat(m_window->context()->format()); + m_teardownSurface->create(); + } - GLint surfaceFBO = m_window->defaultFramebufferObject(); - if (surfaceFBO != 0) - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, surfaceFBO); - 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()) { @@ -126,9 +136,31 @@ namespace Rv GLint widgetFBO = m_view->defaultFramebufferObject(); if (widgetFBO != 0) + { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, widgetFBO); + } + TWK_GLDEBUG; + } + else if (m_window && m_window->context() && m_teardownSurface && m_teardownSurface->isValid() + && m_window->context()->makeCurrent(m_teardownSurface.get())) + { + // Surface gone, context alive: GL deletion only needs a current context. TWK_GLDEBUG; } + else + { + // Callers assume a current context afterwards, so report the failure once. + static std::atomic reported{false}; + if (!reported.exchange(true)) + { + cerr << "ERROR: QTGLVideoDevice::makeCurrent: '" << name() << "' cannot make a context current (window=" + << (!m_window ? "destroyed" : (m_window->handle() ? "alive" : "no surface")) + << " widget=" << (m_view ? "alive" : "null") + << " currentContext=" << (QOpenGLContext::currentContext() ? "yes" : "none") + << " ownContext=" << (m_window && m_window->context() ? "alive" : "null") + << "); the caller's GL work has no current context" << endl; + } + } if (!isWorkerDevice()) GLVideoDevice::makeCurrent(); @@ -196,9 +228,29 @@ namespace Rv if (m_view) { - // redraw() is backend-agnostic; m_view may be null when a non-GL - // backend (Vulkan/Metal) is active, so m_view->update() is unsafe. - redraw(); + if (m_view->isVisible()) + { +#ifdef PLATFORM_DARWIN + // Make sure that the QGLWidget gets redrawn by updateGL() even + // when completely overlapped by another window. + // Note that on macOS, Qt correctly detects when the QGLWidget + // is completely overlapped by another window and in which case + // resets the Qt::WA_Mapped attribute. This will prevent the + // GLView::paintGL() operation from being called by + // m_view->updateGL(), which will result in automatically + // interrupting any video playback that might be in progress + // while the RV window is completely overlapped. This is an + // undesirable behaviour during a review session, especially if + // an external video output device is used. + m_view->setAttribute(Qt::WA_Mapped); +#endif + + m_view->update(); + } + else + { + redraw(); + } } } diff --git a/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp b/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp index 78ccb38b4..28eed23e1 100644 --- a/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp +++ b/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -28,6 +28,7 @@ #include #include #include +#include #ifdef PLATFORM_WINDOWS // WIN32_LEAN_AND_MEAN prevents from including the legacy // , which otherwise collides with the already @@ -86,34 +87,30 @@ #endif #ifdef PLATFORM_WINDOWS -// The bundled GLEW under src/pub/glew (version 2.3.0) does not declare the -// EXT_memory_object / EXT_semaphore (or their Win32 companions) entry points. -// The Linux build uses a newer managed GLEW that does, so the Linux call sites -// can resolve the symbols at link time. On Windows we declare the function -// pointer typedefs locally and resolve them at first use via wglGetProcAddress; -// if any are missing the GPU interop path is disabled and VulkanView falls -// back to its CPU pack-and-upload presentation path. -typedef void(GLAPIENTRY* PFNGLCREATEMEMORYOBJECTSEXTPROC_RV)(GLsizei n, GLuint* memoryObjects); -typedef void(GLAPIENTRY* PFNGLDELETEMEMORYOBJECTSEXTPROC_RV)(GLsizei n, const GLuint* memoryObjects); -typedef void(GLAPIENTRY* PFNGLTEXSTORAGEMEM2DEXTPROC_RV)(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, +// The bundled src/pub/glew does not declare the EXT_memory_object / +// EXT_semaphore (+ _win32) entry points, so resolve them via wglGetProcAddress. +// If any are missing, presentation falls back to the CPU path. +using PFNGLCREATEMEMORYOBJECTSEXTPROC_RV = void(GLAPIENTRY*)(GLsizei n, GLuint* memoryObjects); +using PFNGLDELETEMEMORYOBJECTSEXTPROC_RV = void(GLAPIENTRY*)(GLsizei n, const GLuint* memoryObjects); +using PFNGLMEMORYOBJECTPARAMETERIVEXTPROC_RV = void(GLAPIENTRY*)(GLuint memoryObject, GLenum pname, const GLint* params); +using PFNGLTEXSTORAGEMEM2DEXTPROC_RV = void(GLAPIENTRY*)(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height, GLuint memory, GLuint64 offset); -typedef void(GLAPIENTRY* PFNGLIMPORTMEMORYWIN32HANDLEEXTPROC_RV)(GLuint memory, GLuint64 size, GLenum handleType, void* handle); -typedef void(GLAPIENTRY* PFNGLMEMORYOBJECTPARAMETERIVEXTPROC_RV)(GLuint memoryObject, GLenum pname, const GLint* params); -typedef void(GLAPIENTRY* PFNGLGENSEMAPHORESEXTPROC_RV)(GLsizei n, GLuint* semaphores); -typedef void(GLAPIENTRY* PFNGLDELETESEMAPHORESEXTPROC_RV)(GLsizei n, const GLuint* semaphores); -typedef void(GLAPIENTRY* PFNGLIMPORTSEMAPHOREWIN32HANDLEEXTPROC_RV)(GLuint semaphore, GLenum handleType, void* handle); -typedef void(GLAPIENTRY* PFNGLWAITSEMAPHOREEXTPROC_RV)(GLuint semaphore, GLuint numBufferBarriers, const GLuint* buffers, +using PFNGLIMPORTMEMORYWIN32HANDLEEXTPROC_RV = void(GLAPIENTRY*)(GLuint memory, GLuint64 size, GLenum handleType, void* handle); +using PFNGLGENSEMAPHORESEXTPROC_RV = void(GLAPIENTRY*)(GLsizei n, GLuint* semaphores); +using PFNGLDELETESEMAPHORESEXTPROC_RV = void(GLAPIENTRY*)(GLsizei n, const GLuint* semaphores); +using PFNGLIMPORTSEMAPHOREWIN32HANDLEEXTPROC_RV = void(GLAPIENTRY*)(GLuint semaphore, GLenum handleType, void* handle); +using PFNGLWAITSEMAPHOREEXTPROC_RV = void(GLAPIENTRY*)(GLuint semaphore, GLuint numBufferBarriers, const GLuint* buffers, GLuint numTextureBarriers, const GLuint* textures, const GLenum* dstLayouts); -typedef void(GLAPIENTRY* PFNGLSIGNALSEMAPHOREEXTPROC_RV)(GLuint semaphore, GLuint numBufferBarriers, const GLuint* buffers, +using PFNGLSIGNALSEMAPHOREEXTPROC_RV = void(GLAPIENTRY*)(GLuint semaphore, GLuint numBufferBarriers, const GLuint* buffers, GLuint numTextureBarriers, const GLuint* textures, const GLenum* srcLayouts); namespace { PFNGLCREATEMEMORYOBJECTSEXTPROC_RV g_glCreateMemoryObjectsEXT = nullptr; PFNGLDELETEMEMORYOBJECTSEXTPROC_RV g_glDeleteMemoryObjectsEXT = nullptr; + PFNGLMEMORYOBJECTPARAMETERIVEXTPROC_RV g_glMemoryObjectParameterivEXT = nullptr; PFNGLTEXSTORAGEMEM2DEXTPROC_RV g_glTexStorageMem2DEXT = nullptr; PFNGLIMPORTMEMORYWIN32HANDLEEXTPROC_RV g_glImportMemoryWin32HandleEXT = nullptr; - PFNGLMEMORYOBJECTPARAMETERIVEXTPROC_RV g_glMemoryObjectParameterivEXT = nullptr; PFNGLGENSEMAPHORESEXTPROC_RV g_glGenSemaphoresEXT = nullptr; PFNGLDELETESEMAPHORESEXTPROC_RV g_glDeleteSemaphoresEXT = nullptr; PFNGLIMPORTSEMAPHOREWIN32HANDLEEXTPROC_RV g_glImportSemaphoreWin32HandleEXT = nullptr; @@ -127,16 +124,18 @@ namespace bool loadGLInteropExtensions() { if (g_glInteropProbed) + { return g_glInteropAvailable; + } g_glInteropProbed = true; g_glCreateMemoryObjectsEXT = reinterpret_cast(wglGetProcAddress("glCreateMemoryObjectsEXT")); g_glDeleteMemoryObjectsEXT = reinterpret_cast(wglGetProcAddress("glDeleteMemoryObjectsEXT")); + g_glMemoryObjectParameterivEXT = + reinterpret_cast(wglGetProcAddress("glMemoryObjectParameterivEXT")); g_glTexStorageMem2DEXT = reinterpret_cast(wglGetProcAddress("glTexStorageMem2DEXT")); g_glImportMemoryWin32HandleEXT = reinterpret_cast(wglGetProcAddress("glImportMemoryWin32HandleEXT")); - g_glMemoryObjectParameterivEXT = - reinterpret_cast(wglGetProcAddress("glMemoryObjectParameterivEXT")); g_glGenSemaphoresEXT = reinterpret_cast(wglGetProcAddress("glGenSemaphoresEXT")); g_glDeleteSemaphoresEXT = reinterpret_cast(wglGetProcAddress("glDeleteSemaphoresEXT")); g_glImportSemaphoreWin32HandleEXT = @@ -144,37 +143,32 @@ namespace g_glWaitSemaphoreEXT = reinterpret_cast(wglGetProcAddress("glWaitSemaphoreEXT")); g_glSignalSemaphoreEXT = reinterpret_cast(wglGetProcAddress("glSignalSemaphoreEXT")); - // glMemoryObjectParameterivEXT is required, not optional: without it - // the GL side cannot mark an imported memory object dedicated, and a - // dedicated Vulkan export imported as non-dedicated corrupts the image. - g_glInteropAvailable = g_glCreateMemoryObjectsEXT && g_glDeleteMemoryObjectsEXT && g_glTexStorageMem2DEXT - && g_glImportMemoryWin32HandleEXT && g_glMemoryObjectParameterivEXT && g_glGenSemaphoresEXT + g_glInteropAvailable = g_glCreateMemoryObjectsEXT && g_glDeleteMemoryObjectsEXT && g_glMemoryObjectParameterivEXT + && g_glTexStorageMem2DEXT && g_glImportMemoryWin32HandleEXT && g_glGenSemaphoresEXT && g_glDeleteSemaphoresEXT && g_glImportSemaphoreWin32HandleEXT && g_glWaitSemaphoreEXT && g_glSignalSemaphoreEXT; - // Identify the GL driver alongside the interop probe result. Useful when - // the Windows GL context happens to be the Microsoft GDI Generic - // software renderer, in which case interop is expected to fail. + // Unconditional (runs once): identifies the GL driver, e.g. the GDI + // Generic software renderer, which cannot do interop. const GLubyte* vendor = glGetString(GL_VENDOR); const GLubyte* renderer = glGetString(GL_RENDERER); const GLubyte* version = glGetString(GL_VERSION); - if (IPCore::ImageRenderer::debugGpu()) - { - std::cout << "INFO: QTVulkanVideoDevice: GL_VENDOR='" << (vendor ? reinterpret_cast(vendor) : "?") - << "' GL_RENDERER='" << (renderer ? reinterpret_cast(renderer) : "?") << "' GL_VERSION='" - << (version ? reinterpret_cast(version) : "?") << "'" << std::endl; - if (!g_glInteropAvailable) - { - std::cout << "INFO: QTVulkanVideoDevice: GL_EXT_memory_object_win32 / GL_EXT_semaphore_win32 NOT available; " - "falling back to CPU presentation path" - << std::endl; - } - else - { - std::cout << "INFO: QTVulkanVideoDevice: GL interop extensions resolved (GPU-interop available)" << std::endl; - } + std::cout << "INFO: QTVulkanVideoDevice: GL_VENDOR='" << (vendor ? reinterpret_cast(vendor) : "?") << "' GL_RENDERER='" + << (renderer ? reinterpret_cast(renderer) : "?") << "' GL_VERSION='" + << (version ? reinterpret_cast(version) : "?") << "'" << std::endl; + + if (!g_glInteropAvailable) + { + std::cout << "INFO: QTVulkanVideoDevice: GL_EXT_memory_object_win32 / GL_EXT_semaphore_win32 NOT available; " + "falling back to CPU presentation path" + << std::endl; + } + else + { + std::cout << "INFO: QTVulkanVideoDevice: GL interop extensions resolved (GPU-interop available)" << std::endl; } + return g_glInteropAvailable; } } // namespace @@ -183,9 +177,9 @@ namespace // dynamically resolved pointers. Linux still uses the real GLEW symbols. #define glCreateMemoryObjectsEXT g_glCreateMemoryObjectsEXT #define glDeleteMemoryObjectsEXT g_glDeleteMemoryObjectsEXT +#define glMemoryObjectParameterivEXT g_glMemoryObjectParameterivEXT #define glTexStorageMem2DEXT g_glTexStorageMem2DEXT #define glImportMemoryWin32HandleEXT g_glImportMemoryWin32HandleEXT -#define glMemoryObjectParameterivEXT g_glMemoryObjectParameterivEXT #define glGenSemaphoresEXT g_glGenSemaphoresEXT #define glDeleteSemaphoresEXT g_glDeleteSemaphoresEXT #define glImportSemaphoreWin32HandleEXT g_glImportSemaphoreWin32HandleEXT @@ -202,9 +196,6 @@ namespace Rv namespace { // Forces the CPU pack-and-upload present path regardless of GPU/driver. - // Useful for exercising the fallback, which the NVIDIA optimal-tiling fix - // otherwise makes rare. Read once. Platform-neutral (unlike the Windows-only - // GL-interop entry-point helpers above). bool forceCpuPresentation() { static const bool forced = getenv("RV_VULKAN_FORCE_CPU_PRESENT") != nullptr; @@ -212,27 +203,27 @@ namespace Rv } } // namespace - QTVulkanVideoDevice::QTVulkanVideoDevice(VideoModule* module, const string& name, VulkanView* view, QWidget* eventWidget) + QTVulkanVideoDevice::QTVulkanVideoDevice(VideoModule* module, const string& name, VulkanWindow* window, QWidget* eventWidget) : TwkGLF::GLVideoDevice(module, name, VideoDevice::ImageOutput | VideoDevice::ProvidesSync | VideoDevice::SubWindow) - , m_view(view) + , m_window(window) , m_translator(eventWidget ? std::make_unique(this, eventWidget) : nullptr) { - assert(view); + assert(window); } QTVulkanVideoDevice::~QTVulkanVideoDevice() { - // Delete the FBO and its colour texture while the GL context is current. - if (m_glContext && (m_fbo || m_fboColorTex || m_sharedGL[0].memoryObject || m_cpuFlipFbo)) + // Every slot, unconditionally: the imports live in the global share + // group and would outlive m_glContext, pinning the Vulkan memory. + if (m_glContext && m_offscreenSurface && m_glContext->makeCurrent(m_offscreenSurface.get())) { - m_glContext->makeCurrent(m_offscreenSurface.get()); m_fbo.reset(); if (m_fboColorTex) { glDeleteTextures(1, &m_fboColorTex); m_fboColorTex = 0; } - for (uint32_t i = 0; i < VulkanView::FRAMES_IN_FLIGHT; ++i) + for (uint32_t i = 0; i < VulkanWindow::kFramesInFlight; ++i) { cleanupSharedGLObjects(i); } @@ -240,15 +231,11 @@ namespace Rv m_glContext->doneCurrent(); } - // Explicit resets keep the release order: surface, context, translator. m_offscreenSurface.reset(); + m_glContext.reset(); - m_translator.reset(); - } - void QTVulkanVideoDevice::setEventWidget(QWidget* widget) - { - m_translator = widget ? std::make_unique(this, widget) : nullptr; + m_translator.reset(); } //-------------------------------------------------------------------------- @@ -265,11 +252,8 @@ namespace Rv m_glContext = std::make_unique(); m_glContext->setFormat(fmt); - // Join RV's global GL resource-sharing group (enabled via - // Qt::AA_ShareOpenGLContexts at startup). Without this the offscreen - // context is isolated and FTGL font-atlas textures created in - // another context have no storage here, so glyph uploads fail with - // GL_INVALID_OPERATION. + // Join the global share group, or FTGL font-atlas textures created + // elsewhere have no storage here and glyph uploads fail. m_glContext->setShareContext(QOpenGLContext::globalShareContext()); if (!m_glContext->create()) @@ -294,11 +278,8 @@ namespace Rv m_glContext->makeCurrent(m_offscreenSurface.get()); glewExperimental = GL_TRUE; #ifdef PLATFORM_WINDOWS - // The bundled Windows GLEW (src/pub/glew) has a Tweak-modified - // signature: glewInit(GLEWGetProcAddress F). Pass nullptr to use - // the default GL entry-point loader, matching every other Windows - // glewInit call site (rvio main.cpp, InitGL.cpp, FBOVideoDevice.cpp, - // NDIModule.cpp, BlackMagicModule.cpp, AJAModule.cpp). + // The bundled Windows GLEW's glewInit takes a loader; nullptr + // selects the default, as at the other Windows call sites. GLenum err = glewInit(nullptr); #else GLenum err = glewInit(); @@ -319,13 +300,17 @@ namespace Rv return; } - const float dpr = m_view ? m_view->devicePixelRatio() : 1.0f; - int newW = m_view ? static_cast(m_view->width() * dpr + 0.5f) : 128; - int newH = m_view ? static_cast(m_view->height() * dpr + 0.5f) : 128; + const float dpr = m_window ? m_window->devicePixelRatioF() : 1.0f; + int newW = m_window ? static_cast(m_window->width() * dpr + 0.5f) : 128; + int newH = m_window ? static_cast(m_window->height() * dpr + 0.5f) : 128; if (newW < 1) + { newW = 128; + } if (newH < 1) + { newH = 128; + } if (!m_fbo || m_fboWidth != newW || m_fboHeight != newH) { @@ -336,17 +321,32 @@ namespace Rv m_fboColorTex = 0; } + // The control viewport composites blended passes here, so it needs + // RGBA16F. A passive output only receives already-composited blits, + // so RGB10_A2 (the shared image's format) halves its bandwidth. + const bool passiveOutput = m_window && m_window->isPassiveOutput(); + const GLenum fboFormat = passiveOutput ? GL_RGB10_A2 : GL_RGBA16F_ARB; + glGenTextures(1, &m_fboColorTex); glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m_fboColorTex); - glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA16F_ARB, newW, newH, 0, GL_RGBA, GL_FLOAT, nullptr); + if (passiveOutput) + { + glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, fboFormat, newW, newH, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullptr); + } + else + { + glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, fboFormat, newW, newH, 0, GL_RGBA, GL_FLOAT, nullptr); + } glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); - m_fbo = std::make_unique(newW, newH, GL_RGBA16F_ARB); + m_fbo = std::make_unique(newW, newH, fboFormat); m_fbo->attachColorTexture(GL_TEXTURE_RECTANGLE_ARB, m_fboColorTex); GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); if (status != GL_FRAMEBUFFER_COMPLETE_EXT) + { cerr << "ERROR: QTVulkanVideoDevice: FBO incomplete: 0x" << hex << status << dec << endl; + } m_fboWidth = newW; m_fboHeight = newH; @@ -365,8 +365,8 @@ namespace Rv { float refresh = -1.0f; - int w = m_view ? m_view->width() : 0; - int h = m_view ? m_view->height() : 0; + int w = m_window ? m_window->width() : 0; + int h = m_window ? m_window->height() : 0; int tx = x + w / 2; int ty = y + h / 2; @@ -385,12 +385,18 @@ namespace Rv if (refresh != m_refresh) { if (refresh > 0) + { m_refresh = refresh; + } else if (IPCore::debugPlayback) + { cout << "WARNING: ignoring intended desktop refresh rate = " << refresh << endl; + } if (IPCore::debugPlayback) + { cout << "INFO: new desktop refresh rate " << m_refresh << endl; + } } } m_x = x; @@ -405,20 +411,27 @@ namespace Rv static bool noQtHighDPISupport = (getenv("RV_NO_QT_HDPI_SUPPORT") != nullptr); if (noQtHighDPISupport) + { return; + } if (const DesktopVideoDevice* desktopDev = dynamic_cast(device)) { const QList screens = QGuiApplication::screens(); - if (desktopDev->qtScreen() < screens.size()) - m_devicePixelRatio = screens[desktopDev->qtScreen()]->devicePixelRatio(); + const int screen = desktopDev->qtScreen(); + if (screen >= 0 && screen < screens.size()) + { + m_devicePixelRatio = screens[screen]->devicePixelRatio(); + } } } float QTVulkanVideoDevice::devicePixelRatio() const { - if (m_view) - return static_cast(m_view->devicePixelRatio()); + if (m_window) + { + return static_cast(m_window->devicePixelRatioF()); + } return m_devicePixelRatio; } @@ -442,43 +455,64 @@ namespace Rv std::string QTVulkanVideoDevice::hardwareIdentification() const { return "vulkan-hybrid"; } + GLuint QTVulkanVideoDevice::fboID() const { return m_fbo ? m_fbo->fboID() : 0; } + void QTVulkanVideoDevice::cleanupSharedGLObjects(uint32_t slot) const { - SharedGLObjects& glShared = m_sharedGL[slot]; - - if (glShared.drawFbo) + if (m_glShared[slot].drawFbo) { - glDeleteFramebuffersEXT(1, &glShared.drawFbo); - glShared.drawFbo = 0; + glDeleteFramebuffersEXT(1, &m_glShared[slot].drawFbo); + m_glShared[slot].drawFbo = 0; } - if (glShared.texture) + if (m_glShared[slot].texture) { - glDeleteTextures(1, &glShared.texture); - glShared.texture = 0; + glDeleteTextures(1, &m_glShared[slot].texture); + m_glShared[slot].texture = 0; } - if (glShared.memoryObject) + if (m_glShared[slot].memoryObject) { - glDeleteMemoryObjectsEXT(1, &glShared.memoryObject); - glShared.memoryObject = 0; + glDeleteMemoryObjectsEXT(1, &m_glShared[slot].memoryObject); + m_glShared[slot].memoryObject = 0; } - if (glShared.glReadySemaphore) + if (m_glShared[slot].glReadySemaphore) { - glDeleteSemaphoresEXT(1, &glShared.glReadySemaphore); - glShared.glReadySemaphore = 0; + glDeleteSemaphoresEXT(1, &m_glShared[slot].glReadySemaphore); + m_glShared[slot].glReadySemaphore = 0; } - if (glShared.vkReadySemaphore) + if (m_glShared[slot].vkReadySemaphore) { - glDeleteSemaphoresEXT(1, &glShared.vkReadySemaphore); - glShared.vkReadySemaphore = 0; + glDeleteSemaphoresEXT(1, &m_glShared[slot].vkReadySemaphore); + m_glShared[slot].vkReadySemaphore = 0; } - glShared.width = 0; - glShared.height = 0; + m_glShared[slot].width = 0; + m_glShared[slot].height = 0; + } + + void QTVulkanVideoDevice::releaseSharedGLObjects() + { + // No context means nothing was ever imported. + if (!m_glContext || !m_offscreenSurface) + { + return; + } + + if (!m_glContext->makeCurrent(m_offscreenSurface.get())) + { + return; + } + for (uint32_t i = 0; i < VulkanWindow::kFramesInFlight; ++i) + { + cleanupSharedGLObjects(i); + } + m_glContext->doneCurrent(); } void QTVulkanVideoDevice::ensureCpuFallbackTarget(int w, int h) const { if (m_cpuFlipFbo && m_cpuFlipWidth == w && m_cpuFlipHeight == h) + { return; + } cleanupCpuFallbackTarget(); @@ -516,33 +550,85 @@ namespace Rv m_cpuFlipHeight = 0; } + bool QTVulkanVideoDevice::interopGLFailed(const char* what) const + { + GLenum first = glGetError(); + + if (first == GL_NO_ERROR) + { + return false; + } + + // Drain the rest so the next step starts from a clean queue. + while (glGetError() != GL_NO_ERROR) + { + } + + cerr << "ERROR: QTVulkanVideoDevice: " << what << " failed (GL 0x" << hex << first << dec << "); demoting '" << name() + << "' to CPU presentation." << endl; + + m_interopDisabled = true; + + return true; + } + + bool QTVulkanVideoDevice::glDeviceMatchesVulkan() const + { + if (m_glVulkanDeviceMatch != -1) + { + return m_glVulkanDeviceMatch == 1; + } + if (!m_glContext || !m_window || !m_window->isInitialized()) + { + return false; + } + + using GetUnsignedByteIndexedProc = void(GLAPIENTRY*)(GLenum, GLuint, GLubyte*); + const auto getUnsignedByteIndexed = + reinterpret_cast(m_glContext->getProcAddress("glGetUnsignedBytei_vEXT")); + + GLint deviceCount = 0; + if (getUnsignedByteIndexed) + { + glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &deviceCount); + } + + bool matched = false; + for (GLint i = 0; i < deviceCount && !matched; ++i) + { + std::array uuid{}; + getUnsignedByteIndexed(GL_DEVICE_UUID_EXT, static_cast(i), uuid.data()); + matched = m_window->physicalDeviceMatchesUUID(uuid.data(), uuid.size()); + } + + m_glVulkanDeviceMatch = matched ? 1 : 0; + if (!matched && ImageRenderer::debugGpu()) + { + cout << "INFO: QTVulkanVideoDevice: GL/Vulkan device UUIDs do not match or are unavailable; using CPU fallback." << endl; + } + return matched; + } + void QTVulkanVideoDevice::presentCpuFallback(int w, int h) const { TwkGLF::GLFBO* fbo = m_fbo.get(); - // Pack in the swapchain's channel order. glReadPixels with - // GL_UNSIGNED_INT_2_10_10_10_REV packs A2B10G10R10 (R low) for GL_RGBA and - // A2R10G10B10 (R high) for GL_BGRA, so the read format selects the layout - // directly with no CPU conversion. Linux/RADV surfaces commonly offer only - // A2R10G10B10. - const VkFormat scFmt = m_view ? m_view->swapchainFormat() : VK_FORMAT_A2B10G10R10_UNORM_PACK32; + // With GL_UNSIGNED_INT_2_10_10_10_REV, GL_RGBA packs A2B10G10R10 and + // GL_BGRA packs A2R10G10B10, so the read format matches the swapchain. + const VkFormat scFmt = m_window ? m_window->swapchainFormat() : VK_FORMAT_A2B10G10R10_UNORM_PACK32; const GLenum readFormat = (scFmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32) ? GL_BGRA : GL_RGBA; ensureCpuFallbackTarget(w, h); - // Y-flip blit (GL bottom-left -> Vulkan top-left) into the RGB10_A2 target, - // so glReadPixels below reads top-down and packs to the swapchain layout. + // Y-flip (GL bottom-left to Vulkan top-left) into the RGB10_A2 target. glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, fbo->fboID()); glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, m_cpuFlipFbo); glBlitFramebufferEXT(0, 0, w, h, 0, h, w, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST); glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, m_cpuFlipFbo); - // GL-packed readback: glReadPixels stalls until the flip blit finishes, but - // the driver packs directly to the swapchain bit layout, so there is no - // per-pixel CPU pack loop. m_cpuPackedScratch.resize(static_cast(w) * h); glReadPixels(0, 0, w, h, readFormat, GL_UNSIGNED_INT_2_10_10_10_REV, m_cpuPackedScratch.data()); - m_view->presentPixelData(m_cpuPackedScratch.data(), w, h); + m_window->presentPixelData(m_cpuPackedScratch.data(), w, h); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo->fboID()); // restore } @@ -553,229 +639,250 @@ namespace Rv void QTVulkanVideoDevice::syncBuffers() const { - if (!m_view) + if (!m_window) + { return; + } if (!m_glContext || !m_fbo) + { return; + } - // Pair this frame's GL ring objects with the Vulkan in-flight slot the - // frame renders into. getSharedImageInfo()/presentSharedImage() below use - // this same slot; presentSharedImage() advances it only at frame end, so - // the value is stable for the whole call. - const uint32_t slot = m_view->currentFrame(); - SharedGLObjects& glShared = m_sharedGL[slot]; + // The Vulkan in-flight slot; presentSharedImage() advances it only at + // frame end, so it is stable for the whole call. + const uint32_t slot = m_window->currentFrame(); TwkGLF::GLFBO* fbo = m_fbo.get(); const int w = static_cast(fbo->width()); const int h = static_cast(fbo->height()); if (w <= 0 || h <= 0) + { + return; + } + + // No swapchain before the first expose; exposeEvent() calls us again. + if (!m_window->isInitialized()) + { return; + } + + // Skip the frame, before any GL work, while a passive output's previous + // GPU work is still in flight. Always true for the control viewport. + if (m_window->isPassiveOutput() && !m_window->canPresentNow()) + { + return; + } if (!m_glContext->makeCurrent(m_offscreenSurface.get())) + { return; + } - // Get shared image info from VulkanView. RV_VULKAN_FORCE_CPU_PRESENT skips - // interop entirely so getSharedImageInfo() never allocates a shared image - // and the CPU fallback below runs. #ifdef PLATFORM_WINDOWS - // Probe the EXT_memory_object/EXT_semaphore (+ _win32) entry points - // while the GL context is current. If the driver does not expose them, - // skip the Vulkan-side export work entirely and fall through to the - // CPU pack-and-upload path below. - const bool glInteropAvailable = !forceCpuPresentation() && !m_glInteropFailed && loadGLInteropExtensions(); - const VulkanView::SharedImageInfo* sharedInfo = glInteropAvailable ? m_view->getSharedImageInfo(w, h) : nullptr; + const bool glExtensionsAvailable = loadGLInteropExtensions(); #else - const bool glInteropAvailable = !forceCpuPresentation() && !m_glInteropFailed && GLEW_EXT_memory_object && GLEW_EXT_semaphore - && GLEW_EXT_memory_object_fd && GLEW_EXT_semaphore_fd; - const VulkanView::SharedImageInfo* sharedInfo = glInteropAvailable ? m_view->getSharedImageInfo(w, h) : nullptr; + const bool glExtensionsAvailable = + GLEW_EXT_memory_object && GLEW_EXT_semaphore && GLEW_EXT_memory_object_fd && GLEW_EXT_semaphore_fd; #endif + const bool glInteropAvailable = !forceCpuPresentation() && !m_interopDisabled && glExtensionsAvailable && glDeviceMatchesVulkan(); + const VulkanWindow::SharedImageInfo* sharedInfo = glInteropAvailable ? m_window->getSharedImageInfo(w, h) : nullptr; + + // Logged on every transition: the first call can precede swapchain + // creation. + const int presentPath = sharedInfo ? 1 : 0; + if (m_loggedPresentPath != presentPath) + { + m_loggedPresentPath = presentPath; + const VkFormat scFmt = m_window ? m_window->swapchainFormat() : VK_FORMAT_UNDEFINED; + cout << "INFO: QTVulkanVideoDevice: syncBuffers: '" << name() << "' " << w << "x" << h + << " present path = " << (sharedInfo ? "GPU-interop" : "CPU-fallback") << " swapchainFormat=" << scFmt + << (scFmt == VK_FORMAT_A2B10G10R10_UNORM_PACK32 ? " (A2B10G10R10 / 10-bit)" + : scFmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32 ? " (A2R10G10B10 / 10-bit)" + : scFmt == VK_FORMAT_UNDEFINED ? " (UNDEFINED -- swapchain not created yet)" + : " (NOT 10-bit)") + << endl; + } if (!sharedInfo) { - // No zero-copy interop this frame: pack + present via the CPU fallback. - // The GL-packed RGB10_A2 readback handles the Y flip and the swapchain - // channel order (A2B10G10R10 / A2R10G10B10) without a per-pixel loop. - // - // Report the specific reason so the startup record is conclusive for - // someone reading only a log: a bare "CPU fallback" does not say - // whether interop was forced off, unavailable in GL, or refused by - // the Vulkan-side capability probe. std::string reason; if (forceCpuPresentation()) { reason = "RV_VULKAN_FORCE_CPU_PRESENT is set"; } - else if (!glInteropAvailable) + else if (m_interopDisabled) + { + reason = "an earlier GL call on the interop path failed; the device is demoted for the rest of the session"; + } + else if (!glExtensionsAvailable) { reason = "the GL driver does not expose the EXT_memory_object / EXT_semaphore interop entry points"; } + else if (!glDeviceMatchesVulkan()) + { + reason = "the GL context and the Vulkan device are different GPUs, so external-memory interop is unsafe"; + } else { - const VulkanView::InteropConfig& config = m_view->interopConfig(); + const VulkanWindow::InteropConfig& config = m_window->interopConfig(); reason = config.rejectReason.empty() ? "the Vulkan side declined to allocate a shared image" : config.rejectReason; } - m_view->reportPresentPath(VulkanView::PresentPath::CpuReadback, reason); + m_window->reportPresentPath(VulkanWindow::PresentPath::CpuReadback, reason); presentCpuFallback(w, h); return; } - // Re-import only when the shared image was actually reallocated, i.e. its - // capacity (stride width + capacity height) changed. Within capacity the - // Vulkan side keeps the same export, so a resize does not re-import here; - // glShared.width/height cache the imported capacity, not the used - // (requested) size. - if (glShared.width != sharedInfo->strideWidth || glShared.height != sharedInfo->capacityHeight || !glShared.memoryObject) + // Re-import only when the shared image's capacity changed; the slot's + // width and height cache the imported capacity, not the used size. + if (m_glShared[slot].width != sharedInfo->strideWidth || m_glShared[slot].height != sharedInfo->capacityHeight + || !m_glShared[slot].memoryObject) { cleanupSharedGLObjects(slot); - // Clear any pre-existing GL error so the check after the import - // sequence attributes only this sequence's failures. + // Start from a clean error queue so interopGLFailed() below cannot + // attribute an unrelated earlier error to the import. while (glGetError() != GL_NO_ERROR) { } - glCreateMemoryObjectsEXT(1, &glShared.memoryObject); + glCreateMemoryObjectsEXT(1, &m_glShared[slot].memoryObject); - // Mirror the Vulkan side's dedicated-allocation decision. This must - // be set on the memory object BEFORE glTexStorageMem2DEXT, and must - // match the export exactly: a dedicated Vulkan allocation imported - // as non-dedicated (or the reverse) produces a corrupted image - // rather than an error. NVIDIA's OPAQUE_WIN32 path requires it. + // Both sides must agree on dedicated allocation before the import; + // a mismatch corrupts the image silently, so always set it. { const GLint dedicated = sharedInfo->dedicatedAllocation ? GL_TRUE : GL_FALSE; - glMemoryObjectParameterivEXT(glShared.memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT, &dedicated); + glMemoryObjectParameterivEXT(m_glShared[slot].memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT, &dedicated); } #ifdef PLATFORM_WINDOWS - // Windows GL import does NOT take ownership of the HANDLE; the - // Vulkan side and this GL side each keep their own reference. - // VulkanView's cleanupSharedImage() calls CloseHandle on its - // copy; this device's cleanupSharedGLObjects() does not need to - // close anything because glImportMemoryWin32HandleEXT does not - // create a new handle. - glImportMemoryWin32HandleEXT(glShared.memoryObject, sharedInfo->size, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, + // The Win32 import does not take ownership of the HANDLE; + // VulkanWindow::cleanupSharedImage() closes it. + glImportMemoryWin32HandleEXT(m_glShared[slot].memoryObject, sharedInfo->size, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, static_cast(sharedInfo->memoryHandle)); #else + const auto dupFailed = [&](std::string_view fdName) + { + cerr << "ERROR: QTVulkanVideoDevice: dup(" << fdName << ") failed." << endl; + cleanupSharedGLObjects(slot); + m_window->reportPresentPath(VulkanWindow::PresentPath::CpuReadback, "dup() of a shared image FD failed"); + presentCpuFallback(w, h); + }; + // Duplicate the FD because glImportMemoryFdEXT takes ownership int memFd = dup(sharedInfo->memoryFd); if (memFd == -1) { - cerr << "ERROR: QTVulkanVideoDevice: dup(memoryFd) failed." << endl; - cleanupSharedGLObjects(slot); + dupFailed("memoryFd"); return; } - glImportMemoryFdEXT(glShared.memoryObject, sharedInfo->size, GL_HANDLE_TYPE_OPAQUE_FD_EXT, memFd); + glImportMemoryFdEXT(m_glShared[slot].memoryObject, sharedInfo->size, GL_HANDLE_TYPE_OPAQUE_FD_EXT, memFd); #endif - glGenTextures(1, &glShared.texture); - glBindTexture(GL_TEXTURE_2D, glShared.texture); + glGenTextures(1, &m_glShared[slot].texture); + glBindTexture(GL_TEXTURE_2D, m_glShared[slot].texture); - // Import with the tiling the Vulkan side actually created the image - // with. Importing OPTIMAL-tiled memory as LINEAR leaves the image's - // large-scale structure recognizable but scrambles pixels within - // each tile. + // Must match the Vulkan image's tiling, or pixels scramble within + // each tile. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_TILING_EXT, sharedInfo->tiling == VK_IMAGE_TILING_OPTIMAL ? GL_OPTIMAL_TILING_EXT : GL_LINEAR_TILING_EXT); - // Allocate the imported texture at the image's capacity dimensions - // (stride width x capacity height); the FBO blit below writes only the - // used w x h sub-region into its origin corner. - glTexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGB10_A2, sharedInfo->strideWidth, sharedInfo->capacityHeight, glShared.memoryObject, - 0); + // Allocated at capacity; the blit below writes only the used w x h. + glTexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGB10_A2, sharedInfo->strideWidth, sharedInfo->capacityHeight, + m_glShared[slot].memoryObject, 0); glBindTexture(GL_TEXTURE_2D, 0); - glGenSemaphoresEXT(1, &glShared.glReadySemaphore); - glGenSemaphoresEXT(1, &glShared.vkReadySemaphore); + glGenSemaphoresEXT(1, &m_glShared[slot].glReadySemaphore); + glGenSemaphoresEXT(1, &m_glShared[slot].vkReadySemaphore); #ifdef PLATFORM_WINDOWS - glImportSemaphoreWin32HandleEXT(glShared.glReadySemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, + glImportSemaphoreWin32HandleEXT(m_glShared[slot].glReadySemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, static_cast(sharedInfo->glReadySemaphoreHandle)); - glImportSemaphoreWin32HandleEXT(glShared.vkReadySemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, + glImportSemaphoreWin32HandleEXT(m_glShared[slot].vkReadySemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, static_cast(sharedInfo->vkReadySemaphoreHandle)); #else int glReadyFd = dup(sharedInfo->glReadySemaphoreFd); if (glReadyFd == -1) { - cerr << "ERROR: QTVulkanVideoDevice: dup(glReadySemaphoreFd) failed." << endl; - cleanupSharedGLObjects(slot); + dupFailed("glReadySemaphoreFd"); return; } - glImportSemaphoreFdEXT(glShared.glReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, glReadyFd); + glImportSemaphoreFdEXT(m_glShared[slot].glReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, glReadyFd); int vkReadyFd = dup(sharedInfo->vkReadySemaphoreFd); if (vkReadyFd == -1) { - cerr << "ERROR: QTVulkanVideoDevice: dup(vkReadySemaphoreFd) failed." << endl; - cleanupSharedGLObjects(slot); + dupFailed("vkReadySemaphoreFd"); return; } - glImportSemaphoreFdEXT(glShared.vkReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, vkReadyFd); + glImportSemaphoreFdEXT(m_glShared[slot].vkReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, vkReadyFd); #endif - // The import sequence fails by producing a GL error and an unusable - // texture rather than by any return value, so check explicitly. A - // silently failed import is what presents as a corrupted viewport; - // dropping to the CPU rung instead keeps the image correct. - const GLenum importError = glGetError(); - if (importError != GL_NO_ERROR) + // The import calls above report failure only through glGetError(). + if (interopGLFailed("GL<->Vulkan shared image import")) { - cerr << "ERROR: QTVulkanVideoDevice: GL import of the Vulkan shared image failed (GL error 0x" << std::hex << importError - << std::dec << ", tiling=" << (sharedInfo->tiling == VK_IMAGE_TILING_OPTIMAL ? "OPTIMAL" : "LINEAR") - << ", dedicated=" << (sharedInfo->dedicatedAllocation ? "yes" : "no") << "); using the CPU readback path instead." - << endl; - cleanupSharedGLObjects(slot); - m_glInteropFailed = true; - m_view->reportPresentPath(VulkanView::PresentPath::CpuReadback, "GL import of the shared image raised a GL error"); + // Interop is now off for good, so release every slot. + for (uint32_t i = 0; i < VulkanWindow::kFramesInFlight; ++i) + { + cleanupSharedGLObjects(i); + } + m_window->reportPresentPath(VulkanWindow::PresentPath::CpuReadback, "GL import of the shared image raised a GL error"); presentCpuFallback(w, h); return; } - // Cache the imported capacity so we re-import only when it grows. - glShared.width = sharedInfo->strideWidth; - glShared.height = sharedInfo->capacityHeight; + m_glShared[slot].width = sharedInfo->strideWidth; + m_glShared[slot].height = sharedInfo->capacityHeight; - m_view->reportGLImportState(sharedInfo->tiling, sharedInfo->dedicatedAllocation); + m_window->reportGLImportState(sharedInfo->tiling, sharedInfo->dedicatedAllocation); } - // Import succeeded (or was already valid from a previous frame): this - // frame presents zero-copy. Emitting here rather than before the import - // means the record reflects the path actually taken, and can report the - // GL side's settings alongside the Vulkan side's. - m_view->reportPresentPath(VulkanView::PresentPath::ZeroCopy, std::string()); + // session->render() can leave errors pending; drain them so the check + // below sees only the wait/blit/signal sequence's errors. + while (glGetError() != GL_NO_ERROR) + { + } - // Wait for Vulkan to be ready - GLuint waitSrcLayouts[] = {GL_LAYOUT_TRANSFER_SRC_EXT}; - glWaitSemaphoreEXT(glShared.vkReadySemaphore, 0, nullptr, 1, &glShared.texture, waitSrcLayouts); + constexpr GLenum waitSrcLayout = GL_LAYOUT_TRANSFER_SRC_EXT; + glWaitSemaphoreEXT(m_glShared[slot].vkReadySemaphore, 0, nullptr, 1, &m_glShared[slot].texture, &waitSrcLayout); - // Blit from FBO to shared texture GLuint readFbo = fbo->fboID(); - if (!glShared.drawFbo) + if (!m_glShared[slot].drawFbo) { - glGenFramebuffersEXT(1, &glShared.drawFbo); - glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, glShared.drawFbo); - glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, glShared.texture, 0); + glGenFramebuffersEXT(1, &m_glShared[slot].drawFbo); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, m_glShared[slot].drawFbo); + glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_glShared[slot].texture, 0); } else { - glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, glShared.drawFbo); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, m_glShared[slot].drawFbo); } glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, readFbo); - // Note: GL origin is bottom-left, Vulkan origin is top-left. We need to flip Y. + // Flip Y: GL origin is bottom-left, Vulkan origin is top-left. glBlitFramebufferEXT(0, 0, w, h, 0, h, w, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, readFbo); // restore - // Signal Vulkan that GL is done - GLuint signalDstLayouts[] = {GL_LAYOUT_COLOR_ATTACHMENT_EXT}; - glSignalSemaphoreEXT(glShared.glReadySemaphore, 0, nullptr, 1, &glShared.texture, signalDstLayouts); + constexpr GLenum signalDstLayout = GL_LAYOUT_COLOR_ATTACHMENT_EXT; + glSignalSemaphoreEXT(m_glShared[slot].glReadySemaphore, 0, nullptr, 1, &m_glShared[slot].texture, &signalDstLayout); glFlush(); - // Tell VulkanView to present - m_view->presentSharedImage(); + if (interopGLFailed("GL<->Vulkan shared image blit")) + { + for (uint32_t i = 0; i < VulkanWindow::kFramesInFlight; ++i) + { + cleanupSharedGLObjects(i); + } + m_window->reportPresentPath(VulkanWindow::PresentPath::CpuReadback, "GL blit into the shared image raised a GL error"); + presentCpuFallback(w, h); + return; + } + + m_window->reportPresentPath(VulkanWindow::PresentPath::ZeroCopy, std::string()); + m_window->presentSharedImage(); } //-------------------------------------------------------------------------- @@ -784,9 +891,10 @@ namespace Rv void QTVulkanVideoDevice::redraw() const { - if (m_view) + // requestUpdate() coalesces, so a burst of redraws renders once. + if (m_window) { - QCoreApplication::postEvent(m_view, new QEvent(QEvent::UpdateRequest)); + m_window->requestUpdate(); } } @@ -796,12 +904,12 @@ namespace Rv VideoDevice::Resolution QTVulkanVideoDevice::resolution() const { - if (!m_view) + if (!m_window) { return Resolution(0, 0, 1.0f, 1.0f); } - const float dpr = m_view->devicePixelRatio(); - return Resolution(static_cast(m_view->width() * dpr + 0.5f), static_cast(m_view->height() * dpr + 0.5f), 1.0f, 1.0f); + const float dpr = m_window->devicePixelRatioF(); + return Resolution(static_cast(m_window->width() * dpr + 0.5f), static_cast(m_window->height() * dpr + 0.5f), 1.0f, 1.0f); } VideoDevice::Offset QTVulkanVideoDevice::offset() const { return Offset(m_x, m_y); } @@ -810,56 +918,54 @@ namespace Rv VideoDevice::VideoFormat QTVulkanVideoDevice::format() const { - if (!m_view) + if (!m_window) { return VideoFormat(0, 0, 1.0, 1.0, 0.0, hardwareIdentification()); } - const float dpr = m_view->devicePixelRatio(); - return VideoFormat(static_cast(m_view->width() * dpr + 0.5f), static_cast(m_view->height() * dpr + 0.5f), 1.0, 1.0, + const float dpr = m_window->devicePixelRatioF(); + return VideoFormat(static_cast(m_window->width() * dpr + 0.5f), static_cast(m_window->height() * dpr + 0.5f), 1.0, 1.0, (m_refresh != -1.0f) ? m_refresh : 0.0f, hardwareIdentification()); } size_t QTVulkanVideoDevice::width() const { - if (!m_view) + if (!m_window) { return 0; } - return static_cast(m_view->width() * m_view->devicePixelRatio() + 0.5f); + return static_cast(m_window->width() * m_window->devicePixelRatioF() + 0.5f); } size_t QTVulkanVideoDevice::height() const { - if (!m_view) + if (!m_window) { return 0; } - return static_cast(m_view->height() * m_view->devicePixelRatio() + 0.5f); + return static_cast(m_window->height() * m_window->devicePixelRatioF() + 0.5f); } void QTVulkanVideoDevice::open(const StringVector& /*args*/) { - if (m_view) + if (m_window) { - m_view->show(); + m_window->show(); } - m_isOpen = true; } void QTVulkanVideoDevice::close() { - if (m_view) + if (m_window) { - m_view->hide(); + m_window->hide(); } - m_isOpen = false; } bool QTVulkanVideoDevice::isOpen() const { - if (m_view) + if (m_window) { - return m_view->isVisible(); + return m_window->isVisible(); } return false; } diff --git a/src/lib/app/RvCommon/RvApplication.cpp b/src/lib/app/RvCommon/RvApplication.cpp index 9eaf1e2b0..8682d028a 100644 --- a/src/lib/app/RvCommon/RvApplication.cpp +++ b/src/lib/app/RvCommon/RvApplication.cpp @@ -867,8 +867,7 @@ namespace Rv if (videoModules().empty()) { - // With a non-OpenGL presentation backend view() returns null — no - // GL context to make current; presentation handles it per-frame. + // view() is null on a non-OpenGL backend. if (doc->view()) { doc->view()->makeCurrent(); @@ -876,10 +875,7 @@ namespace Rv try { - // With a non-OpenGL presentation backend view() is null — pass - // nullptr as the GL share device. DesktopVideoDevice can still - // be created; it only needs the share device when open() is - // called later. + // DesktopVideoDevice only needs the share device at open(). QTGLVideoDevice* shareDevice = doc->view() ? doc->view()->videoDevice() : nullptr; addVideoModule(m_desktopModule = new DesktopVideoModule(0, shareDevice)); } @@ -907,7 +903,6 @@ namespace Rv // we're on (video device) so make sure the primary display group is // correct. // - // Use the session's control device — valid for any presentation backend. doc->session()->graph().setPrimaryDisplayGroup(doc->session()->controlVideoDevice()); if (RvApp()->documents().size() == 1 && opts.present) @@ -959,8 +954,6 @@ namespace Rv if (!m->isOpen()) { RvDocument* doc = reinterpret_cast(documents().front()->opaquePointer()); - // With a non-OpenGL presentation backend view() is null — no GL - // context to make current. if (doc->view()) { doc->view()->makeCurrent(); @@ -1709,7 +1702,6 @@ namespace Rv #endif string optionArgs = setVideoDeviceStateFromSettings(d); - // With a non-OpenGL presentation backend view() is null — skip GL makeCurrent. if (rvDoc->view()) { rvDoc->view()->videoDevice()->makeCurrent(); @@ -1801,16 +1793,32 @@ namespace Rv else { const VideoDevice* d = session->outputVideoDevice(); + const bool separateOutput = d && d != session->controlVideoDevice(); - if (d != session->controlVideoDevice()) + // + // Unbind before closing: close() destroys the GL context owning the + // FBOs the renderer cloned for this device, and unbind() must + // release them while it is still alive. + // + session->setOutputVideoDevice(session->controlVideoDevice()); + + if (separateOutput) { const_cast(d)->close(); #ifdef PLATFORM_DARWIN // rvDoc->setDoubleBuffer(true); #endif - } - session->setOutputVideoDevice(session->controlVideoDevice()); + // + // close() leaves no context current, and DesktopVideoDevice + // cannot restore one when the main view is Vulkan (its share + // device is null). Make the main view's context current. + // + if (const TwkGLF::GLVideoDevice* mainView = dynamic_cast(session->controlVideoDevice())) + { + mainView->makeCurrent(); + } + } #if 0 if (opts.vsync && !rvDoc->vsyncDisabled()) @@ -1948,6 +1956,82 @@ namespace Rv return options.toUtf8().constData(); } + void RvApplication::rebuildDesktopVideoDevices(RvSession* session, QTGLVideoDevice* shareDevice, bool mainViewIsVulkan) + { + if (!m_desktopModule) + { + return; + } + + // The selected screen is restored by name (Options::presentDevice) + // since the rebuild may destroy the device it pointed to. + const bool wasPresenting = m_presentationMode; + + // Returns false when the backend is unchanged; the share device is + // still rebound below. + const bool rebuilt = m_desktopModule->rebuildDevices(shareDevice, mainViewIsVulkan); + + const VideoModule::VideoDevices& devices = m_desktopModule->devices(); + for (VideoDevice* device : devices) + { + if (DesktopVideoDevice* dd = dynamic_cast(device)) + { + dd->setShareDevice(shareDevice); + } + } + + // + // Re-point the graph's display groups at the new device objects, even + // with presentation off, or a later setOutputVideoDevice() finds no + // group. refreshPhysicalDevices() keeps each group's colour pipeline, + // unlike setPhysicalDevices(). + // + if (rebuilt && session) + { + session->graph().refreshPhysicalDevices(videoModules()); + session->graph().setPrimaryDisplayGroup(session->controlVideoDevice()); + } + + // The callers reset the session output device, so re-open and re-bind it. + if (!wasPresenting || !session) + { + return; + } + + Rv::Options& opts = Rv::Options::sharedOptions(); + VideoDevice* d = findPresentationDevice(opts.presentDevice); + if (!d) + { + cerr << "ERROR: presentation device not found after rebuild." << endl; + session->setOutputVideoDevice(session->controlVideoDevice()); + m_presentationMode = false; + return; + } + + if (DesktopVideoDevice* dd = dynamic_cast(d)) + { + dd->setShareDevice(shareDevice); + } + + try + { + if (!d->isOpen()) + { + string optionArgs = setVideoDeviceStateFromSettings(d); + StringVector vargs; + algorithm::split(vargs, optionArgs, is_any_of(string(" \t\n\r")), token_compress_on); + d->open(vargs); + } + session->setOutputVideoDevice(d); + } + catch (const std::exception& exc) + { + cerr << "ERROR: failed to re-open presentation device after rebuild: " << exc.what() << endl; + session->setOutputVideoDevice(session->controlVideoDevice()); + m_presentationMode = false; + } + } + bool RvApplication::isInPresentationMode() { return m_presentationMode; } int RvApplication::findVideoModuleIndexByName(const string& name) const diff --git a/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h b/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h index dc78b2f2c..3dd9e6e9a 100644 --- a/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h +++ b/src/lib/app/RvCommon/RvCommon/DesktopVideoDevice.h @@ -17,7 +17,7 @@ #include #include -#include +#include #include #include @@ -55,22 +55,42 @@ namespace Rv class DesktopVideoDevice : public TwkGLF::GLBindableVideoDevice { public: - class ScreenView : public QOpenGLWidget + // + // QOpenGLWindow, not a top-level QOpenGLWidget: only a ctor-supplied + // share context guarantees the renderer's share group, which + // transfer() needs to see the renderer's textures. PartialUpdateBlit + // keeps a backing FBO and does not clear before paintGL(). + // + class ScreenWindow : public QOpenGLWindow + { + public: + ScreenWindow(const QSurfaceFormat& fmt, QOpenGLContext* glShareContext); + + void initializeGL() override; + void paintGL() override; + + private: + QOpenGLContext* m_glShareContext = nullptr; + }; + + // QWidget container for the ScreenWindow, as GLView is for GLWindow. + class ScreenView : public QWidget { public: // // 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. + // the control view may be a QOpenGLWidget or a QOpenGLWindow. A + // null share context falls back to Qt's global share context. // ScreenView(const QSurfaceFormat& fmt, QWidget* parent, QOpenGLContext* glShareContext, Qt::WindowFlags flags); - void initializeGL() override; - void paintGL() override; + ScreenWindow* glWindow() const { return m_glWindow; } private: - QOpenGLContext* m_glShareContext = nullptr; + ScreenWindow* m_glWindow = nullptr; + QWidget* m_container = nullptr; }; public: @@ -177,7 +197,21 @@ namespace Rv virtual void unbind() const; - virtual void clearCaches() const {} + virtual void clearCaches() const { releaseFBOClones(); } + + // + // Delete the FBO clones in m_fboMap. Must run while this device's view + // context is still alive, so close() calls it first. Idempotent. + // + void releaseFBOClones() const; + + // + // This context's cached clone of a renderer FBO, wrapping its colour + // texture (FBOs are not shared across contexts, textures are). The + // cache key is a pointer the renderer can reuse, so hits are + // re-verified. Returns null if no complete clone could be built. + // + TwkGLF::GLFBO* cloneForSource(const TwkGLF::GLFBO* sourceFbo) const; // // Configurations @@ -208,9 +242,9 @@ namespace Rv // From QTGLVideoDevice - void setViewWidget(QOpenGLWidget*); + void setViewWidget(ScreenView*); - QOpenGLWidget* viewWidget() const { return m_view; } + ScreenView* viewWidget() const { return m_view; } virtual void makeCurrent() const; @@ -241,6 +275,21 @@ namespace Rv static std::vector createDesktopVideoDevices(TwkApp::VideoModule* module, const QTGLVideoDevice* shareDevice); + // As above, with the backend supplied by the caller. Use once the main view is live. + static std::vector createDesktopVideoDevices(TwkApp::VideoModule* module, const QTGLVideoDevice* shareDevice, + bool useVulkan); + + // + // Presentation backend for the initial build: true for a 10-bit request + // this machine's Vulkan can present. Reads the persisted preference, + // which can differ from the live main-view backend, so it must not be + // used once a view exists. Always false on macOS. + // + static bool shouldUseVulkanPresentation(); + + // True when the persisted display depth is RGB 10 + A 2. + static bool tenBitDisplayRequested(); + protected: void addDefaultDataFormats(size_t bits = 8); void sortVideoFormatsByWidth(); @@ -261,9 +310,15 @@ namespace Rv protected: const QTGLVideoDevice* m_share; const TwkGLF::GLVideoDevice* m_viewDevice; - QOpenGLWidget* m_view; + ScreenView* m_view; DesktopStereoMode m_stereoMode; mutable FBOMap m_fboMap; + + // Last unusable source texture reported, so the report fires once per transition. + mutable GLuint m_reportedBadSourceTex{0}; + + // Latches the "no backing FBO" report. + mutable bool m_transferStalled{false}; TwkGLF::GLState* m_glGlobalState; DesktopVideoFormats m_videoFormats; DesktopDataFormats m_dataFormats; diff --git a/src/lib/app/RvCommon/RvCommon/DesktopVideoModule.h b/src/lib/app/RvCommon/RvCommon/DesktopVideoModule.h index fddf720cb..5dbc6b0ee 100644 --- a/src/lib/app/RvCommon/RvCommon/DesktopVideoModule.h +++ b/src/lib/app/RvCommon/RvCommon/DesktopVideoModule.h @@ -29,6 +29,15 @@ namespace Rv DesktopVideoModule(NativeDisplayPtr np, QTGLVideoDevice* shareDevice); virtual ~DesktopVideoModule(); + // + // Rebuild the per-screen presentation devices onto targetVulkan, the + // backend the main view is actually running (not the persisted + // preference). Returns false and leaves the devices untouched when the + // backend has not changed. The caller re-binds the share device and + // re-opens the session's presentation output. + // + bool rebuildDevices(const QTGLVideoDevice* shareDevice, bool targetVulkan); + virtual std::string name() const; virtual void open(); virtual void close(); diff --git a/src/lib/app/RvCommon/RvCommon/GLView.h b/src/lib/app/RvCommon/RvCommon/GLView.h index 44d5649ef..58cd8250e 100644 --- a/src/lib/app/RvCommon/RvCommon/GLView.h +++ b/src/lib/app/RvCommon/RvCommon/GLView.h @@ -12,6 +12,7 @@ #include #include #include +#include class QOpenGLContext; class QWindow; @@ -22,6 +23,10 @@ namespace Rv class QTGLVideoDevice; class GLWindow; + // -debug gpu helpers shared by GLView and GLWindow. + std::string glDebugEnvOrUnset(const char* name); + std::string glDebugFormatSummary(const QSurfaceFormat&); + // // GLView // diff --git a/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h b/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h index 6f2f20a8f..9f40ef61c 100644 --- a/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h +++ b/src/lib/app/RvCommon/RvCommon/QTGLVideoDevice.h @@ -8,9 +8,11 @@ #ifndef __RvCommon__QTGLVideoDevice__h__ #define __RvCommon__QTGLVideoDevice__h__ #include +#include #include #include #include +#include #include #include #include @@ -43,6 +45,9 @@ namespace Rv QTGLVideoDevice(TwkApp::VideoModule*, const std::string& name); virtual ~QTGLVideoDevice(); + QTGLVideoDevice(const QTGLVideoDevice&) = delete; + QTGLVideoDevice& operator=(const QTGLVideoDevice&) = delete; + void setWidget(QOpenGLWidget*); // @@ -136,6 +141,12 @@ namespace Rv // it, redraw() would call update() on a freed QOpenGLWindow. // QPointer m_window; + // + // Fallback surface for makeCurrent() once Qt has destroyed m_window's + // native surface but not its context (shutdown). Created while the + // window is healthy: QOffscreenSurface needs the platform plugin. + // + mutable std::unique_ptr m_teardownSurface; QTTranslator* m_translator; }; diff --git a/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h b/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h index f05bd857a..a415148bb 100644 --- a/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h +++ b/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h @@ -3,17 +3,20 @@ // // SPDX-License-Identifier: Apache-2.0 // -#pragma once +#ifndef __RvCommon__QTVulkanVideoDevice__h__ +#define __RvCommon__QTVulkanVideoDevice__h__ #include #include -#include +#include #include #include #include #include #include +#include + QT_BEGIN_NAMESPACE class QOpenGLContext; class QOffscreenSurface; @@ -22,22 +25,27 @@ QT_END_NAMESPACE namespace Rv { - class VulkanView; + class VulkanWindow; // // QTVulkanVideoDevice // - // Wraps a VulkanView as a TwkGLF::GLVideoDevice so that ImageRenderer's + // Wraps a VulkanWindow as a TwkGLF::GLVideoDevice so that ImageRenderer's // existing GL rendering pipeline (renderMain, shader cache, etc.) can run // unchanged on the Vulkan presentation path. // class QTVulkanVideoDevice : public TwkGLF::GLVideoDevice { public: - QTVulkanVideoDevice(TwkApp::VideoModule* module, const std::string& name, VulkanView* view, QWidget* eventWidget); + // eventWidget is the window's container QWidget, used by QTTranslator + // for coordinate mapping and mouse grab. + QTVulkanVideoDevice(TwkApp::VideoModule* module, const std::string& name, VulkanWindow* window, QWidget* eventWidget); ~QTVulkanVideoDevice() override; - void setEventWidget(QWidget* widget); + QTVulkanVideoDevice(const QTVulkanVideoDevice&) = delete; + QTVulkanVideoDevice& operator=(const QTVulkanVideoDevice&) = delete; + + void resetInteropDeviceMatch() const { m_glVulkanDeviceMatch = -1; } const QTTranslator& translator() const { return *m_translator; } @@ -45,6 +53,10 @@ namespace Rv void setAbsolutePosition(int x, int y); + // Drop every GL object imported from the Vulkan side so none outlives + // the memory it aliases. syncBuffers() re-imports on the next frame. + void releaseSharedGLObjects(); + // VideoDevice API void makeCurrent() const override; void syncBuffers() const override; @@ -73,20 +85,25 @@ namespace Rv const TwkGLF::GLFBO* defaultFBO() const override; std::string hardwareIdentification() const override; + // Readiness probe: the FBO id, or 0 before it exists. Unlike + // defaultFBO() it does not create the context; DesktopVideoDevice:: + // transfer() waits for a non-zero id. + GLuint fboID() const override; + private: - // Ensure the QOpenGLContext + FBO exist and match the current view size. + // Ensure the QOpenGLContext + FBO exist and match the current window size. // Makes the GL context current and binds the FBO on return. void ensureGLContext() const; - VulkanView* m_view; + // The window container owns the window, so Qt can delete it + // independently of this device. + QPointer m_window; std::unique_ptr m_translator; float m_devicePixelRatio{1.0f}; int m_x{0}; int m_y{0}; float m_refresh{-1.0f}; - bool m_isOpen{false}; - // Qt GL context + offscreen surface for GL rendering. mutable std::unique_ptr m_glContext; mutable std::unique_ptr m_offscreenSurface; mutable std::unique_ptr m_fbo; @@ -94,9 +111,7 @@ namespace Rv mutable int m_fboWidth{0}; mutable int m_fboHeight{0}; - // GPU Interop GL objects, ringed per in-flight slot to match VulkanView's - // per-slot Vulkan shared image/semaphores. Indexed by the Vulkan slot for - // the frame being rendered (VulkanView::currentFrame()). + // Interop GL objects, indexed by VulkanWindow::currentFrame(). struct SharedGLObjects { GLuint memoryObject{0}; @@ -104,27 +119,34 @@ namespace Rv GLuint glReadySemaphore{0}; GLuint vkReadySemaphore{0}; GLuint drawFbo{0}; - int width{0}; // imported capacity width - int height{0}; // imported capacity height + + // Imported capacity, not the used size. + int width{0}; + int height{0}; }; - mutable std::array m_sharedGL{}; + mutable std::array m_glShared{}; + + // Last reported present path: -1 none yet, 0 CPU-fallback, 1 GPU-interop. + mutable int m_loggedPresentPath{-1}; + // -1 until queried, 0 when GL and Vulkan use different/unidentifiable + // physical devices, 1 when their device UUIDs match. + mutable int m_glVulkanDeviceMatch{-1}; - // Latched once the GL side fails to import a Vulkan-exported shared - // image. Without this the next frame re-attempts the same import with - // the same configuration and fails identically, once per frame. The - // session stays on the CPU readback path instead. - mutable bool m_glInteropFailed{false}; + // Latched once any GL call on the interop path fails; the device then + // stays on the CPU path. + mutable bool m_interopDisabled{false}; + + // Drain glGetError(); on error, report which step failed, latch + // m_interopDisabled and return true. Callers must then release the + // slot's GL objects and present through the CPU fallback. + bool interopGLFailed(const char* what) const; void cleanupSharedGLObjects(uint32_t slot) const; + bool glDeviceMatchesVulkan() const; - // CPU-fallback GL state (used only when GPU interop is unavailable or - // refused). A flipped RGB10_A2 blit target lets GL pack the 10-bit pixels - // directly with glReadPixels(GL_UNSIGNED_INT_2_10_10_10_REV) and handle the - // Y flip, eliminating the per-pixel CPU pack loop. The readback format - // (GL_RGBA vs GL_BGRA) selects the swapchain's channel order - // (A2B10G10R10 / A2R10G10B10). Not ringed: the fallback is a synchronous - // readback, so a single reused target is sufficient. + // CPU-fallback target: a Y-flipped RGB10_A2 copy that glReadPixels packs + // directly. Not ringed, since the readback is synchronous. mutable GLuint m_cpuFlipFbo{0}; mutable GLuint m_cpuFlipTex{0}; mutable int m_cpuFlipWidth{0}; @@ -134,9 +156,9 @@ namespace Rv void ensureCpuFallbackTarget(int w, int h) const; void cleanupCpuFallbackTarget() const; - // Pack + present the framebuffer via the CPU fallback (GL-packed RGB10_A2 - // readback). Used when no zero-copy interop path is available. void presentCpuFallback(int w, int h) const; }; } // namespace Rv + +#endif // __RvCommon__QTVulkanVideoDevice__h__ diff --git a/src/lib/app/RvCommon/RvCommon/RvApplication.h b/src/lib/app/RvCommon/RvCommon/RvApplication.h index acb513f04..987530606 100644 --- a/src/lib/app/RvCommon/RvCommon/RvApplication.h +++ b/src/lib/app/RvCommon/RvCommon/RvApplication.h @@ -99,6 +99,14 @@ namespace Rv RvPreferences* prefDialog(); RvProfileManager* profileManager(); + // + // Set once the last document starts tearing down, so queued events + // (e.g. the console's output-driven auto-show) do not reopen windows. + // + bool isShuttingDown() const { return m_shuttingDown; } + + void setShuttingDown() { m_shuttingDown = true; } + bool networkDialogRunning() const { return m_networkDialog ? true : false; } void processNetworkOpts(bool startup = true); @@ -126,6 +134,16 @@ namespace Rv void setPresentationMode(bool); bool isInPresentationMode(); + // + // Rebuild the desktop presentation devices to match the main view's + // backend, re-bind the share device, and re-open the presentation + // output if presentation mode is on. shareDevice is null when the main + // view is Vulkan. mainViewIsVulkan comes from the caller because only + // it knows which view widget exists now. + // + // session is the document whose main view changed backend. + void rebuildDesktopVideoDevices(RvSession* session, QTGLVideoDevice* shareDevice, bool mainViewIsVulkan); + DesktopVideoModule* desktopVideoModule() const { return m_desktopModule; } static int parseInFiles(int argc, char* argv[]); @@ -161,6 +179,7 @@ namespace Rv RvWebManager* m_webManager; TwkApp::VideoDevice* m_presentationDevice; bool m_presentationMode; + bool m_shuttingDown{false}; mutable pthread_mutex_t m_deleteLock; std::string m_executableNameCaps; DesktopVideoModule* m_desktopModule; diff --git a/src/lib/app/RvCommon/RvCommon/RvDocument.h b/src/lib/app/RvCommon/RvCommon/RvDocument.h index 5f3e5bfae..271aa3e4f 100644 --- a/src/lib/app/RvCommon/RvCommon/RvDocument.h +++ b/src/lib/app/RvCommon/RvCommon/RvDocument.h @@ -83,11 +83,8 @@ namespace Rv QWidget* viewWidget() const; // - // Active presentation video device for whichever backend is in use - // (the OpenGL GLView or, on Linux, the Vulkan VulkanView). Returns - // nullptr if no view has been created yet. Prefer this over - // view()->videoDevice() in backend-neutral code so the Vulkan/Metal - // paths (where view() is null) stay crash-safe. + // Video device of the active view (GLView or VulkanView), or nullptr. + // Prefer it over view()->videoDevice(): view() is null on Vulkan. // TwkGLF::GLVideoDevice* viewVideoDevice() const; @@ -97,6 +94,9 @@ namespace Rv // Replace a live VulkanView with GLView after a runtime Vulkan failure. void fallbackVulkanToGLView(); + + // Promote a live GLView to a VulkanView so a 10-bit request applies immediately. + void swapGLViewToVulkan(); #endif const QAction* lastPopupAction() const { return m_lastPopupAction; } diff --git a/src/lib/app/RvCommon/RvCommon/VulkanDesktopVideoDevice.h b/src/lib/app/RvCommon/RvCommon/VulkanDesktopVideoDevice.h new file mode 100644 index 000000000..1250a7009 --- /dev/null +++ b/src/lib/app/RvCommon/RvCommon/VulkanDesktopVideoDevice.h @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef __RvCommon__VulkanDesktopVideoDevice__h__ +#define __RvCommon__VulkanDesktopVideoDevice__h__ + +#include + +#include + +namespace Rv +{ + class VulkanView; + + // + // VulkanDesktopVideoDevice + // + // A second-display presentation output that presents through a 10-bit + // Vulkan swapchain on Linux and Windows. The inherited transfer() and + // transfer2() composite into the view's QTVulkanVideoDevice; only the + // window lifecycle and presentation are overridden. + // + class VulkanDesktopVideoDevice : public DesktopVideoDevice + { + public: + VulkanDesktopVideoDevice(TwkApp::VideoModule* module, const std::string& name, int screen, const QTGLVideoDevice* shareDevice); + ~VulkanDesktopVideoDevice() override; + + // None of these chain to the base, which drives a QOpenGLWidget + // (m_view) that is never created here. + void open(const StringVector& args) override; + void close() override; + bool isOpen() const override; + void makeCurrent() const override; + + void redraw() const override; + void redrawImmediately() const override; + void syncBuffers() const override; + + private: + // Owns the QTVulkanVideoDevice used as the base m_viewDevice. + std::unique_ptr m_vulkanView; + }; + +} // namespace Rv + +#endif // __RvCommon__VulkanDesktopVideoDevice__h__ diff --git a/src/lib/app/RvCommon/RvCommon/VulkanView.h b/src/lib/app/RvCommon/RvCommon/VulkanView.h index 9f82a946e..dcd67d69c 100644 --- a/src/lib/app/RvCommon/RvCommon/VulkanView.h +++ b/src/lib/app/RvCommon/RvCommon/VulkanView.h @@ -3,55 +3,45 @@ // // SPDX-License-Identifier: Apache-2.0 // -#pragma once +#ifndef __RvCommon__VulkanView__h__ +#define __RvCommon__VulkanView__h__ -#include +#include #include -#include -#include -#include -#include - -#include -#include #include -#include -#include + +QT_BEGIN_NAMESPACE +class QWindow; +QT_END_NAMESPACE namespace Rv { class RvDocument; class QTVulkanVideoDevice; + class VulkanWindow; // // VulkanView // - // A QWidget subclass that presents 10-bit Vulkan images on Linux and Windows. - // All IPCore image processing runs in OpenGL via a separate - // QOpenGLContext+QOffscreenSurface; the Vulkan path is used only for - // final 10-bit pixel delivery to avoid 8-bit GLX visual truncation. + // Host QWidget that embeds the native Vulkan viewport (VulkanWindow) via + // QWidget::createWindowContainer(), and owns the QTVulkanVideoDevice that + // drives it. Mirrors GLView/GLWindow; the native window keeps the main + // window off Qt's render-to-texture composite path. // class VulkanView : public QWidget { Q_OBJECT public: - using Timer = TwkUtil::Timer; - - explicit VulkanView(RvDocument* doc, QWidget* parent = nullptr, bool noResize = true); + VulkanView(RvDocument* doc, QWidget* parent = nullptr, bool noResize = true); ~VulkanView() override; QTVulkanVideoDevice* videoDevice() const { return m_videoDevice.get(); } - void setEventWidget(QWidget* widget); - void stopProcessingEvents(); - bool event(QEvent* event) override; - bool eventFilter(QObject* object, QEvent* event) override; - - bool firstPaintCompleted() const { return m_firstPaintCompleted; } + bool firstPaintCompleted() const; void setContentSize(int w, int h) { m_csize = QSize(w, h); } @@ -61,317 +51,38 @@ namespace Rv QSize minimumSizeHint() const override { return m_msize; } - void absolutePosition(int& x, int& y) const; - - float devicePixelRatio() const; - - // Format of the active swapchain image: VK_FORMAT_A2B10G10R10_UNORM_PACK32 - // or VK_FORMAT_A2R10G10B10_UNORM_PACK32 (the two differ in R/B order). - // VK_FORMAT_UNDEFINED before the swapchain is created. - VkFormat swapchainFormat() const { return m_vkSwapchainFormat; } - - // - // Presentation path taken this session. RV prefers ZeroCopy, degrades - // to CpuReadback (slower, still 10-bit), and only then to OpenGL - // (which forgoes 10-bit). See emitPresentationRecord(). - // - enum class PresentPath - { - Undetermined, - ZeroCopy, // GL renders straight into a Vulkan-exported image - CpuReadback, // GL packs RGB10_A2 to host memory, Vulkan uploads it - OpenGL // Vulkan abandoned; RvDocument swaps in GLView - }; - - // Resolved GL<->Vulkan interop configuration. Negotiated once per - // device from what the driver reports exportable, never from GPU - // vendor identity or host platform. Both the Vulkan export and the GL - // import read their settings from this one struct so the two sides - // cannot disagree -- a disagreement about tiling or dedicated - // allocation corrupts the image rather than raising an error. - struct InteropConfig - { - bool supported{false}; - VkFormat format{VK_FORMAT_A2B10G10R10_UNORM_PACK32}; - VkImageTiling tiling{VK_IMAGE_TILING_LINEAR}; - VkImageUsageFlags usage{0}; - - // Probe-time floor for dedicated allocation: true when the handle - // type reports DEDICATED_ONLY, which is a hard requirement. The - // softer "prefers dedicated" signal belongs to a concrete image - // rather than to the format, so it is read per-image from - // VkMemoryDedicatedRequirements at allocation time and recorded in - // SharedImageInfo::dedicatedAllocation, which is what the GL side - // mirrors. - bool dedicatedAllocation{false}; - - // Raw VkExternalMemoryFeatureFlags the winning candidate reported, - // so a log read by someone without the machine can tell whether - // dedicated allocation was required by the handle type or merely - // preferred by the image. - VkExternalMemoryFeatureFlags externalFeatures{0}; - - // Set when an RV_VULKAN_FORCE_* override displaced what the probe - // would otherwise have chosen; the record reports both values. - bool tilingOverridden{false}; - bool dedicatedOverridden{false}; - VkImageTiling probedTiling{VK_IMAGE_TILING_LINEAR}; - bool probedDedicated{false}; - - // Why no candidate was usable (empty when supported is true). - std::string rejectReason; - // Per-candidate probe outcome, one entry per candidate tried. - std::vector candidateLog; - }; - - const InteropConfig& interopConfig() const { return m_interopConfig; } - - // Record the path actually taken, and the GL side's view of the shared - // image, then emit the one-per-session startup record. Called by - // QTVulkanVideoDevice once the first frame establishes which path ran. - void reportPresentPath(PresentPath path, const std::string& reason); - void reportGLImportState(VkImageTiling tiling, bool dedicated); - - // - // Vulkan presentation, called by QTVulkanVideoDevice::syncBuffers(). - // - - // GPU Interop API - // - // External handles to a Vulkan device-memory block (and its - // GL<->Vulkan sync semaphores) that GL imports as a memory - // object + semaphores. On Linux these are opaque file - // descriptors; on Windows they are Win32 HANDLEs. Stored as - // void* in the header to keep out of public Qt - // includes; the .cpp casts to HANDLE. - struct SharedImageInfo - { -#ifdef PLATFORM_WINDOWS - void* memoryHandle{nullptr}; // HANDLE; nullptr when unset - void* glReadySemaphoreHandle{nullptr}; - void* vkReadySemaphoreHandle{nullptr}; -#else - int memoryFd{-1}; // -1 when unset - int glReadySemaphoreFd{-1}; - int vkReadySemaphoreFd{-1}; -#endif - size_t size{0}; - int width{0}; // used sub-region width presented this frame - int height{0}; // used sub-region height presented this frame - int strideWidth{0}; // GL texture width = capacity rowPitch / 4 - int capacityHeight{0}; // allocated image height (>= height); GL texture height - - // The negotiated tiling this image was actually created with. GL - // must import with the matching GL_{OPTIMAL,LINEAR}_TILING_EXT: - // importing OPTIMAL-tiled memory as LINEAR yields an image whose - // large-scale structure survives but whose pixels are scrambled - // within each tile. - VkImageTiling tiling{VK_IMAGE_TILING_LINEAR}; - - // Whether the export used a dedicated allocation. GL must set - // GL_DEDICATED_MEMORY_OBJECT_EXT to exactly this before - // glTexStorageMem2DEXT; a mismatch corrupts the image. - bool dedicatedAllocation{false}; - }; - - // Number of frames the present path keeps in flight. Per-frame Vulkan - // sync objects and the GL<->Vulkan shared resources are stored in rings - // of this size and indexed by currentFrame(). 2 pipelines the present so - // a frame's GL work + submit can begin before the prior present retires; - // the throttle is FIFO acquire back-pressure + the start-of-frame fence - // wait (no per-frame end-of-frame block). - static constexpr uint32_t FRAMES_IN_FLIGHT = 2; - - // Index of the in-flight ring slot the next/current frame uses. The GL - // side (QTVulkanVideoDevice) reads this to pair its own ring objects with - // the Vulkan slot for the frame being rendered. - uint32_t currentFrame() const { return m_currentFrame; } - - const SharedImageInfo* getSharedImageInfo(int w, int h); - void presentSharedImage(); - - // CPU fallback API (not used when GPU interop is active) - void presentPixelData(const void* pixels, int w, int h); + // See VulkanWindow::supports10BitPresentation(). + static bool supports10BitPresentation(); + private: // - // Probe for whether this machine's Vulkan can present a 10-bit format - // (A2B10G10R10 or A2R10G10B10). Used at RvDocument construction time to - // decide whether a 10-bit display request should route to the Vulkan - // path or fall back to OpenGL. Creates a throwaway QVulkanInstance + - // dummy surface and queries the advertised surface formats; it never - // throws; returns false if Vulkan is unavailable for any reason. + // Keep the viewport window alive across top-level window churn. Qt + // recreates the top-level QWidgetWindow when a widget is reparented + // into it (e.g. a plugin adding a QWebEngineView), deleting the child + // viewport. destroyed() fires before children are deleted, so the + // viewport is detached there and re-attached later (as in GLView). // - static bool supports10BitPresentation(); - - public slots: - void eventProcessingTimeout(); + void watchParentWindow(); + void parentWindowDestroyed(); + void reattachVulkanWindow(); protected: - // Called once when the widget is first shown. - void initialize(); - - // Called each time a new frame should be rendered. - void render(); - - void showEvent(QShowEvent* event) override; - void resizeEvent(QResizeEvent* event) override; - void paintEvent(QPaintEvent* event) override; - - QPaintEngine* paintEngine() const override { return nullptr; } + void showEvent(QShowEvent*) override; private: - bool initVulkan(); - void cleanupVulkan(); - bool createSwapchain(); - void cleanupSwapchain(); - - // Probe the driver for an exportable shared-image configuration and - // resolve m_interopConfig. Runs exactly once per device, at device - // creation -- not per shared-image slot and not again on resize. - void negotiateInteropConfig(); - - // Emit the one-per-session startup record describing the negotiated - // configuration and the path taken. Unconditional: it must not be - // gated on ImageRenderer::debugGpu(), because Windows/NVIDIA is - // verified by QA against a build, and a log that needs a debug flag - // set in advance costs a whole verification round. - void emitPresentationRecord(); - - // Post a coalesced UpdateRequest: at most one render is queued at a time, - // so a burst of resize events collapses to a single render at the latest - // size instead of one heavy swapchain recreate per event. - void requestUpdate(); - RvDocument* m_doc; + VulkanWindow* m_vulkanWindow{nullptr}; + QWidget* m_container{nullptr}; std::unique_ptr m_videoDevice; - - bool m_initialized{false}; - bool m_firstPaintCompleted{false}; - bool m_postFirstNonEmptyRender; - bool m_stopProcessingEvents{false}; - bool m_userActive{true}; - bool m_updatePending{false}; - QSize m_csize{1024, 576}; QSize m_msize{128, 128}; - QWidget* m_eventWidget{nullptr}; - - unsigned int m_lastKey{0}; - QEvent::Type m_lastKeyType{QEvent::None}; - Timer m_activityTimer; - Timer m_activationTimer; - QTimer m_eventProcessingTimer; - - // Vulkan state - VkInstance m_vkInstance{VK_NULL_HANDLE}; - VkSurfaceKHR m_vkSurface{VK_NULL_HANDLE}; - VkPhysicalDevice m_vkPhysicalDevice{VK_NULL_HANDLE}; - VkDevice m_vkDevice{VK_NULL_HANDLE}; - VkQueue m_vkQueue{VK_NULL_HANDLE}; - uint32_t m_queueFamilyIndex{0}; - VkCommandPool m_vkCommandPool{VK_NULL_HANDLE}; - - VkSwapchainKHR m_vkSwapchain{VK_NULL_HANDLE}; - VkFormat m_vkSwapchainFormat{VK_FORMAT_UNDEFINED}; - VkExtent2D m_vkSwapchainExtent{}; - std::vector m_vkSwapchainImages; - std::vector m_vkCommandBuffers; - - // Per-in-flight-slot acquire semaphore + frame fence. - struct FrameSync - { - VkSemaphore imageAvailable{VK_NULL_HANDLE}; - VkFence fence{VK_NULL_HANDLE}; - }; - - // Per-in-flight-slot ring (indexed by m_currentFrame). - std::array m_frameSync{}; - uint32_t m_currentFrame{0}; - // Per-swapchain-image (indexed by imageIndex, sized to the swapchain - // image count, (re)built in createSwapchain / freed in cleanupSwapchain). - // The present-wait semaphore MUST be tied to the image, not the frame: - // with 2 frames in flight the same image can be re-acquired while its - // prior present is still pending, and reusing a per-frame semaphore there - // trips the present-semaphore-reuse validation error. m_imagesInFlight - // records which frame fence currently owns each image so a re-acquired - // in-flight image is waited on before reuse. - std::vector m_vkRenderFinished; - std::vector m_imagesInFlight; - - // CPU-fallback staging buffer, ringed per in-flight slot: the frame maps - // and overwrites it before acquiring, so with the per-frame block removed - // it must not alias a buffer whose copy from a still-in-flight frame is - // pending. The slot's frame fence (waited at frame start) gates reuse. - struct StagingBuffer - { - VkBuffer buffer{VK_NULL_HANDLE}; - VkDeviceMemory memory{VK_NULL_HANDLE}; - size_t size{0}; - }; - - std::array m_staging{}; - - // Shared Image for GPU Interop, ringed per in-flight slot (indexed by - // m_currentFrame). SharedImageInfo's default member initializers give the - // correct unset state (FDs/handles = -1/nullptr), so value-initializing - // the array is safe. - struct SharedImage - { - VkImage image{VK_NULL_HANDLE}; - VkDeviceMemory memory{VK_NULL_HANDLE}; - VkSemaphore glReady{VK_NULL_HANDLE}; - VkSemaphore vkReady{VK_NULL_HANDLE}; - SharedImageInfo info; - - // Grow-only allocated capacity of the shared image. A resize within - // capacity reuses the existing allocation/export (no rebuild, no FD - // re-export, no GL re-import); the image is only reallocated when the - // request exceeds capacity, at which point capacity grows to the - // componentwise max of the request and the screen size (monotonic). - int capacityW{0}; - int capacityH{0}; - }; - - std::array m_shared{}; - - void cleanupSharedImage(uint32_t slot); - - // Rebalance a slot's glReady/vkReady binary-semaphore pair when a frame is - // aborted at acquire time. The GL side (syncBuffers) has already signaled - // glReady[slot] and waited vkReady[slot] before the acquire result is - // known; if the frame returns without its normal submit, this issues a - // minimal submit that waits glReady[slot] and signals vkReady[slot] so the - // pair cannot desync across the skipped frame. - void drainSharedSemaphores(uint32_t slot); - - // Recreate swapchain (and shared image) after OUT_OF_DATE / SUBOPTIMAL. - void handleSwapchainOutOfDate(); - - // Queue a one-shot switch to GLView; no-op during shutdown. - void requestGLFallback(); - - // False while closing or when the widget has no drawable size. - bool presentationAllowed() const; - - bool m_glFallbackRequested{false}; - - // Negotiated interop configuration and the state behind the startup - // record. m_interopNegotiated guards the once-per-device probe; - // m_recordEmitted guards the once-per-session record. - InteropConfig m_interopConfig; - bool m_interopNegotiated{false}; - bool m_recordEmitted{false}; - - PresentPath m_presentPath{PresentPath::Undetermined}; - std::string m_presentPathReason; - VkColorSpaceKHR m_vkSwapchainColorSpace{VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; - - // What the GL side reported importing, so the record can show the two - // sides agreeing (or not) rather than only what Vulkan intended. - bool m_glImportReported{false}; - VkImageTiling m_glImportTiling{VK_IMAGE_TILING_LINEAR}; - bool m_glImportDedicated{false}; + // See watchParentWindow(). + QWindow* m_watchedParentWindow{nullptr}; + QMetaObject::Connection m_watchedParentConnection; + bool m_reattachPending{false}; }; } // namespace Rv + +#endif // __RvCommon__VulkanView__h__ diff --git a/src/lib/app/RvCommon/RvCommon/VulkanWindow.h b/src/lib/app/RvCommon/RvCommon/VulkanWindow.h new file mode 100644 index 000000000..caa7b87b5 --- /dev/null +++ b/src/lib/app/RvCommon/RvCommon/VulkanWindow.h @@ -0,0 +1,339 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef __RvCommon__VulkanWindow__h__ +#define __RvCommon__VulkanWindow__h__ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QPlatformWindow; +class QWidget; +QT_END_NAMESPACE + +namespace Rv +{ + class RvDocument; + class QTVulkanVideoDevice; + + // + // VulkanWindow + // + // The RV viewport as a native Vulkan surface, presenting 10-bit images on + // Linux and Windows. IPCore still renders in OpenGL (see + // QTVulkanVideoDevice); Vulkan only delivers the final pixels. + // + // Embedded by VulkanView via QWidget::createWindowContainer(), like + // GLWindow/GLView, which keeps the QMainWindow off Qt's render-to-texture + // composite path. + // + class VulkanWindow : public QWindow + { + Q_OBJECT + + public: + using Timer = TwkUtil::Timer; + + explicit VulkanWindow(RvDocument* doc, bool noResize = true); + ~VulkanWindow() override; + + QTVulkanVideoDevice* videoDevice() const { return m_videoDevice; } + + // Owned by the hosting VulkanView, not by this window. + void setVideoDevice(QTVulkanVideoDevice* device) { m_videoDevice = device; } + + // The container QWidget; used for focus and the popup check in render(). + void setEventWidget(QWidget* widget) { m_eventWidget = widget; } + + void stopProcessingEvents(); + + bool event(QEvent* event) override; + + bool firstPaintCompleted() const { return m_firstPaintCompleted; } + + void absolutePosition(int& x, int& y) const; + + float devicePixelRatioF() const; + + // A2B10G10R10 or A2R10G10B10 (R/B order differs); UNDEFINED before + // the swapchain exists. + VkFormat swapchainFormat() const { return m_vkSwapchainFormat; } + + // Presentation path taken, in order of preference. + enum class PresentPath + { + Undetermined, + ZeroCopy, // GL renders straight into a Vulkan-exported image + CpuReadback, // GL packs RGB10_A2 to host memory, Vulkan uploads it + OpenGL // Vulkan abandoned; RvDocument swaps in GLView + }; + + // GL<->Vulkan interop configuration, negotiated once per device from + // what the driver reports exportable. Both the Vulkan export and the + // GL import read it: a tiling or dedicated-allocation mismatch + // silently corrupts the image. + struct InteropConfig + { + bool supported{false}; + VkFormat format{VK_FORMAT_A2B10G10R10_UNORM_PACK32}; + VkImageTiling tiling{VK_IMAGE_TILING_LINEAR}; + VkImageUsageFlags usage{0}; + + // True when the handle type reports DEDICATED_ONLY. The per-image + // "prefers dedicated" result lives in + // SharedImageInfo::dedicatedAllocation. + bool dedicatedAllocation{false}; + + // Raw flags of the winning candidate, for the startup record. + VkExternalMemoryFeatureFlags externalFeatures{0}; + + // Set when an RV_VULKAN_FORCE_* override displaced what the probe + // would otherwise have chosen; the record reports both values. + bool tilingOverridden{false}; + bool dedicatedOverridden{false}; + VkImageTiling probedTiling{VK_IMAGE_TILING_LINEAR}; + bool probedDedicated{false}; + + // Why no candidate was usable (empty when supported is true). + std::string rejectReason; + // Per-candidate probe outcome, one entry per candidate tried. + std::vector candidateLog; + }; + + const InteropConfig& interopConfig() const { return m_interopConfig; } + + // Called by QTVulkanVideoDevice once the first frame establishes the + // path; emits the startup record. + void reportPresentPath(PresentPath path, const std::string& reason); + void reportGLImportState(VkImageTiling tiling, bool dedicated); + + // Lets the GL bridge refuse external-memory interop across GPUs. + bool physicalDeviceMatchesUUID(const unsigned char* uuid, size_t size) const; + + // External handles GL imports as a memory object and semaphores: + // opaque FDs on Linux, Win32 HANDLEs on Windows (void* to keep + // out of this header). + struct SharedImageInfo + { +#ifdef PLATFORM_WINDOWS + void* memoryHandle{nullptr}; // HANDLE; nullptr when unset + void* glReadySemaphoreHandle{nullptr}; + void* vkReadySemaphoreHandle{nullptr}; +#else + int memoryFd{-1}; // -1 when unset + int glReadySemaphoreFd{-1}; + int vkReadySemaphoreFd{-1}; +#endif + size_t size{0}; + int width{0}; // used sub-region width presented this frame + int height{0}; // used sub-region height presented this frame + int strideWidth{0}; // GL texture width = capacity rowPitch / 4 + int capacityHeight{0}; // allocated image height (>= height); GL texture height + // GL must import with the matching GL_{OPTIMAL,LINEAR}_TILING_EXT. + VkImageTiling tiling{VK_IMAGE_TILING_LINEAR}; + + // GL must set GL_DEDICATED_MEMORY_OBJECT_EXT to exactly this. + bool dedicatedAllocation{false}; + }; + + // Ring capacity. Runtime depth defaults to 1 for latency; + // RV_VULKAN_MAX_FRAMES_IN_FLIGHT=2 uses both slots. + static constexpr uint32_t kFramesInFlight = 2; + + // Ring slot for the current frame; the GL side pairs its objects to it. + uint32_t currentFrame() const { return m_currentFrame; } + + // A doc-less window is a passive presentation output, presented by its + // VulkanDesktopVideoDevice. It never drives or blocks the frame loop. + bool isPassiveOutput() const { return m_doc == nullptr; } + + // Checked before any GL work. False means skip the frame (a retry is + // armed). Always true for the control viewport. + bool canPresentNow(); + + const SharedImageInfo* getSharedImageInfo(int w, int h); + void presentSharedImage(); + + // CPU fallback API (not used when GPU interop is active) + void presentPixelData(const void* pixels, int w, int h); + + bool isInitialized() const { return m_initialized; } + + // Whether Vulkan can present a 10-bit format here. Never throws. + static bool supports10BitPresentation(); + + public slots: + void eventProcessingTimeout(); + + protected: + void initialize(); + void render(); + + void exposeEvent(QExposeEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + + private: + bool initVulkan(); + void cleanupVulkan(); + void requestBestEffortRetry(); + bool createSwapchain(); + void cleanupSwapchain(); + + // Recreates the swapchain if it no longer matches the surface extent. + bool ensureSwapchainMatchesSurface(); + + // Resolves m_interopConfig once per device (not per slot or resize). + void negotiateInteropConfig(); + + // Once per session. Unconditional: needed in logs without -debug gpu. + void emitPresentationRecord(); + + RvDocument* m_doc; + QTVulkanVideoDevice* m_videoDevice{nullptr}; + + bool m_initialized{false}; + + // Platform window the VkSurfaceKHR was created against; see + // handleSurfaceLost(). + const QPlatformWindow* m_initializedHandle{nullptr}; + + bool m_firstPaintCompleted{false}; + bool m_postFirstNonEmptyRender; + bool m_stopProcessingEvents{false}; + bool m_userActive{true}; + + QWidget* m_eventWidget{nullptr}; + + unsigned int m_lastKey{0}; + QEvent::Type m_lastKeyType{QEvent::None}; + Timer m_activityTimer; + Timer m_activationTimer; + // Forward-progress guard for canPresentNow(). + Timer m_lastPresentTimer; + QTimer m_eventProcessingTimer; + + VkInstance m_vkInstance{VK_NULL_HANDLE}; + VkSurfaceKHR m_vkSurface{VK_NULL_HANDLE}; + VkPhysicalDevice m_vkPhysicalDevice{VK_NULL_HANDLE}; + VkDevice m_vkDevice{VK_NULL_HANDLE}; + VkQueue m_vkQueue{VK_NULL_HANDLE}; + uint32_t m_queueFamilyIndex{0}; + // Logged only on change: createSwapchain() runs on every resize. + VkSurfaceFormatKHR m_loggedSurfaceFormat{}; + + bool m_loggedSurfaceFormatList{false}; + + bool m_externalInteropSupported{false}; + VkCommandPool m_vkCommandPool{VK_NULL_HANDLE}; + + VkSwapchainKHR m_vkSwapchain{VK_NULL_HANDLE}; + VkFormat m_vkSwapchainFormat{VK_FORMAT_UNDEFINED}; + VkExtent2D m_vkSwapchainExtent{}; + std::vector m_vkSwapchainImages; + std::vector m_vkCommandBuffers; + + // Per-in-flight-slot ring (indexed by m_currentFrame). + struct FrameSync + { + VkSemaphore imageAvailable{VK_NULL_HANDLE}; + VkFence fence{VK_NULL_HANDLE}; + }; + + std::array m_frameSync{}; + uint32_t m_currentFrame{0}; + + // Per swapchain image. The present-wait semaphore must be per image, + // not per frame: an image can be re-acquired while its present is still + // pending. m_imagesInFlight holds the frame fence owning each image. + std::vector m_vkRenderFinished; + std::vector m_imagesInFlight; + + // CPU-fallback staging buffer, per slot so a frame never overwrites a + // buffer an in-flight copy still reads; the slot fence gates reuse. + struct StagingBuffer + { + VkBuffer buffer{VK_NULL_HANDLE}; + VkDeviceMemory memory{VK_NULL_HANDLE}; + size_t size{0}; + }; + + std::array m_staging{}; + + // Shared interop image, per slot. + struct SharedImage + { + VkImage image{VK_NULL_HANDLE}; + VkDeviceMemory memory{VK_NULL_HANDLE}; + VkSemaphore glReadySemaphore{VK_NULL_HANDLE}; + VkSemaphore vkReadySemaphore{VK_NULL_HANDLE}; + SharedImageInfo info{}; + + // Grow-only capacity: a resize within it reuses the export and the + // GL import. + int capacityW{0}; + int capacityH{0}; + }; + + std::array m_shared{}; + + void cleanupSharedImage(uint32_t slot); + + // GL has already signaled glReady and waited vkReady when an acquire + // fails; this minimal submit keeps the semaphore pair balanced. + void drainSharedSemaphores(uint32_t slot); + + // After a failed fenced submit: re-signal the slot fence so the next + // wait on it cannot hang, consuming waitSemaphore if one is given. + void recoverFailedSubmit(uint32_t slot, VkSemaphore waitSemaphore); + + // Recreate the swapchain after OUT_OF_DATE. SUBOPTIMAL remains usable. + void handleSwapchainOutOfDate(); + + // Tear down and re-initialize after Qt destroyed and recreated the + // platform window, which invalidates the VkSurfaceKHR. + void handleSurfaceLost(); + + // Must run while the platform window (and the VkSurfaceKHR) is still + // alive; see the QEvent::PlatformSurface handler in event(). + void releaseVulkanResources(); + + // Queue a one-shot switch to GLView; no-op during shutdown. + void requestGLFallback(); + + // False while closing or when the widget has no drawable size. + bool presentationAllowed() const; + + bool m_glFallbackRequested{false}; + + InteropConfig m_interopConfig; + bool m_interopNegotiated{false}; + // Set when the shared image cannot be used with the current swapchain + // (no blit support, unaligned row pitch); cleared on swapchain rebuild. + bool m_sharedImageUnusable{false}; + bool m_recordEmitted{false}; + + PresentPath m_presentPath{PresentPath::Undetermined}; + std::string m_presentPathReason; + + // What GL reported importing, so the record shows both sides. + bool m_glImportReported{false}; + VkImageTiling m_glImportTiling{VK_IMAGE_TILING_LINEAR}; + bool m_glImportDedicated{false}; + }; + +} // namespace Rv + +#endif // __RvCommon__VulkanWindow__h__ diff --git a/src/lib/app/RvCommon/RvConsoleWindow.cpp b/src/lib/app/RvCommon/RvConsoleWindow.cpp index f4deb0acd..4a8ab8ce1 100644 --- a/src/lib/app/RvCommon/RvConsoleWindow.cpp +++ b/src/lib/app/RvCommon/RvConsoleWindow.cpp @@ -7,6 +7,7 @@ //****************************************************************************** #include +#include #include #include #include @@ -101,6 +102,9 @@ namespace Rv setWindowTitle(UI_APPLICATION_NAME " Console"); setWindowIcon(QIcon(qApp->applicationDirPath() + QString(RV_ICON_PATH_SUFFIX))); setSizeGripEnabled(true); + + // RV relies on quitOnLastWindowClosed; a log window must not keep it alive. + setAttribute(Qt::WA_QuitOnClose, false); bool doRedirect = (getenv("RV_NO_CONSOLE_REDIRECT") == 0); // setAttribute(Qt::WA_MacBrushedMetal); @@ -133,12 +137,25 @@ namespace Rv m_consoleBuf->sync(); processTextBuffer(); -#if defined(NDEBUG) || !defined(PLATFORM_WINDOWS) + // + // Restore cout/cerr whenever the redirect was installed: Py_Finalize + // can still write after this window is gone. The buffers are non-null + // only if the install ran, so this is safe in every build and twice. + // if (m_stdoutBuf) + { cout.rdbuf(m_stdoutBuf); + m_stdoutBuf = nullptr; + } + if (m_stderrBuf) + { cerr.rdbuf(m_stderrBuf); -#endif + m_stderrBuf = nullptr; + } + + delete m_consoleBuf; + m_consoleBuf = nullptr; } void RvConsoleWindow::processTimer() @@ -246,7 +263,8 @@ namespace Rv } } - if (shouldShow) + // Never re-show during shutdown: it would be the last visible window and block exit. + if (shouldShow && !(RvApp() && RvApp()->isShuttingDown())) { show(); raise(); diff --git a/src/lib/app/RvCommon/RvDocument.cpp b/src/lib/app/RvCommon/RvDocument.cpp index 74dd90f6a..cc0ca5094 100644 --- a/src/lib/app/RvCommon/RvDocument.cpp +++ b/src/lib/app/RvCommon/RvDocument.cpp @@ -27,10 +27,7 @@ #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) #include #include -// The X11/QtX11Extras includes (Linux-only, above) drag in GL headers that -// conflict with GLEW; only assert the include-order invariant on Linux. -// On Windows, GLEW is intentionally included first (see the -// PLATFORM_WINDOWS block higher up) and the conflict does not apply. +// Windows includes GLEW first on purpose; only Linux has the X11 conflict. #if defined(PLATFORM_LINUX) #ifdef __glew_h_ #error "GLEW IS DEFINED BEFORE QTGUI!" @@ -157,10 +154,10 @@ namespace Rv , m_closeEventReceived(false) , m_vsyncDisabled(false) , m_hdpiResizeWorkaroundDone(false) - , m_oldGLView(0) - , m_glView(0) , m_diagnosticsView(nullptr) , m_diagnosticsDock(nullptr) + , m_oldGLView(0) + , m_glView(0) , m_sourceEditor(0) , m_displayLink(0) , m_blockingOverlay(0) @@ -202,19 +199,12 @@ namespace Rv // #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) - // --- Backend selection: Vulkan for 10-bit, OpenGL otherwise --- // - // The display-depth preference is the user intent. A 10-bit request - // (RGB 10 + A 2) routes to the Vulkan presentation path, which avoids - // the 8-bit truncation that the OpenGL+Qt path is subject to on both - // Linux (GLX visual) and Windows (WGL pixel-format negotiation); - // everything else (8-bit, default) stays on the legacy OpenGL GLView. - // If 10-bit is requested but this machine's Vulkan cannot present - // 10-bit, we fall back to GLView and log why. The choice is made - // once per window at construction; changing the preference takes - // effect on the next launch / new window. + // 10-bit (RGB 10 + A 2) presents through Vulkan, since GLX/WGL + // pixel-format negotiation truncates to 8-bit; everything else stays + // on GLView. // - const bool want10bit = (opts.dispRedBits == 10 && opts.dispGreenBits == 10 && opts.dispBlueBits == 10 && opts.dispAlphaBits == 2); + const bool want10bit = DesktopVideoDevice::tenBitDisplayRequested(); if (ImageRenderer::debugGpu()) { @@ -222,13 +212,14 @@ namespace Rv << " A" << opts.dispAlphaBits << " -> want10bit=" << (want10bit ? "true" : "false") << endl; } - bool useVulkan = false; + // Shared with the presentation output so both stay on the same backend. + const bool useVulkan = DesktopVideoDevice::shouldUseVulkanPresentation(); + if (want10bit) { - useVulkan = VulkanView::supports10BitPresentation(); if (ImageRenderer::debugGpu()) { - cout << "INFO: RvDocument: supports10BitPresentation()=" << (useVulkan ? "true" : "false") << endl; + cout << "INFO: RvDocument: shouldUseVulkanPresentation()=" << (useVulkan ? "true" : "false") << endl; } if (!useVulkan) { @@ -245,7 +236,6 @@ namespace Rv if (useVulkan) { - // --- Vulkan path --- m_vulkanView = new VulkanView(this, m_centralWidget, !m_startupResize); m_vulkanView->setFocusPolicy(Qt::StrongFocus); @@ -253,16 +243,12 @@ namespace Rv m_vulkanView->setAcceptDrops(true); m_vulkanView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_vulkanView->resize(m_vulkanView->sizeHint()); - m_vulkanView->setEventWidget(m_vulkanView); m_viewWidget = m_vulkanView; m_vulkanView->videoDevice()->makeCurrent(); - - initializeSession(); } else { - // --- OpenGL path --- if (docs.empty()) { m_glView = @@ -273,9 +259,7 @@ namespace Rv { RvSession* s = static_cast(docs.front()); RvDocument* rvDoc = static_cast(s->opaquePointer()); - // The front document may be on the Vulkan/Metal path, where view() - // is null; share its GL context only if it has one (mirrors the - // first-window case above, which passes a null share context). + // view() is null if the front document is on Vulkan. QOpenGLContext* shareContext = rvDoc->view() ? rvDoc->view()->context() : nullptr; m_glView = new GLView(this, shareContext, this, opts.stereoMode && !strcmp(opts.stereoMode, "hardware"), opts.vsync != 0 && !m_vsyncDisabled, @@ -285,7 +269,6 @@ namespace Rv m_viewWidget = m_glView; } #else - // --- OpenGL path --- if (docs.empty()) { m_glView = @@ -296,8 +279,6 @@ namespace Rv { RvSession* s = static_cast(docs.front()); RvDocument* rvDoc = (RvDocument*)s->opaquePointer(); - // The front document may be on an alternative presentation path, where - // view() is null; share its GL context only if it has one. QOpenGLContext* shareContext = rvDoc->view() ? rvDoc->view()->context() : nullptr; m_glView = new GLView(this, shareContext, this, opts.stereoMode && !strcmp(opts.stereoMode, "hardware"), opts.vsync != 0 && !m_vsyncDisabled, @@ -305,27 +286,20 @@ namespace Rv opts.dispRedBits, opts.dispGreenBits, opts.dispBlueBits, opts.dispAlphaBits, !m_startupResize); } m_viewWidget = m_glView; -#endif // PLATFORM_LINUX +#endif - // DiagnosticsView is an independent QOpenGLWidget with its own GL context; - // it only needs a valid surface format, not the main view's context. On the - // Vulkan/Metal presentation path m_glView is null, so fall back to the global - // default format (OpenGL 2.1, set in RV/main.cpp) which ImGui's GL2 backend - // expects. + // DiagnosticsView has its own context and only needs a surface format. const QSurfaceFormat diagnosticsFormat = m_glView ? m_glView->format() : QSurfaceFormat::defaultFormat(); m_diagnosticsView = new DiagnosticsView(nullptr, diagnosticsFormat); // Dockable to QMainWindow, not centralwidget. - if (m_diagnosticsView) - { - m_diagnosticsDock = new QDockWidget(tr("Diagnostics"), this); - m_diagnosticsDock->setObjectName("Diagnostics"); - m_diagnosticsDock->setWidget(m_diagnosticsView); - m_diagnosticsDock->setAllowedAreas(Qt::AllDockWidgetAreas); - addDockWidget(Qt::BottomDockWidgetArea, m_diagnosticsDock); - m_diagnosticsDock->hide(); // Hide by default - m_diagnosticsView->setWindowFlag(Qt::Widget); // Not a top-level window - } + m_diagnosticsDock = new QDockWidget(tr("Diagnostics"), this); + m_diagnosticsDock->setObjectName("Diagnostics"); + m_diagnosticsDock->setWidget(m_diagnosticsView); + m_diagnosticsDock->setAllowedAreas(Qt::AllDockWidgetAreas); + addDockWidget(Qt::BottomDockWidgetArea, m_diagnosticsDock); + m_diagnosticsDock->hide(); // Hide by default + m_diagnosticsView->setWindowFlag(Qt::Widget); // Not a top-level window m_stackedLayout = new QStackedLayout(m_centralWidget); m_stackedLayout->setStackingMode(QStackedLayout::StackAll); @@ -380,11 +354,12 @@ namespace Rv // input and dims the UI. // // 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. + // than a child widget. The viewport is a native window on both + // backends (QOpenGLWindow or a Vulkan surface), 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"); @@ -420,29 +395,26 @@ namespace Rv void RvDocument::initializeSession() { // - // On the OpenGL path this is 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 + // 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. // - // On the Vulkan/Metal presentation path there is no GLView. Those - // views make their own GL context current and call in here themselves - // once initialized, so the guard is on having *a* view, not on having - // a GLView, and makeCurrent() is skipped when GLView is absent. + // Not driven from a view init callback: loading packages can add a + // QWebEngineView, which destroys the viewport's native window while + // that callback is still on the stack. // - if (!m_viewWidget) + TwkGLF::GLVideoDevice* viewDevice = viewVideoDevice(); + + if (!viewDevice) { return; } if (!m_session) { - if (m_glView) - { - m_glView->makeCurrent(); - } + viewDevice->makeCurrent(); m_session = new RvSession; // m_session->setFrameBuffer(fb); @@ -619,6 +591,10 @@ namespace Rv // Then this is the last document, so shutdown network // + // Before closing anything, so the console's queued auto-show cannot + // reopen it and keep the application alive. + RvApp()->setShuttingDown(); + if (RvNetworkDialog* d = RvApp()->networkWindow()) { if (d->serverRunning()) @@ -718,8 +694,7 @@ namespace Rv } else if (m == IPCore::Session::eventDeviceChangedMessage()) { - // translator() is not on the shared GLVideoDevice base, so it is - // accessed via the concrete backend view here (still null-safe). + // translator() is not on the GLVideoDevice base. if (m_session->eventVideoDevice()) { const int w = m_session->eventVideoDevice()->width(); @@ -926,14 +901,28 @@ namespace Rv return; } - cout << "INFO: Vulkan 10-bit presentation failed at runtime; falling back to OpenGL." << endl; + Rv::Options& opts = Options::sharedOptions(); + const bool requestedTenBit = DesktopVideoDevice::tenBitDisplayRequested(); + if (requestedTenBit) + { + cout << "INFO: Vulkan 10-bit presentation failed at runtime; falling back to 8-bit OpenGL." << endl; + } + else + { + cout << "INFO: Switching the main view from Vulkan to OpenGL for the requested display depth." << endl; + } VulkanView* oldVulkanView = m_vulkanView; m_vulkanView = nullptr; oldVulkanView->stopProcessingEvents(); - Rv::Options& opts = Options::sharedOptions(); + // OpenGL cannot provide the 10/10/10/2 surface that required Vulkan, so + // the recovery view is 8-bit; the persisted 10-bit preference is kept. + const int fallbackRedBits = requestedTenBit ? 8 : opts.dispRedBits; + const int fallbackGreenBits = requestedTenBit ? 8 : opts.dispGreenBits; + const int fallbackBlueBits = requestedTenBit ? 8 : opts.dispBlueBits; + const int fallbackAlphaBits = requestedTenBit ? 8 : opts.dispAlphaBits; const TwkApp::Application::Documents& docs = TwkApp::App()->documents(); GLView* newGLView = nullptr; @@ -941,7 +930,7 @@ namespace Rv { newGLView = new GLView(this, 0, this, opts.stereoMode && !strcmp(opts.stereoMode, "hardware"), opts.vsync != 0 && !m_vsyncDisabled, - true, opts.dispRedBits, opts.dispGreenBits, opts.dispBlueBits, opts.dispAlphaBits, !m_startupResize); + true, fallbackRedBits, fallbackGreenBits, fallbackBlueBits, fallbackAlphaBits, !m_startupResize); } else { @@ -949,8 +938,8 @@ namespace Rv RvDocument* rvDoc = static_cast(s->opaquePointer()); QOpenGLContext* shareContext = rvDoc->view() ? rvDoc->view()->context() : nullptr; newGLView = new GLView(this, shareContext, this, opts.stereoMode && !strcmp(opts.stereoMode, "hardware"), - opts.vsync != 0 && !m_vsyncDisabled, true, opts.dispRedBits, opts.dispGreenBits, opts.dispBlueBits, - opts.dispAlphaBits, !m_startupResize); + opts.vsync != 0 && !m_vsyncDisabled, true, fallbackRedBits, fallbackGreenBits, fallbackBlueBits, + fallbackAlphaBits, !m_startupResize); } newGLView->setContentSize(oldVulkanView->sizeHint().width(), oldVulkanView->sizeHint().height()); @@ -985,21 +974,87 @@ namespace Rv m_glView->videoDevice()->sendEvent(TwkApp::RenderContextChangeEvent("gl-context-changed", m_glView->videoDevice())); } - if (DesktopVideoModule* m = RvApp()->desktopVideoModule()) + RvApp()->rebuildDesktopVideoDevices(m_session, m_glView->videoDevice(), false); + + // Deferred: this can be reached from inside the VulkanView's present path. + oldVulkanView->deleteLater(); + + newGLView->videoDevice()->makeCurrent(); + newGLView->update(); + } + + // Hot-swap GLView -> VulkanView and rebind the live session to the Vulkan device. + void RvDocument::swapGLViewToVulkan() + { + if (!m_glView || isClosing()) + { + return; + } + + cout << "INFO: RvDocument: switching main view from OpenGL to Vulkan." << endl; + + // Flush any GLView pending from an earlier swap before reusing m_oldGLView. + lazyDeleteGLView(); + + GLView* oldGLView = m_glView; + const Qt::KeyboardModifiers cur = oldGLView->videoDevice()->translator().currentModifiers(); + oldGLView->stopProcessingEvents(); + + VulkanView* newVulkanView = new VulkanView(this, m_centralWidget, !m_startupResize); + + newVulkanView->setContentSize(oldGLView->sizeHint().width(), oldGLView->sizeHint().height()); + newVulkanView->setMinimumContentSize(oldGLView->minimumSizeHint().width(), oldGLView->minimumSizeHint().height()); + newVulkanView->setMinimumSize(QSize(oldGLView->minimumSizeHint().width(), oldGLView->minimumSizeHint().height())); + newVulkanView->setFocusPolicy(Qt::StrongFocus); + newVulkanView->setMouseTracking(true); + newVulkanView->setAcceptDrops(true); + newVulkanView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + m_stackedLayout->addWidget(newVulkanView); + m_stackedLayout->removeWidget(oldGLView); + + // + // Vulkan initializes asynchronously from exposeEvent(), so commit now; + // a failure falls back through requestGLFallback(), which is guarded + // on m_vulkanView, so assign it before showing. + // + m_vulkanView = newVulkanView; + m_viewWidget = newVulkanView; + + // Backend-neutral code keys the active backend on m_glView being null. + m_glView = nullptr; + + m_vulkanView->show(); + m_viewWidget->setFocus(Qt::OtherFocusReason); + + m_topViewToolBar->setDevice(m_vulkanView->videoDevice()); + + if (m_session) { - for (TwkApp::VideoDevice* device : m->devices()) + const bool same = m_session->outputVideoDevice() == m_session->controlVideoDevice(); + m_session->setEventVideoDevice(0); + m_session->setOutputVideoDevice(0); + m_session->setControlVideoDevice(m_vulkanView->videoDevice()); + if (same) { - if (DesktopVideoDevice* desktopDevice = dynamic_cast(device)) - { - desktopDevice->setShareDevice(m_glView->videoDevice()); - } + m_session->setOutputVideoDevice(m_vulkanView->videoDevice()); } + + m_vulkanView->videoDevice()->sendEvent(TwkApp::RenderContextChangeEvent("vulkan-context-changed", m_vulkanView->videoDevice())); } - delete oldVulkanView; + // No GL share device on Vulkan; Qt::AA_ShareOpenGLContexts still shares. + RvApp()->rebuildDesktopVideoDevices(m_session, nullptr, true); - newGLView->videoDevice()->makeCurrent(); - newGLView->update(); + m_vulkanView->videoDevice()->translator().setCurrentModifiers(cur); + + // Lazy-delete, as in rebuildGLView: deleting inline can crash. + m_oldGLView = oldGLView; + m_oldGLView->hide(); + QTimer::singleShot(100, this, SLOT(lazyDeleteGLView())); + + m_vulkanView->videoDevice()->makeCurrent(); + m_vulkanView->update(); } #endif @@ -1028,12 +1083,6 @@ namespace Rv void RvDocument::rebuildGLView(bool stereo, bool vsync, bool doubleBuffer, int red, int green, int blue, int alpha) { - // - // Rebuilding the GLView only makes sense on the OpenGL path. On the - // Vulkan/Metal presentation path m_glView is null and every deref below - // would crash. Current callers already bail when !m_glView, so this is a - // defensive guard against future callers. - // if (!m_glView) { return; @@ -1099,18 +1148,7 @@ namespace Rv if (resetGLPrefs) resetGLStateAndPrefs(); - if (DesktopVideoModule* m = RvApp()->desktopVideoModule()) - { - const TwkApp::VideoModule::VideoDevices& devices = m->devices(); - - for (size_t i = 0; i < devices.size(); i++) - { - if (DesktopVideoDevice* d = dynamic_cast(devices[i])) - { - d->setShareDevice(m_glView->videoDevice()); - } - } - } + RvApp()->rebuildDesktopVideoDevices(m_session, m_glView->videoDevice(), false); m_glView->videoDevice()->translator().setCurrentModifiers(cur); m_oldGLView = oldGLView; @@ -1121,16 +1159,18 @@ namespace Rv void RvDocument::showDiagnostics() { if (m_diagnosticsDock) + { m_diagnosticsDock->show(); + } } void RvDocument::setStereo(bool b) { #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) - // GL-format queries below are only valid on the OpenGL path; on the - // Vulkan presentation path m_glView is null. if (!m_glView) + { return; + } #endif const bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); @@ -1151,7 +1191,9 @@ namespace Rv return; #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) if (!m_glView) + { return; + } #endif const bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); @@ -1170,7 +1212,9 @@ namespace Rv { #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) if (!m_glView) + { return; + } #endif bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); @@ -1187,9 +1231,71 @@ namespace Rv void RvDocument::setDisplayOutput(DisplayOutputType type) { + // Persist the requested depth first, so an early return below, a later + // Vulkan -> GL fallback and the next launch all honour it. + { + const int bits = (type == OpenGL8888) ? 8 : (type == OpenGL1010102 ? 10 : 0); + const int alphaBits = (type == OpenGL8888) ? 8 : (type == OpenGL1010102 ? 2 : 0); + + Rv::Options& opts = Options::sharedOptions(); + opts.dispRedBits = bits; + opts.dispGreenBits = bits; + opts.dispBlueBits = bits; + opts.dispAlphaBits = alphaBits; + + { + RV_QSETTINGS; + settings.beginGroup("Display"); + settings.setValue("dispRedBits", bits); + settings.setValue("dispGreenBits", bits); + settings.setValue("dispBlueBits", bits); + settings.setValue("dispAlphaBits", alphaBits); + settings.endGroup(); + } + } + #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + // + // 10-bit goes through Vulkan: an OpenGL context rebuild at 10-bit + // fails on Mesa GLX and WGL. + // + if (type == OpenGL1010102) + { + // Already on Vulkan. + if (!m_glView) + { + return; + } + + if (VulkanView::supports10BitPresentation()) + { + swapGLViewToVulkan(); + return; + } + + QMessageBox box(this); + box.setWindowModality(Qt::WindowModal); +#ifdef PLATFORM_LINUX + box.setIconPixmap(QPixmap(qApp->applicationDirPath() + QString(RV_ICON_PATH_SUFFIX)).scaledToHeight(64)); +#else + box.setIcon(QMessageBox::Critical); +#endif + box.setWindowTitle(tr(UI_APPLICATION_NAME ": 10-bit Display Output Unavailable")); + box.setText(tr("This display cannot present 10-bit output")); + box.setInformativeText( + tr("This graphics hardware or driver does not provide a 10-bit presentation surface. " UI_APPLICATION_NAME + " cannot output 10-bit on this display and will continue in 8-bit.")); + + box.exec(); + return; + } + + // Leaving 10-bit while Vulkan is live: swap back to OpenGL. if (!m_glView) + { + fallbackVulkanToGLView(); return; + } #endif const bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); @@ -2457,7 +2563,9 @@ namespace Rv { TwkApp::VideoDevice* vdev = viewVideoDevice(); if (!vdev) + { return; + } TwkApp::GenericStringEvent event("file-changed", vdev, path.toUtf8().data()); vdev->sendEvent(event); } diff --git a/src/lib/app/RvCommon/RvPreferences.cpp b/src/lib/app/RvCommon/RvPreferences.cpp index d47c33e0a..35295dfb3 100644 --- a/src/lib/app/RvCommon/RvPreferences.cpp +++ b/src/lib/app/RvCommon/RvPreferences.cpp @@ -128,6 +128,9 @@ namespace Rv connect(m_ui.exrNumThreadsEdit, SIGNAL(textChanged(const QString&)), this, SLOT(exrThreadNumChanged(const QString&))); setWindowTitle(UI_APPLICATION_NAME " Preferences"); + // Auxiliary window: never keeps RV alive. + setAttribute(Qt::WA_QuitOnClose, false); + #ifndef PLATFORM_DARWIN m_ui.appleClientStorageToggle->setEnabled(false); #endif diff --git a/src/lib/app/RvCommon/RvProfileManager.cpp b/src/lib/app/RvCommon/RvProfileManager.cpp index 298925842..6fd0a8b4c 100644 --- a/src/lib/app/RvCommon/RvProfileManager.cpp +++ b/src/lib/app/RvCommon/RvProfileManager.cpp @@ -33,6 +33,10 @@ namespace Rv , m_createDialog(0) { m_ui.setupUi(this); + + // Auxiliary window: never keeps RV alive. + setAttribute(Qt::WA_QuitOnClose, false); + loadModel(); m_createDialogUI.setupUi(m_createDialog = new QDialog(this, Qt::Sheet)); diff --git a/src/lib/app/RvCommon/VulkanDesktopVideoDevice.cpp b/src/lib/app/RvCommon/VulkanDesktopVideoDevice.cpp new file mode 100644 index 000000000..70a5d7402 --- /dev/null +++ b/src/lib/app/RvCommon/VulkanDesktopVideoDevice.cpp @@ -0,0 +1,118 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + +#include +#include +#include +#include + +namespace Rv +{ + using namespace std; + + VulkanDesktopVideoDevice::VulkanDesktopVideoDevice(TwkApp::VideoModule* module, const std::string& name, int screen, + const QTGLVideoDevice* shareDevice) + : DesktopVideoDevice(module, name, screen, shareDevice) + { + // + // Re-advertise at 10-bit (the base advertised RGB8); only built once + // the 10-bit probe has passed. The format indices are unchanged, so + // the persisted "dataFormat" preference survives a backend rebuild. + // + m_dataFormats.clear(); + addDefaultDataFormats(10); + } + + VulkanDesktopVideoDevice::~VulkanDesktopVideoDevice() { close(); } + + void VulkanDesktopVideoDevice::open(const StringVector& args) + { + if (isOpen()) + { + close(); + } + + // A null doc makes the view passive (see VulkanWindow.h). + m_vulkanView = std::make_unique(/*doc*/ nullptr, /*parent*/ nullptr, /*noResize*/ true); + + // The view owns this device. There is no QOpenGLWidget, so this + // bypasses setViewWidget() and installs the translator by hand. + setViewDevice(m_vulkanView->videoDevice()); + + // A focusable second top-level fights the main window for activation. + m_vulkanView->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_vulkanView->setWindowFlag(Qt::WindowDoesNotAcceptFocus, true); + + // Inert, created for parity with the base class's setViewWidget(). + m_translator = new QTTranslator(this, m_vulkanView.get()); + + // Place before show(): the swapchain is built on first expose. + const QRect screenRect = screenGeometry(); + m_vulkanView->move(screenRect.x(), screenRect.y()); + m_vulkanView->setGeometry(screenRect); + + m_vulkanView->setWindowState(useFullScreen() ? Qt::WindowFullScreen : Qt::WindowNoState); + + m_vulkanView->setGeometry(screenRect); + + m_vulkanView->show(); + + // Create the FBO now so transfer()'s fboID() readiness check passes on + // the first frame. + if (m_viewDevice) + { + m_viewDevice->makeCurrent(); + } + } + + void VulkanDesktopVideoDevice::close() + { + // + // The view owns m_viewDevice, so do not chain to the base close(), + // which would delete it again. releaseFBOClones() needs the device + // current, so it runs before setViewDevice(nullptr). + // + releaseFBOClones(); + + setViewDevice(nullptr); + + m_vulkanView.reset(); + + delete m_translator; + m_translator = nullptr; + } + + bool VulkanDesktopVideoDevice::isOpen() const { return m_vulkanView != nullptr; } + + void VulkanDesktopVideoDevice::makeCurrent() const + { + if (m_viewDevice) + { + m_viewDevice->makeCurrent(); + } + } + + void VulkanDesktopVideoDevice::redraw() const + { + // Intentionally empty: presenting here would steal the current GL + // context mid-draw. The control device's render() presents this device + // in-frame via syncBuffers(). + } + + void VulkanDesktopVideoDevice::redrawImmediately() const { redraw(); } + + void VulkanDesktopVideoDevice::syncBuffers() const + { + if (m_viewDevice && m_vulkanView && m_vulkanView->isVisible()) + { + m_viewDevice->syncBuffers(); + } + } + +} // namespace Rv + +#endif // PLATFORM_LINUX || PLATFORM_WINDOWS diff --git a/src/lib/app/RvCommon/VulkanView.cpp b/src/lib/app/RvCommon/VulkanView.cpp index 81e91971b..d4b8f6406 100644 --- a/src/lib/app/RvCommon/VulkanView.cpp +++ b/src/lib/app/RvCommon/VulkanView.cpp @@ -7,2677 +7,269 @@ #if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) #include +#include #include #include -#include #include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#ifdef PLATFORM_WINDOWS -// WIN32_LEAN_AND_MEAN prevents from including the legacy -// , which otherwise collides with the already -// pulled in transitively by Qt headers above. -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#else -#include -#endif - -// -// Environment variables recognized by the Vulkan presentation path -// ---------------------------------------------------------------- -// These exist so a corrupted or failing display can be narrowed to a stage -// without a rebuild -- notably by a tester running a build on hardware the -// developer cannot access. Every override is reported in the startup record -// (see VulkanView::emitPresentationRecord), alongside the value negotiation -// would otherwise have chosen. -// -// RV_VULKAN_FORCE_CPU_PRESENT -// Set (to any value) to skip GL<->Vulkan zero-copy interop entirely and -// present via the CPU readback path. Still 10-bit, just slower. -// -// RV_VULKAN_FORCE_TILING = optimal | linear -// Override the negotiated shared-image tiling. The override is honored -// only if the driver reports that tiling as exportable; otherwise it is -// logged and refused, because presenting through a configuration whose -// correctness was not established is what this path is meant to avoid. -// An unrecognized value is logged and ignored (negotiation proceeds). -// -// RV_VULKAN_FORCE_NO_DEDICATED -// Set (to any value) to suppress dedicated allocation even when the -// driver reports it as preferred. Refused when the driver reports -// DEDICATED_ONLY, since that is a requirement rather than a preference. -// -// Both sides of the interop read their settings from one negotiated struct, -// so an override applies to the Vulkan export and the GL import together. -// namespace Rv { using namespace std; - using namespace TwkApp; - using namespace IPCore; - namespace + VulkanView::VulkanView(RvDocument* doc, QWidget* parent, bool noResize) + : QWidget(parent) + , m_doc(doc) { - // Read an env var that is treated as a boolean flag by presence. - bool envFlagSet(const char* name) { return getenv(name) != nullptr; } - - constexpr std::string_view tilingName(VkImageTiling tiling) - { - switch (tiling) - { - case VK_IMAGE_TILING_OPTIMAL: - return "OPTIMAL"; - case VK_IMAGE_TILING_LINEAR: - return "LINEAR"; - default: - return "(other)"; - } - } - - constexpr std::string_view colorSpaceName(VkColorSpaceKHR colorSpace) - { - switch (colorSpace) - { - case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: - return "SRGB_NONLINEAR"; - case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: - return "EXTENDED_SRGB_LINEAR"; - case VK_COLOR_SPACE_EXTENDED_SRGB_NONLINEAR_EXT: - return "EXTENDED_SRGB_NONLINEAR"; - case VK_COLOR_SPACE_HDR10_ST2084_EXT: - return "HDR10_ST2084"; - case VK_COLOR_SPACE_HDR10_HLG_EXT: - return "HDR10_HLG"; - case VK_COLOR_SPACE_BT2020_LINEAR_EXT: - return "BT2020_LINEAR"; - case VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT: - return "DISPLAY_P3_NONLINEAR"; - case VK_COLOR_SPACE_PASS_THROUGH_EXT: - return "PASS_THROUGH"; - default: - return "(other)"; - } - } + m_vulkanWindow = new VulkanWindow(doc, noResize); + m_container = QWidget::createWindowContainer(m_vulkanWindow, this); - // Decode RV_VULKAN_FORCE_TILING. Returns nullopt when unset or when the - // value is not recognized; an unrecognized value is reported rather - // than silently behaving as if the variable were unset. - std::optional forcedTilingRequested() - { - const char* value = getenv("RV_VULKAN_FORCE_TILING"); - if (!value) - { - return std::nullopt; - } + // A doc-less view is a passive presentation output (see VulkanWindow.h). + const bool passiveOutput = (m_doc == nullptr); - std::string lowered(value); - std::transform(lowered.begin(), lowered.end(), lowered.begin(), - [](unsigned char ch) { return static_cast(::tolower(ch)); }); + m_container->setFocusPolicy(passiveOutput ? Qt::NoFocus : Qt::StrongFocus); - if (lowered == "optimal") - { - return VK_IMAGE_TILING_OPTIMAL; - } - if (lowered == "linear") - { - return VK_IMAGE_TILING_LINEAR; - } + // RV queries the presentation device before the window is shown, and + // Qt only hands out a VkSurfaceKHR once the platform surface exists. + m_vulkanWindow->create(); - cout << "WARNING: VulkanView: RV_VULKAN_FORCE_TILING='" << value << "' is not recognized (expected 'optimal' or 'linear'); " - << "ignoring it and using the negotiated tiling" << endl; - return std::nullopt; - } + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_container); - // Both A2B10G10R10 and A2R10G10B10 are 10-bit-per-channel packed formats; - // they differ only in R/B component order. Both are acceptable for 10-bit - // presentation -- the R/B order is handled where pixels are packed (CPU - // fallback) or blitted (GPU interop). A2B10G10R10 (== GL_RGB10_A2) is - // preferred when the surface offers it, but many Linux/RADV surfaces only - // advertise A2R10G10B10. - bool isTenBitFormat(VkFormat format) - { - return format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; - } + // Last-resort guard in case parentWindowDestroyed() did not detach it. + connect(m_vulkanWindow, &QObject::destroyed, this, [this]() { m_vulkanWindow = nullptr; }); - constexpr std::string_view formatName(VkFormat format) + ostringstream str; + if (m_doc) { - switch (format) - { - case VK_FORMAT_B8G8R8A8_UNORM: - return "B8G8R8A8_UNORM"; - case VK_FORMAT_B8G8R8A8_SRGB: - return "B8G8R8A8_SRGB"; - case VK_FORMAT_R8G8B8A8_UNORM: - return "R8G8B8A8_UNORM"; - case VK_FORMAT_R8G8B8A8_SRGB: - return "R8G8B8A8_SRGB"; - case VK_FORMAT_A2B10G10R10_UNORM_PACK32: - return "A2B10G10R10_UNORM_PACK32"; - case VK_FORMAT_A2R10G10B10_UNORM_PACK32: - return "A2R10G10B10_UNORM_PACK32"; - case VK_FORMAT_R16G16B16A16_SFLOAT: - return "R16G16B16A16_SFLOAT"; - default: - return "(other)"; - } + str << UI_APPLICATION_NAME " Main Window (Vulkan)" << "/" << m_doc; } - - std::optional findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) + else { - VkPhysicalDeviceMemoryProperties memProperties; - vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); - for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) - { - if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) - { - return i; - } - } - return std::nullopt; + // One output per screen, all doc-less: key the name on the view. + str << UI_APPLICATION_NAME " Presentation (Vulkan)" << "/" << static_cast(this); } + // No event widget, so no QTTranslator: VulkanWindow::event() then + // ignores all input for a passive output. + m_videoDevice = std::make_unique(nullptr, str.str(), m_vulkanWindow, passiveOutput ? nullptr : m_container); + m_vulkanWindow->setVideoDevice(m_videoDevice.get()); + m_vulkanWindow->setEventWidget(passiveOutput ? nullptr : m_container); - // Resolve a device-level Vulkan entry point as its PFN type. - template Fn deviceProc(VkDevice device, const char* name) - { - return reinterpret_cast(vkGetDeviceProcAddr(device, name)); - } + setObjectName((m_doc && m_doc->session()) ? m_doc->session()->name().c_str() : "no session"); - // Record a single-image layout transition (color aspect, 1 mip, 1 layer, - // no queue family ownership transfer). - void imageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, - VkAccessFlags srcAccess, VkAccessFlags dstAccess, VkPipelineStageFlags srcStage, VkPipelineStageFlags dstStage) + if (!passiveOutput) { - VkImageMemoryBarrier barrier = {}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = oldLayout; - barrier.newLayout = newLayout; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = image; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = srcAccess; - barrier.dstAccessMask = dstAccess; - - vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); + setFocusProxy(m_container); } - } // namespace - - //-------------------------------------------------------------------------- - // VulkanView implementation - //-------------------------------------------------------------------------- - - VulkanView::VulkanView(RvDocument* doc, QWidget* parent, bool noResize) - : QWidget(parent) - , m_doc(doc) - , m_postFirstNonEmptyRender(noResize) - { - // Force the creation of a native window early. - setAttribute(Qt::WA_NativeWindow); - setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_OpaquePaintEvent); - setAttribute(Qt::WA_PaintOnScreen); - setAttribute(Qt::WA_TranslucentBackground); - setAutoFillBackground(false); - - // Wait to configure the QWindow until it's created - if (QWindow* window = windowHandle()) + else { - window->setSurfaceType(QSurface::VulkanSurface); - - // Set 10-bit format - QSurfaceFormat fmt; - fmt.setRedBufferSize(10); - fmt.setGreenBufferSize(10); - fmt.setBlueBufferSize(10); - fmt.setAlphaBufferSize(2); - window->setFormat(fmt); + setFocusPolicy(Qt::NoFocus); } - ostringstream str; - str << UI_APPLICATION_NAME " Main Window (Vulkan)" << "/" << m_doc; - m_videoDevice = std::make_unique(nullptr, str.str(), this, nullptr); - - m_activityTimer.start(); - - m_eventProcessingTimer.setSingleShot(true); - connect(&m_eventProcessingTimer, SIGNAL(timeout()), this, SLOT(eventProcessingTimeout())); + watchParentWindow(); } VulkanView::~VulkanView() { - // Release the device (and its GL imports) before the Vulkan memory they alias. - m_videoDevice.reset(); - for (uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) + // + // A detached viewport window (see parentWindowDestroyed()) outlives the + // widget tree and holds a raw pointer to the device, and a detached + // container is not deleted with this widget. Undo both here. + // + // This must run before ~QWidget, which emits destroyed() from children + // after the VulkanView sub-object is gone; delivering it to our slots + // would trip Qt's assertObjectType or write through a dangling `this`. + // + if (m_watchedParentConnection) { - cleanupSharedImage(i); + disconnect(m_watchedParentConnection); + m_watchedParentConnection = QMetaObject::Connection(); } - cleanupSwapchain(); - cleanupVulkan(); - } + m_watchedParentWindow = nullptr; - //-------------------------------------------------------------------------- - - void VulkanView::setEventWidget(QWidget* widget) - { - m_eventWidget = widget; - if (m_videoDevice) + if (m_vulkanWindow) { - m_videoDevice->setEventWidget(widget); - } - } - - void VulkanView::stopProcessingEvents() { m_stopProcessingEvents = true; } - - void VulkanView::absolutePosition(int& x, int& y) const - { - QPoint gp = mapToGlobal(QPoint(0, 0)); - x = gp.x(); - y = gp.y(); - } - - float VulkanView::devicePixelRatio() const { return static_cast(devicePixelRatioF()); } - - //-------------------------------------------------------------------------- - // Vulkan Initialisation - //-------------------------------------------------------------------------- + disconnect(m_vulkanWindow, nullptr, this, nullptr); - void VulkanView::initialize() - { - if (m_initialized) - { - return; + m_vulkanWindow->setVideoDevice(nullptr); + m_vulkanWindow->setEventWidget(nullptr); } - if (QWindow* window = windowHandle()) + if (m_container && !m_container->parentWidget()) { - window->setSurfaceType(QSurface::VulkanSurface); - QSurfaceFormat fmt; - fmt.setRedBufferSize(10); - fmt.setGreenBufferSize(10); - fmt.setBlueBufferSize(10); - fmt.setAlphaBufferSize(2); - window->setFormat(fmt); + delete m_container; + m_container = nullptr; } - if (!initVulkan()) - { - cerr << "ERROR: VulkanView: initVulkan failed; falling back to OpenGL" << endl; - requestGLFallback(); - return; - } + m_videoDevice.reset(); + } - m_initialized = true; + void VulkanView::showEvent(QShowEvent* event) + { + QWidget::showEvent(event); - if (m_doc) - { - m_doc->initializeSession(); - } + // The parent is only known once the container is shown, which is + // nested inside this show, so also check one event-loop turn later. + watchParentWindow(); + QTimer::singleShot(0, this, &VulkanView::watchParentWindow); } - bool VulkanView::supports10BitPresentation() + void VulkanView::watchParentWindow() { - QVulkanInstance qtVkInst; - if (!qtVkInst.create()) - { - cerr << "ERROR: VulkanView: supports10BitPresentation: QVulkanInstance create failed" << endl; - return false; - } - - VkInstance instance = qtVkInst.vkInstance(); - if (instance == VK_NULL_HANDLE) + // + // Watch the top-level widget's window: it is what Qt destroys, and it + // is known before the container re-parents the viewport into it. A + // top-level view (a presentation output) has no enclosing window. + // + QWidget* topLevel = window(); + if (topLevel == this) { - return false; + return; } - QWindow dummyWindow; - dummyWindow.setSurfaceType(QSurface::VulkanSurface); - dummyWindow.create(); - dummyWindow.setVulkanInstance(&qtVkInst); + QWindow* topLevelWindow = topLevel ? topLevel->windowHandle() : nullptr; - VkSurfaceKHR dummySurface = qtVkInst.surfaceForWindow(&dummyWindow); - if (!dummySurface) + if (topLevelWindow == m_watchedParentWindow) { - cerr << "ERROR: VulkanView: supports10BitPresentation: failed to create dummy surface" << endl; - return false; - } - - uint32_t deviceCount = 0; - vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); - if (deviceCount == 0) - { - cerr << "ERROR: VulkanView: supports10BitPresentation: vkEnumeratePhysicalDevices returned 0 devices" << endl; - return false; + return; } - std::vector devices(deviceCount); - vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); - if (ImageRenderer::debugGpu()) + if (m_watchedParentConnection) { - cout << "INFO: VulkanView: supports10BitPresentation: probing " << deviceCount << " physical device(s)" << endl; + disconnect(m_watchedParentConnection); } - bool any10bit = false; - for (uint32_t di = 0; di < devices.size(); ++di) - { - VkPhysicalDevice dev = devices[di]; - - uint32_t formatCount = 0; - if (vkGetPhysicalDeviceSurfaceFormatsKHR(dev, dummySurface, &formatCount, nullptr) != VK_SUCCESS || formatCount == 0) - { - continue; - } - std::vector formats(formatCount); - vkGetPhysicalDeviceSurfaceFormatsKHR(dev, dummySurface, &formatCount, formats.data()); - - bool has10bit = false; - for (const auto& fmt : formats) - { - if (isTenBitFormat(fmt.format)) - { - has10bit = true; - any10bit = true; - break; - } - } - - VkPhysicalDeviceProperties props = {}; - vkGetPhysicalDeviceProperties(dev, &props); - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: device[" << di << "] '" << props.deviceName - << "': 10-bit surface format=" << (has10bit ? "YES" : "NO") << endl; - } - } + m_watchedParentWindow = topLevelWindow; - // The surface returned by surfaceForWindow() is owned by the platform - // integration and is released when dummyWindow is destroyed on return; - // QVulkanInstance has no destroySurface() in this Qt version. - if (ImageRenderer::debugGpu()) + if (topLevelWindow) { - cout << "INFO: VulkanView: supports10BitPresentation: returning " << (any10bit ? "true" : "false") << endl; + m_watchedParentConnection = connect(topLevelWindow, &QObject::destroyed, this, &VulkanView::parentWindowDestroyed); } - return any10bit; } - bool VulkanView::initVulkan() + void VulkanView::parentWindowDestroyed() { - // The VkInstance is created and owned by Qt. - static QVulkanInstance* qtVkInst = nullptr; - if (!qtVkInst) - { - // Ask Qt for a 1.1 instance: the interop capability probe uses - // vkGetPhysicalDeviceImageFormatProperties2, which is core in 1.1. - // Without it negotiateInteropConfig() cannot establish whether a - // configuration is exportable and has to refuse interop outright. - // A 1.0-only loader is still tolerated -- retry unversioned and let - // the probe fall back to the KHR alias, or degrade if that is - // absent too. - qtVkInst = new QVulkanInstance(); - qtVkInst->setApiVersion(QVersionNumber(1, 1)); - if (!qtVkInst->create()) - { - cout << "WARNING: VulkanView: QVulkanInstance create failed at apiVersion 1.1; retrying with the loader default" << endl; - delete qtVkInst; - qtVkInst = new QVulkanInstance(); - if (!qtVkInst->create()) - { - cerr << "ERROR: VulkanView: QVulkanInstance create failed" << endl; - delete qtVkInst; - qtVkInst = nullptr; - return false; - } - } - } - - m_vkInstance = qtVkInst->vkInstance(); - - // Create Surface - QWindow* window = windowHandle(); - if (!window) - { - return false; - } - - window->setVulkanInstance(qtVkInst); - - m_vkSurface = qtVkInst->surfaceForWindow(window); - if (!m_vkSurface) - { - cerr << "ERROR: VulkanView: Failed to create Vulkan surface" << endl; - return false; - } - - // Pick Physical Device - uint32_t deviceCount = 0; - vkEnumeratePhysicalDevices(m_vkInstance, &deviceCount, nullptr); - if (deviceCount == 0) - { - return false; - } - std::vector devices(deviceCount); - vkEnumeratePhysicalDevices(m_vkInstance, &deviceCount, devices.data()); - - m_vkPhysicalDevice = VK_NULL_HANDLE; - bool foundQueue = false; - m_queueFamilyIndex = 0; - - for (VkPhysicalDevice dev : devices) - { - uint32_t queueFamilyCount = 0; - vkGetPhysicalDeviceQueueFamilyProperties(dev, &queueFamilyCount, nullptr); - std::vector queueFamilies(queueFamilyCount); - vkGetPhysicalDeviceQueueFamilyProperties(dev, &queueFamilyCount, queueFamilies.data()); - - for (uint32_t i = 0; i < queueFamilyCount; i++) - { - VkBool32 presentSupport = false; - vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, m_vkSurface, &presentSupport); - if ((queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) - { - m_vkPhysicalDevice = dev; - m_queueFamilyIndex = i; - foundQueue = true; - break; - } - } - if (foundQueue) - { - break; - } - } + // + // Emitted before the window deletes its children, so detaching here + // saves the viewport. It is hidden while parentless and re-attached + // once the top-level has its new window. + // + QWindow* destroyedWindow = m_watchedParentWindow; + m_watchedParentWindow = nullptr; - if (!foundQueue) + // Leave it alone while it still belongs to QWindowContainer's + // placeholder parent. + if (m_vulkanWindow && m_vulkanWindow->parent() == destroyedWindow) { - cerr << "ERROR: VulkanView: initVulkan: No physical device with graphics and present support found." << endl; - return false; + m_vulkanWindow->hide(); + m_vulkanWindow->setParent(nullptr); } + // + // QWindowContainer::parentWasMoved() dereferences the top-level's + // windowHandle() unchecked, and it is null until Qt recreates it, so + // the container must leave the tree too. + // + if (m_container) { - VkPhysicalDeviceProperties props = {}; - vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &props); - if (ImageRenderer::debugGpu()) + if (layout()) { - cout << "INFO: VulkanView: initVulkan: picked physical device '" << props.deviceName << "' (of " << deviceCount - << " available)" << endl; + layout()->removeWidget(m_container); } - } - - // Create Logical Device - float queuePriority = 1.0f; - VkDeviceQueueCreateInfo queueCreateInfo = {}; - queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queueCreateInfo.queueFamilyIndex = m_queueFamilyIndex; - queueCreateInfo.queueCount = 1; - queueCreateInfo.pQueuePriorities = &queuePriority; - - std::vector deviceExtensions = { - VK_KHR_SWAPCHAIN_EXTENSION_NAME, -#ifdef PLATFORM_WINDOWS - VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME, - VK_KHR_EXTERNAL_SEMAPHORE_WIN32_EXTENSION_NAME, -#else - VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, - VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, -#endif - }; - - VkDeviceCreateInfo createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - createInfo.pQueueCreateInfos = &queueCreateInfo; - createInfo.queueCreateInfoCount = 1; - createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); - createInfo.ppEnabledExtensionNames = deviceExtensions.data(); - - if (vkCreateDevice(m_vkPhysicalDevice, &createInfo, nullptr, &m_vkDevice) != VK_SUCCESS) - { - return false; - } - - vkGetDeviceQueue(m_vkDevice, m_queueFamilyIndex, 0, &m_vkQueue); - - // Negotiate the GL<->Vulkan interop configuration once, here. It is a - // property of the device, not of a shared-image slot or of the current - // window size, so it must not be recomputed per slot or on resize. - negotiateInteropConfig(); - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: initVulkan: interop negotiation ran (once per device); result=" - << (m_interopConfig.supported ? "supported" : "unsupported") << endl; - } - - auto failInit = [this]() - { - cleanupVulkan(); - return false; - }; - // Command pool - VkCommandPoolCreateInfo poolInfo = {}; - poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - poolInfo.queueFamilyIndex = m_queueFamilyIndex; - if (vkCreateCommandPool(m_vkDevice, &poolInfo, nullptr, &m_vkCommandPool) != VK_SUCCESS) - { - return failInit(); - } - - // Sync objects. The per-swapchain-image renderFinished semaphores live - // in createSwapchain (sized to the image count); here we create only the - // per-in-flight-slot acquire semaphores and frame fences. - VkSemaphoreCreateInfo semaphoreInfo = {}; - semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - - // Per-in-flight-slot acquire semaphore + frame fence. Fences are created - // signaled so the first wait on a slot passes without a prior submit. - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - for (FrameSync& sync : m_frameSync) - { - if (vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &sync.imageAvailable) != VK_SUCCESS) - { - return failInit(); - } - if (vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &sync.fence) != VK_SUCCESS) - { - return failInit(); - } + m_container->hide(); + m_container->setParent(nullptr); } - return true; - } - - // Queue fallbackVulkanToGLView on the next event-loop tick (at most once). - void VulkanView::requestGLFallback() - { - if (m_glFallbackRequested || !m_doc || m_stopProcessingEvents || m_doc->isClosing()) + if (m_reattachPending) { return; } - m_glFallbackRequested = true; - // The OpenGL rung forgoes 10-bit, so it must be visible in the log - // rather than inferred from the absence of a Vulkan record. Callers - // that know why set m_presentPathReason before calling. - reportPresentPath(PresentPath::OpenGL, - m_presentPathReason.empty() ? std::string("Vulkan presentation could not be established") : m_presentPathReason); - - QTimer::singleShot(0, m_doc, [doc = m_doc]() { doc->fallbackVulkanToGLView(); }); + m_reattachPending = true; + QTimer::singleShot(0, this, &VulkanView::reattachVulkanWindow); } - // Skip swapchain work during close or zero-size resize. - bool VulkanView::presentationAllowed() const + void VulkanView::reattachVulkanWindow() { - if (m_stopProcessingEvents) - { - return false; - } - if (width() <= 0 || height() <= 0) - { - return false; - } - if (m_doc && m_doc->isClosing()) - { - return false; - } - return true; - } + m_reattachPending = false; - // Reset acquire semaphore and rebuild swapchain/shared image at the new size. - void VulkanView::handleSwapchainOutOfDate() - { - if (!m_vkDevice || !presentationAllowed()) + if (!m_vulkanWindow) { return; } - VkSemaphoreCreateInfo semaphoreInfo = {}; - semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - for (FrameSync& sync : m_frameSync) - { - if (sync.imageAvailable) - { - vkDestroySemaphore(m_vkDevice, sync.imageAvailable, nullptr); - sync.imageAvailable = VK_NULL_HANDLE; - } - if (vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &sync.imageAvailable) != VK_SUCCESS) - { - sync.imageAvailable = VK_NULL_HANDLE; - requestGLFallback(); - return; - } - } - - // Recreate only the swapchain, not the shared image. The shared image is - // a content-sized TRANSFER_SRC image, independent of the window-sized - // swapchain; createSwapchain() reuses the old swapchain (oldSwapchain) so - // this is a warm recreate. The next render()'s getSharedImageInfo() will - // rebuild the shared image only if the content size actually changed. - if (!createSwapchain()) - { - requestGLFallback(); - } - } - - void VulkanView::cleanupVulkan() - { - if (m_vkDevice) - { - vkDeviceWaitIdle(m_vkDevice); - - for (FrameSync& sync : m_frameSync) - { - if (sync.imageAvailable) - { - vkDestroySemaphore(m_vkDevice, sync.imageAvailable, nullptr); - sync.imageAvailable = VK_NULL_HANDLE; - } - if (sync.fence) - { - vkDestroyFence(m_vkDevice, sync.fence, nullptr); - sync.fence = VK_NULL_HANDLE; - } - } - // m_vkRenderFinished are per-swapchain-image; freed in cleanupSwapchain. - - if (m_vkCommandPool) - { - vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr); - m_vkCommandPool = VK_NULL_HANDLE; - } - - vkDestroyDevice(m_vkDevice, nullptr); - m_vkDevice = VK_NULL_HANDLE; - } - m_vkQueue = VK_NULL_HANDLE; - // The VkInstance and VkSurfaceKHR are owned by Qt (QVulkanInstance), so they are not destroyed here. - } - - bool VulkanView::createSwapchain() - { - if (!m_vkDevice || !m_vkSurface) - { - return false; - } - - if (!presentationAllowed()) - { - return false; - } - - VkSurfaceCapabilitiesKHR capabilities; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_vkPhysicalDevice, m_vkSurface, &capabilities); - - // Negotiate 10-bit format - uint32_t formatCount; - vkGetPhysicalDeviceSurfaceFormatsKHR(m_vkPhysicalDevice, m_vkSurface, &formatCount, nullptr); - std::vector formats(formatCount); - vkGetPhysicalDeviceSurfaceFormatsKHR(m_vkPhysicalDevice, m_vkSurface, &formatCount, formats.data()); + QWidget* topLevel = window(); + QWindow* topLevelWindow = topLevel ? topLevel->windowHandle() : nullptr; - if (ImageRenderer::debugGpu()) + if (!topLevelWindow) { - cout << "INFO: VulkanView: createSwapchain: surface offers " << formatCount << " format/colorSpace pair(s):" << endl; - for (uint32_t i = 0; i < formats.size(); ++i) - { - cout << "INFO: VulkanView: [" << i << "] format=" << formats[i].format << " (" << formatName(formats[i].format) - << ") colorSpace=" << formats[i].colorSpace << " (" << colorSpaceName(formats[i].colorSpace) << ")" << endl; - } + // Qt recreates the top-level's window lazily; keep waiting. + m_reattachPending = true; + QTimer::singleShot(0, this, &VulkanView::reattachVulkanWindow); + return; } - // Select the format and its color space TOGETHER, as one pairing the - // surface actually offers. Picking a format first and inheriting - // whatever color space accompanies it depends on driver list order: a - // surface that lists A2B10G10R10 under HDR10_ST2084 before listing it - // under SRGB_NONLINEAR would yield an HDR swapchain fed the SDR-encoded - // pixels RV renders. - // - // A2B10G10R10 (== GL_RGB10_A2) is preferred because it is the layout - // the interop shared texture and the CPU fallback packing produce - // natively, making the transfer a plain copy. A2R10G10B10 (common on - // Linux/RADV) is accepted too: the opposite R/B order is resolved by - // component-wise packing on the CPU path and by vkCmdBlitImage on the - // interop path, so red and blue are not swapped. - VkSurfaceFormatKHR surfaceFormat = formats.empty() ? VkSurfaceFormatKHR{} : formats[0]; - bool found10bit = false; - bool sawTenBitNonSdr = false; - - constexpr std::array preferredOrder = {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}; - for (VkFormat want : preferredOrder) + // Re-parenting the container makes it re-adopt the viewport window. + if (m_container) { - for (const auto& fmt : formats) - { - if (fmt.format != want) - { - continue; - } - - if (fmt.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) - { - surfaceFormat = fmt; - found10bit = true; - break; - } - - // A 10-bit format, but only in a color space RV does not encode - // for. Remember it so the fallback reason can say so. - sawTenBitNonSdr = true; - } - if (found10bit) - { - break; - } - } + m_container->setParent(this); - if (!found10bit) - { - if (sawTenBitNonSdr) - { - cout << "WARNING: VulkanView: the surface offers 10-bit formats only in non-SDR color spaces " - "(RV renders SDR-encoded pixels, so presenting into one would mis-encode color); " - "requesting OpenGL fallback" - << endl; - m_presentPathReason = "surface offers 10-bit only in a non-SDR color space"; - } - else + if (layout()) { - cout << "WARNING: VulkanView: Real surface lacks a 10-bit format (A2B10G10R10/A2R10G10B10); requesting OpenGL fallback" - << endl; - m_presentPathReason = "surface offers no 10-bit format"; + layout()->addWidget(m_container); } - requestGLFallback(); - return false; - } - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: createSwapchain: chose " << formatName(surfaceFormat.format) << " / " - << colorSpaceName(surfaceFormat.colorSpace) << " (10-bit SDR OK)" << endl; + m_container->show(); + setFocusProxy(m_container); } - m_vkSwapchainFormat = surfaceFormat.format; - m_vkSwapchainColorSpace = surfaceFormat.colorSpace; - - m_vkSwapchainExtent = capabilities.currentExtent; - if (m_vkSwapchainExtent.width == std::numeric_limits::max()) - { - m_vkSwapchainExtent = {static_cast(width()), static_cast(height())}; - } - if (m_vkSwapchainExtent.width == 0 || m_vkSwapchainExtent.height == 0) + if (m_vulkanWindow->parent() != topLevelWindow) { - requestGLFallback(); - return false; + m_vulkanWindow->setParent(topLevelWindow); } - uint32_t imageCount = capabilities.minImageCount + 1; - if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) - { - imageCount = capabilities.maxImageCount; - } + m_vulkanWindow->show(); - VkSwapchainCreateInfoKHR createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; - createInfo.surface = m_vkSurface; - createInfo.minImageCount = imageCount; - createInfo.imageFormat = surfaceFormat.format; - createInfo.imageColorSpace = surfaceFormat.colorSpace; - createInfo.imageExtent = m_vkSwapchainExtent; - createInfo.imageArrayLayers = 1; - createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; - createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - createInfo.preTransform = capabilities.currentTransform; - createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR; // VSync - createInfo.clipped = VK_TRUE; - // Warm recreate: hand the retiring swapchain to the driver so it can reuse - // its backing resources (much cheaper than a cold create on every resize). - createInfo.oldSwapchain = m_vkSwapchain; + watchParentWindow(); - // Create into a local handle so a failed create leaves the existing - // swapchain and command buffers intact (the fallback paths stay valid). - VkSwapchainKHR newSwapchain = VK_NULL_HANDLE; - if (vkCreateSwapchainKHR(m_vkDevice, &createInfo, nullptr, &newSwapchain) != VK_SUCCESS) + if (m_container) { - requestGLFallback(); - return false; - } + m_container->updateGeometry(); - // New swapchain is live. Retire the old one only now: wait for its last - // submitted frame to finish, free its command buffers, then destroy it. - if (m_vkSwapchain != VK_NULL_HANDLE) - { - vkDeviceWaitIdle(m_vkDevice); - if (!m_vkCommandBuffers.empty()) + if (layout()) { - vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, static_cast(m_vkCommandBuffers.size()), - m_vkCommandBuffers.data()); - m_vkCommandBuffers.clear(); + layout()->activate(); } - vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr); - } - m_vkSwapchain = newSwapchain; - - vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, nullptr); - m_vkSwapchainImages.resize(imageCount); - vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, m_vkSwapchainImages.data()); - - m_vkCommandBuffers.resize(imageCount); - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = m_vkCommandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = static_cast(m_vkCommandBuffers.size()); - if (vkAllocateCommandBuffers(m_vkDevice, &allocInfo, m_vkCommandBuffers.data()) != VK_SUCCESS) - { - cleanupSwapchain(); - requestGLFallback(); - return false; } - // Per-swapchain-image present-wait semaphores + in-flight fence map. The - // device is idle here (the retire path above waited on it), so any old - // renderFinished semaphores from a previous swapchain are safe to destroy. - for (VkSemaphore sem : m_vkRenderFinished) - { - if (sem) - { - vkDestroySemaphore(m_vkDevice, sem, nullptr); - } - } - m_vkRenderFinished.assign(imageCount, VK_NULL_HANDLE); - VkSemaphoreCreateInfo rfInfo = {}; - rfInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - for (uint32_t i = 0; i < imageCount; ++i) + // The new platform window invalidates the VkSurfaceKHR; the redraw + // makes VulkanWindow rebuild it. + if (m_doc && m_doc->session()) { - if (vkCreateSemaphore(m_vkDevice, &rfInfo, nullptr, &m_vkRenderFinished[i]) != VK_SUCCESS) - { - cleanupSwapchain(); - requestGLFallback(); - return false; - } + m_doc->session()->askForRedraw(); } - // Fresh swapchain images: none are in flight yet. - m_imagesInFlight.assign(imageCount, VK_NULL_HANDLE); - - return true; } - void VulkanView::cleanupSwapchain() + void VulkanView::stopProcessingEvents() { - if (m_vkDevice) + if (m_vulkanWindow) { - vkDeviceWaitIdle(m_vkDevice); - - for (VkSemaphore sem : m_vkRenderFinished) - { - if (sem) - { - vkDestroySemaphore(m_vkDevice, sem, nullptr); - } - } - m_vkRenderFinished.clear(); - m_imagesInFlight.clear(); - - for (StagingBuffer& staging : m_staging) - { - if (staging.buffer) - { - vkDestroyBuffer(m_vkDevice, staging.buffer, nullptr); - staging.buffer = VK_NULL_HANDLE; - } - if (staging.memory) - { - vkFreeMemory(m_vkDevice, staging.memory, nullptr); - staging.memory = VK_NULL_HANDLE; - } - staging.size = 0; - } - - if (!m_vkCommandBuffers.empty()) - { - vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, static_cast(m_vkCommandBuffers.size()), - m_vkCommandBuffers.data()); - m_vkCommandBuffers.clear(); - } - - if (m_vkSwapchain) - { - vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr); - m_vkSwapchain = VK_NULL_HANDLE; - } + m_vulkanWindow->stopProcessingEvents(); } } - namespace - { - bool isNvidiaPhysicalDevice(VkPhysicalDevice dev) - { - VkPhysicalDeviceProperties props = {}; - vkGetPhysicalDeviceProperties(dev, &props); - return props.vendorID == 0x10DE; - } - - bool nvidiaInteropWorkaroundDisabled() - { - static const bool disabled = getenv("RV_VULKAN_DISABLE_NVIDIA_INTEROP_WORKAROUND") != nullptr; - return disabled; - } - - // NVIDIA Linux 550+ drivers return blank pixels to OpenGL for LINEAR shared - // images >= ~2 MiB (forum thread #349436). Allocating the shared image with - // VK_IMAGE_TILING_OPTIMAL avoids that broken linear path and restores - // correct zero-copy interop, so OPTIMAL is the default on NVIDIA. Both the - // GL and Vulkan sides here are the same NVIDIA driver/GPU, so the - // vendor-private optimal layout matches on import without needing explicit - // DRM-format-modifier negotiation. AMD/Intel keep the existing LINEAR path. - // Set RV_VULKAN_DISABLE_NVIDIA_INTEROP_WORKAROUND to revert NVIDIA to LINEAR - // (reproduces the blank-image bug, for debugging). - bool useOptimalTilingForInterop(VkPhysicalDevice dev) - { -#if defined(PLATFORM_LINUX) - return !nvidiaInteropWorkaroundDisabled() && isNvidiaPhysicalDevice(dev); -#else - (void)dev; - return false; -#endif - } + bool VulkanView::firstPaintCompleted() const { return m_vulkanWindow && m_vulkanWindow->firstPaintCompleted(); } - // Resolve vkGetPhysicalDeviceImageFormatProperties2, preferring the - // core 1.1 entry point and falling back to the KHR alias. Qt owns the - // VkInstance, so which one exists depends on the apiVersion Qt created - // it with; initVulkan asks Qt for 1.1 but must tolerate a 1.0 loader. - PFN_vkGetPhysicalDeviceImageFormatProperties2 getImageFormatProperties2(VkInstance instance) - { - static PFN_vkGetPhysicalDeviceImageFormatProperties2 fn = nullptr; - static bool resolved = false; - if (!resolved) - { - resolved = true; - fn = reinterpret_cast( - vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceImageFormatProperties2")); - if (!fn) - { - fn = reinterpret_cast( - vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceImageFormatProperties2KHR")); - } - } - return fn; - } - } // namespace - - // - // Probe the driver for an exportable shared-image configuration. - // - // The configuration is chosen from what the driver reports, not from GPU - // vendor identity or host platform. Candidates are ordered OPTIMAL before - // LINEAR: OPTIMAL is the layout drivers are built around, and LINEAR is - // the compatibility rung that additionally carries the rowPitch % 4 - // constraint that can fail allocation outright. - // - // Usage always includes COLOR_ATTACHMENT because the GL side attaches the - // imported texture to GL_COLOR_ATTACHMENT0 and renders into it. Declaring - // only TRANSFER_SRC lets the driver pick an internal compressed layout the - // GL import does not decode, which corrupts the image rather than failing. - // If no candidate with the honest usage is exportable, interop is refused - // rather than narrowed to a declaration the code then violates. - // - void VulkanView::negotiateInteropConfig() - { - if (m_interopNegotiated) - { - return; - } - m_interopNegotiated = true; - - InteropConfig cfg; - cfg.format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; // == GL_RGB10_A2 - cfg.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - -#ifdef PLATFORM_WINDOWS - const VkExternalMemoryHandleTypeFlagBits handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; -#else - const VkExternalMemoryHandleTypeFlagBits handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; -#endif - - PFN_vkGetPhysicalDeviceImageFormatProperties2 probe = getImageFormatProperties2(m_vkInstance); - if (!probe) - { - cfg.supported = false; - cfg.rejectReason = "vkGetPhysicalDeviceImageFormatProperties2 is unavailable " - "(Vulkan instance predates 1.1 and lacks VK_KHR_get_physical_device_properties2), " - "so exportability cannot be established"; - m_interopConfig = cfg; - return; - } - - constexpr std::array candidates = {VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_TILING_LINEAR}; - - bool found = false; - for (VkImageTiling tiling : candidates) - { - VkPhysicalDeviceExternalImageFormatInfo extInfo = {}; - extInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO; - extInfo.handleType = handleType; - - VkPhysicalDeviceImageFormatInfo2 fmtInfo = {}; - fmtInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; - fmtInfo.pNext = &extInfo; - fmtInfo.format = cfg.format; - fmtInfo.type = VK_IMAGE_TYPE_2D; - fmtInfo.tiling = tiling; - fmtInfo.usage = cfg.usage; - fmtInfo.flags = 0; - - VkExternalImageFormatProperties extProps = {}; - extProps.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES; - - VkImageFormatProperties2 props = {}; - props.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; - props.pNext = &extProps; - - const VkResult result = probe(m_vkPhysicalDevice, &fmtInfo, &props); - - ostringstream entry; - entry << tilingName(tiling) << ": "; - - if (result != VK_SUCCESS) - { - entry << "not supported for this format/usage (VkResult " << result << ")"; - cfg.candidateLog.push_back(entry.str()); - continue; - } - - const VkExternalMemoryFeatureFlags features = extProps.externalMemoryProperties.externalMemoryFeatures; - const bool exportable = (extProps.externalMemoryProperties.compatibleHandleTypes & handleType) != 0 - && (features & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0 - && (features & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0; - - if (!exportable) - { - entry << "supported but not exportable+importable for this handle type" - << " (features=0x" << std::hex << features << std::dec << ")"; - cfg.candidateLog.push_back(entry.str()); - continue; - } - - // DEDICATED_ONLY is the only dedicated-allocation signal available - // from the external-memory probe: it is a hard requirement of the - // handle type. The softer "prefers dedicated" signal is a property - // of a concrete image, not of the format, and is read per-image - // from VkMemoryDedicatedRequirements at allocation time -- see - // getSharedImageInfo(). This value is therefore the floor, and the - // per-slot SharedImageInfo::dedicatedAllocation is the final - // decision the GL side must mirror. - const bool dedicatedOnly = (features & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; - - cfg.supported = true; - cfg.tiling = tiling; - cfg.externalFeatures = features; - cfg.dedicatedAllocation = dedicatedOnly; - cfg.probedTiling = tiling; - cfg.probedDedicated = cfg.dedicatedAllocation; - - entry << "exportable (features=0x" << std::hex << features << std::dec << ") -- selected"; - cfg.candidateLog.push_back(entry.str()); - found = true; - break; - } - - if (!found) - { - cfg.supported = false; - cfg.rejectReason = "no candidate tiling is exportable at A2B10G10R10 with " - "COLOR_ATTACHMENT|TRANSFER_SRC usage"; - m_interopConfig = cfg; - return; - } - - // Apply the diagnostic overrides last, so the record can report both - // the negotiated value and the forced one. An override is honored only - // when the driver reported that configuration as usable. - const std::optional forcedTiling = forcedTilingRequested(); - if (forcedTiling && *forcedTiling != cfg.tiling) - { - // Re-probe the forced tiling rather than trusting the request: - // presenting through an unverified configuration is exactly what - // the fallback ladder exists to prevent. - VkPhysicalDeviceExternalImageFormatInfo extInfo = {}; - extInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO; - extInfo.handleType = handleType; - - VkPhysicalDeviceImageFormatInfo2 fmtInfo = {}; - fmtInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; - fmtInfo.pNext = &extInfo; - fmtInfo.format = cfg.format; - fmtInfo.type = VK_IMAGE_TYPE_2D; - fmtInfo.tiling = *forcedTiling; - fmtInfo.usage = cfg.usage; - - VkExternalImageFormatProperties extProps = {}; - extProps.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES; - VkImageFormatProperties2 props = {}; - props.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; - props.pNext = &extProps; - - const VkResult result = probe(m_vkPhysicalDevice, &fmtInfo, &props); - const VkExternalMemoryFeatureFlags features = extProps.externalMemoryProperties.externalMemoryFeatures; - const bool ok = result == VK_SUCCESS && (extProps.externalMemoryProperties.compatibleHandleTypes & handleType) != 0 - && (features & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0 - && (features & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0; - - if (ok) - { - cfg.tilingOverridden = true; - cfg.tiling = *forcedTiling; - cfg.externalFeatures = features; - cfg.dedicatedAllocation = (features & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; - } - else - { - cout << "WARNING: VulkanView: RV_VULKAN_FORCE_TILING=" << tilingName(*forcedTiling) - << " refused -- the driver does not report it as exportable; using the negotiated " << tilingName(cfg.tiling) << endl; - } - } - - if (envFlagSet("RV_VULKAN_FORCE_NO_DEDICATED") && cfg.dedicatedAllocation) - { - if ((cfg.externalFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0) - { - cout << "WARNING: VulkanView: RV_VULKAN_FORCE_NO_DEDICATED refused -- the driver reports " - << "DEDICATED_ONLY for this configuration, which is a requirement rather than a preference" << endl; - } - else - { - cfg.dedicatedOverridden = true; - cfg.dedicatedAllocation = false; - } - } - - m_interopConfig = cfg; - } - - void VulkanView::reportPresentPath(PresentPath path, const std::string& reason) - { - m_presentPath = path; - m_presentPathReason = reason; - emitPresentationRecord(); - } - - void VulkanView::reportGLImportState(VkImageTiling tiling, bool dedicated) - { - m_glImportTiling = tiling; - m_glImportDedicated = dedicated; - m_glImportReported = true; - } - - // - // One record per session, emitted unconditionally. Windows/NVIDIA is - // verified by testers against a produced build rather than by the - // developer, so this has to be sufficient on its own to establish which - // path ran and what was negotiated. Per-frame and per-candidate detail - // stays behind ImageRenderer::debugGpu(). - // - void VulkanView::emitPresentationRecord() - { - if (m_recordEmitted) - { - return; - } - m_recordEmitted = true; - - VkPhysicalDeviceProperties props = {}; - if (m_vkPhysicalDevice != VK_NULL_HANDLE) - { - vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &props); - } - - std::string_view pathName = "undetermined"; - switch (m_presentPath) - { - case PresentPath::ZeroCopy: - pathName = "GPU zero-copy interop (10-bit)"; - break; - case PresentPath::CpuReadback: - pathName = "CPU readback (10-bit, slower)"; - break; - case PresentPath::OpenGL: - pathName = "OpenGL (Vulkan abandoned; not 10-bit)"; - break; - case PresentPath::Undetermined: - break; - } - - ostringstream record; - record << "INFO: RV Vulkan presentation report\n"; - record << "INFO: GPU : " << (m_vkPhysicalDevice != VK_NULL_HANDLE ? props.deviceName : "(none)") << " vendorID=0x" - << std::hex << props.vendorID << std::dec << " driverVersion=" << props.driverVersion - << " apiVersion=" << VK_VERSION_MAJOR(props.apiVersion) << "." << VK_VERSION_MINOR(props.apiVersion) << "." - << VK_VERSION_PATCH(props.apiVersion) << "\n"; - record << "INFO: Present path : " << pathName << "\n"; - if (!m_presentPathReason.empty()) - { - record << "INFO: Reason : " << m_presentPathReason << "\n"; - } - record << "INFO: Swapchain : " << formatName(m_vkSwapchainFormat) << " / " << colorSpaceName(m_vkSwapchainColorSpace) - << "\n"; - - const InteropConfig& config = m_interopConfig; - if (config.supported) - { - record << "INFO: Shared image : " << formatName(config.format) << " tiling=" << tilingName(config.tiling) - << " usage=COLOR_ATTACHMENT|TRANSFER_SRC\n"; - record << "INFO: Dedicated alloc: " << (config.dedicatedAllocation ? "yes" : "no") << " (driver externalMemoryFeatures=0x" - << std::hex << config.externalFeatures << std::dec - << (config.externalFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT ? " DEDICATED_ONLY" : "") << ")\n"; - if (config.tilingOverridden) - { - record << "INFO: Tiling override: RV_VULKAN_FORCE_TILING forced " << tilingName(config.tiling) << "; negotiation chose " - << tilingName(config.probedTiling) << "\n"; - } - if (config.dedicatedOverridden) - { - record << "INFO: Dedicated ovr : RV_VULKAN_FORCE_NO_DEDICATED suppressed dedicated allocation; negotiation chose " - << (config.probedDedicated ? "yes" : "no") << "\n"; - } - if (m_glImportReported) - { - const bool agree = m_glImportTiling == config.tiling && m_glImportDedicated == config.dedicatedAllocation; - record << "INFO: GL import : tiling=" << tilingName(m_glImportTiling) - << " dedicated=" << (m_glImportDedicated ? "yes" : "no") << " -- " - << (agree ? "matches the Vulkan export" : "DISAGREES WITH THE VULKAN EXPORT (expect a corrupted image)") << "\n"; - } - } - else - { - record << "INFO: Shared image : not used -- " - << (config.rejectReason.empty() ? "interop not negotiated" : config.rejectReason) << "\n"; - } - - for (const std::string& entry : config.candidateLog) - { - record << "INFO: Probe candidate: " << entry << "\n"; - } - - // Kept for comparison until the Linux/NVIDIA probe result is confirmed - // to agree; the vendor heuristic is removed once it does. - record << "INFO: Legacy vendor heuristic would have chosen: " - << (m_vkPhysicalDevice != VK_NULL_HANDLE && useOptimalTilingForInterop(m_vkPhysicalDevice) ? "OPTIMAL" : "LINEAR") << "\n"; - - if (envFlagSet("RV_VULKAN_FORCE_CPU_PRESENT")) - { - record << "INFO: Override : RV_VULKAN_FORCE_CPU_PRESENT is set\n"; - } - - cout << record.str() << flush; - } - - void VulkanView::cleanupSharedImage(uint32_t slot) - { - SharedImageInfo& info = m_shared[slot].info; - - if (m_vkDevice) - { - vkDeviceWaitIdle(m_vkDevice); - - if (m_shared[slot].image) - { - vkDestroyImage(m_vkDevice, m_shared[slot].image, nullptr); - m_shared[slot].image = VK_NULL_HANDLE; - } - if (m_shared[slot].memory) - { - vkFreeMemory(m_vkDevice, m_shared[slot].memory, nullptr); - m_shared[slot].memory = VK_NULL_HANDLE; - } - if (m_shared[slot].glReady) - { - vkDestroySemaphore(m_vkDevice, m_shared[slot].glReady, nullptr); - m_shared[slot].glReady = VK_NULL_HANDLE; - } - if (m_shared[slot].vkReady) - { - vkDestroySemaphore(m_vkDevice, m_shared[slot].vkReady, nullptr); - m_shared[slot].vkReady = VK_NULL_HANDLE; - } - } - -#ifdef PLATFORM_WINDOWS - if (info.memoryHandle) - { - ::CloseHandle(static_cast(info.memoryHandle)); - info.memoryHandle = nullptr; - } - if (info.glReadySemaphoreHandle) - { - ::CloseHandle(static_cast(info.glReadySemaphoreHandle)); - info.glReadySemaphoreHandle = nullptr; - } - if (info.vkReadySemaphoreHandle) - { - ::CloseHandle(static_cast(info.vkReadySemaphoreHandle)); - info.vkReadySemaphoreHandle = nullptr; - } -#else - if (info.memoryFd != -1) - { - ::close(info.memoryFd); - info.memoryFd = -1; - } - if (info.glReadySemaphoreFd != -1) - { - ::close(info.glReadySemaphoreFd); - info.glReadySemaphoreFd = -1; - } - if (info.vkReadySemaphoreFd != -1) - { - ::close(info.vkReadySemaphoreFd); - info.vkReadySemaphoreFd = -1; - } -#endif - info.width = 0; - info.height = 0; - info.size = 0; - info.capacityHeight = 0; - info.tiling = VK_IMAGE_TILING_LINEAR; - info.dedicatedAllocation = false; - m_shared[slot].capacityW = 0; - m_shared[slot].capacityH = 0; - } - - void VulkanView::drainSharedSemaphores(uint32_t slot) - { - // No shared image for this slot yet -> the GL side never signaled/waited - // its pair, so there is nothing to rebalance. - if (!m_vkDevice || !m_shared[slot].glReady || !m_shared[slot].vkReady) - { - return; - } - - // Consume the pending glReady signal from the GL side and re-signal - // vkReady so the next use of this slot starts balanced (exactly what a - // normal present's submit would have done for the pair). - VkSubmitInfo drain = {}; - drain.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - const std::array waitSemaphores = {m_shared[slot].glReady}; - const std::array waitStages = {VK_PIPELINE_STAGE_TRANSFER_BIT}; - drain.waitSemaphoreCount = static_cast(waitSemaphores.size()); - drain.pWaitSemaphores = waitSemaphores.data(); - drain.pWaitDstStageMask = waitStages.data(); - drain.commandBufferCount = 0; - const std::array signalSemaphores = {m_shared[slot].vkReady}; - drain.signalSemaphoreCount = static_cast(signalSemaphores.size()); - drain.pSignalSemaphores = signalSemaphores.data(); - - const VkResult result = vkQueueSubmit(m_vkQueue, 1, &drain, VK_NULL_HANDLE); - if (result == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - } - - const VulkanView::SharedImageInfo* VulkanView::getSharedImageInfo(int w, int h) - { - if (!m_vkDevice) - return nullptr; - - // Build/return the shared image for the current in-flight ring slot. - const uint32_t slot = m_currentFrame; - SharedImageInfo& info = m_shared[slot].info; - - // The swapchain always tracks the window size, so recreate it on any size - // change. This is independent of the grow-only shared image below: a drag - // still recreates the (warm) swapchain each step, but no longer rebuilds - // or re-exports the shared image. - if (!m_vkSwapchain || m_vkSwapchainExtent.width != static_cast(w) - || m_vkSwapchainExtent.height != static_cast(h)) - { - // Warm recreate via oldSwapchain (createSwapchain retires the old one). - if (!createSwapchain()) - { - return nullptr; - } - } - - // Grow-only: if the request fits the slot's current allocated capacity, - // reuse the existing image/export and just update the used sub-region - // (presentSharedImage copies/blits info.width x info.height from it). - if (m_shared[slot].image && w <= m_shared[slot].capacityW && h <= m_shared[slot].capacityH) - { - info.width = w; - info.height = h; - return &info; - } - - // Grow (or first allocation): rebuild at a capacity that is the - // componentwise max of the request, the screen size, and the current - // capacity, so it grows monotonically and the common drag-to-fullscreen - // case allocates at most once. - int screenW = 0; - int screenH = 0; - if (QScreen* scr = QGuiApplication::primaryScreen()) - { - const qreal dpr = scr->devicePixelRatio(); - screenW = static_cast(scr->geometry().width() * dpr); - screenH = static_cast(scr->geometry().height() * dpr); - } - const int capW = std::max({w, screenW, m_shared[slot].capacityW}); - const int capH = std::max({h, screenH, m_shared[slot].capacityH}); - - cleanupSharedImage(slot); - - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: getSharedImageInfo: (re)allocating shared image slot " << slot << " capacity " << capW << "x" << capH - << " for request " << w << "x" << h << endl; - } - - // The interop configuration was negotiated once at device creation from - // what the driver reports exportable. If nothing was exportable, refuse - // the zero-copy path here so syncBuffers() takes the CPU readback rung - // rather than presenting through a configuration whose correctness was - // never established. - if (!m_interopConfig.supported) - { - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: getSharedImageInfo: interop unavailable (" << m_interopConfig.rejectReason - << "); using the CPU readback path" << endl; - } - return nullptr; - } - - const bool optimalTiling = m_interopConfig.tiling == VK_IMAGE_TILING_OPTIMAL; - - // 1. Create Shared Image - VkExternalMemoryImageCreateInfo extMemInfo = {}; - extMemInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; -#ifdef PLATFORM_WINDOWS - extMemInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; -#else - extMemInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; -#endif - - VkImageCreateInfo imageInfo = {}; - imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.pNext = &extMemInfo; - imageInfo.imageType = VK_IMAGE_TYPE_2D; - // The shared image is imported into GL as GL_RGB10_A2, whose bit layout - // is A2B10G10R10, so the Vulkan side must use the matching format - // regardless of the swapchain format. When the swapchain is A2R10G10B10 - // the difference is reconciled by a component-wise blit in - // presentSharedImage() (not a raw copy). - imageInfo.format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; // matches GL_RGB10_A2 - // Allocate at capacity; presentSharedImage transfers only the used - // info.width x info.height sub-region (anchored at origin 0,0). - imageInfo.extent = {static_cast(capW), static_cast(capH), 1}; - imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; - imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; - imageInfo.tiling = m_interopConfig.tiling; - // Usage must cover every use on BOTH sides: Vulkan reads the image as a - // transfer source, and GL attaches it to GL_COLOR_ATTACHMENT0 and - // renders into it. Declaring only TRANSFER_SRC lets the driver pick an - // internal compressed layout that the GL import does not decode, which - // corrupts the image rather than raising an error. - imageInfo.usage = m_interopConfig.usage; - imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - - if (vkCreateImage(m_vkDevice, &imageInfo, nullptr, &m_shared[slot].image) != VK_SUCCESS) - { - cerr << "ERROR: VulkanView: Failed to create shared image" << endl; - return nullptr; - } - - // When the swapchain format differs from the shared image format - // (A2B10G10R10), presentSharedImage() reconciles them with a blit rather - // than a raw copy. That requires the shared image to be a valid blit - // source and the swapchain image a valid blit destination. If the driver - // does not support that, refuse the GPU-interop path so syncBuffers() - // uses the (channel-correct) CPU fallback instead. - if (m_vkSwapchainFormat != VK_FORMAT_A2B10G10R10_UNORM_PACK32) - { - VkFormatProperties srcProps = {}; - VkFormatProperties dstProps = {}; - vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, VK_FORMAT_A2B10G10R10_UNORM_PACK32, &srcProps); - vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, m_vkSwapchainFormat, &dstProps); - // The shared image's blit-source support depends on its actual - // (negotiated) tiling. - const VkFormatFeatureFlags srcFeatures = optimalTiling ? srcProps.optimalTilingFeatures : srcProps.linearTilingFeatures; - const bool blitOk = - (srcFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) && (dstProps.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT); - if (!blitOk) - { - if (ImageRenderer::debugGpu()) - { - cout << "INFO: VulkanView: GPU interop unavailable for " << formatName(m_vkSwapchainFormat) - << " swapchain (blit unsupported); using CPU fallback." << endl; - } - cleanupSharedImage(slot); - return nullptr; - } - } - - // rowPitch is only meaningful (and vkGetImageSubresourceLayout only valid) - // for LINEAR tiling. For OPTIMAL tiling the GL import uses the logical - // capacity width and lets the driver resolve the layout. - if (optimalTiling) - { - info.strideWidth = capW; - } - else - { - // Handle padded linear row pitch by matching the GL texture stride to Vulkan's rowPitch. - VkImageSubresource subresource = {}; - subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - subresource.mipLevel = 0; - subresource.arrayLayer = 0; - VkSubresourceLayout layout; - vkGetImageSubresourceLayout(m_vkDevice, m_shared[slot].image, &subresource, &layout); - - if (layout.rowPitch % 4 != 0) - { - // Cannot represent this stride as an integer pixel-width texture; fall back to CPU bridge. - cleanupSharedImage(slot); - return nullptr; - } - info.strideWidth = static_cast(layout.rowPitch / 4); - } - info.capacityHeight = capH; // GL imports the texture at capacity dimensions - info.tiling = m_interopConfig.tiling; - - // Resolve the final dedicated-allocation decision for THIS image. The - // probe supplied the floor (DEDICATED_ONLY, a requirement of the handle - // type); "prefers dedicated" is a property of a concrete image and is - // only available here, from VkMemoryDedicatedRequirements. The GL side - // mirrors info.dedicatedAllocation, so this is the single decision both - // sides use -- deciding it independently is what corrupts the image. - VkMemoryRequirements memReqs; - bool useDedicated = m_interopConfig.dedicatedAllocation; - { - VkMemoryDedicatedRequirements dedicatedReqs = {}; - dedicatedReqs.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS; - - VkMemoryRequirements2 memReqs2 = {}; - memReqs2.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2; - memReqs2.pNext = &dedicatedReqs; - - VkImageMemoryRequirementsInfo2 reqInfo = {}; - reqInfo.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2; - reqInfo.image = m_shared[slot].image; - - auto pfnGetImageMemoryRequirements2 = - deviceProc(m_vkDevice, "vkGetImageMemoryRequirements2"); - if (!pfnGetImageMemoryRequirements2) - { - pfnGetImageMemoryRequirements2 = - deviceProc(m_vkDevice, "vkGetImageMemoryRequirements2KHR"); - } - - if (pfnGetImageMemoryRequirements2) - { - pfnGetImageMemoryRequirements2(m_vkDevice, &reqInfo, &memReqs2); - memReqs = memReqs2.memoryRequirements; - if (dedicatedReqs.requiresDedicatedAllocation || dedicatedReqs.prefersDedicatedAllocation) - { - useDedicated = true; - } - } - else - { - vkGetImageMemoryRequirements(m_vkDevice, m_shared[slot].image, &memReqs); - } - - // An explicit override may only relax a preference, never a - // requirement (DEDICATED_ONLY or requiresDedicatedAllocation). - if (m_interopConfig.dedicatedOverridden && !m_interopConfig.dedicatedAllocation && !dedicatedReqs.requiresDedicatedAllocation) - { - useDedicated = false; - } - } - info.dedicatedAllocation = useDedicated; - - // Build the allocation pNext chain back to front, so each link is - // attached exactly once regardless of which options are active: - // - // allocInfo -> exportAllocInfo [-> dedicatedAllocInfo] [-> exportWin32Info] - // - void* chain = nullptr; - -#ifdef PLATFORM_WINDOWS - // Required by the Vulkan specification for OPAQUE_WIN32 handles: the - // export must state the access rights and security attributes the - // handle is created with. Its absence is tolerated by some drivers but - // is a real violation on the platform being debugged. - VkExportMemoryWin32HandleInfoKHR exportWin32Info = {}; - exportWin32Info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_WIN32_HANDLE_INFO_KHR; - exportWin32Info.pNext = chain; - exportWin32Info.pAttributes = nullptr; // default security attributes - exportWin32Info.dwAccess = GENERIC_ALL; - exportWin32Info.name = nullptr; // unnamed: shared within this process only - chain = &exportWin32Info; -#endif - - // Dedicated allocation when the driver requires or prefers it for this - // image. NVIDIA's OPAQUE_WIN32 path in particular needs this paired - // with GL_DEDICATED_MEMORY_OBJECT_EXT on the import side. - VkMemoryDedicatedAllocateInfo dedicatedAllocInfo = {}; - if (useDedicated) - { - dedicatedAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; - dedicatedAllocInfo.pNext = chain; - dedicatedAllocInfo.image = m_shared[slot].image; - dedicatedAllocInfo.buffer = VK_NULL_HANDLE; - chain = &dedicatedAllocInfo; - } - - VkExportMemoryAllocateInfo exportAllocInfo = {}; - exportAllocInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; - exportAllocInfo.pNext = chain; -#ifdef PLATFORM_WINDOWS - exportAllocInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; -#else - exportAllocInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; -#endif - - VkMemoryAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.pNext = &exportAllocInfo; - allocInfo.allocationSize = memReqs.size; - const std::optional memoryTypeIndex = - findMemoryType(m_vkPhysicalDevice, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - if (!memoryTypeIndex) - { - cerr << "ERROR: VulkanView: No device-local memory type for shared image" << endl; - cleanupSharedImage(slot); - return nullptr; - } - allocInfo.memoryTypeIndex = *memoryTypeIndex; - - if (vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_shared[slot].memory) != VK_SUCCESS) - { - cerr << "ERROR: VulkanView: Failed to allocate shared image memory" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - if (vkBindImageMemory(m_vkDevice, m_shared[slot].image, m_shared[slot].memory, 0) != VK_SUCCESS) - { - cerr << "ERROR: VulkanView: Failed to bind shared image memory" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - // Export device memory as a platform-specific external handle - // (opaque FD on Linux, Win32 HANDLE on Windows). The receiving GL - // side imports this with the matching GL_EXT_memory_object_{fd,win32} - // extension so writes from GL land in this Vulkan image. -#ifdef PLATFORM_WINDOWS - auto pfnGetMemoryWin32HandleKHR = deviceProc(m_vkDevice, "vkGetMemoryWin32HandleKHR"); - if (!pfnGetMemoryWin32HandleKHR) - { - cerr << "ERROR: VulkanView: vkGetMemoryWin32HandleKHR not found" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - VkMemoryGetWin32HandleInfoKHR getHandleInfo = {}; - getHandleInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_WIN32_HANDLE_INFO_KHR; - getHandleInfo.memory = m_shared[slot].memory; - getHandleInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; - - HANDLE memHandle = nullptr; - if (pfnGetMemoryWin32HandleKHR(m_vkDevice, &getHandleInfo, &memHandle) != VK_SUCCESS || !memHandle) - { - cerr << "ERROR: VulkanView: Failed to get memory HANDLE" << endl; - cleanupSharedImage(slot); - return nullptr; - } -#else - auto pfnGetMemoryFdKHR = deviceProc(m_vkDevice, "vkGetMemoryFdKHR"); - if (!pfnGetMemoryFdKHR) - { - cerr << "ERROR: VulkanView: vkGetMemoryFdKHR not found" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - VkMemoryGetFdInfoKHR getFdInfo = {}; - getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; - getFdInfo.memory = m_shared[slot].memory; - getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; - - int memFd = -1; - if (pfnGetMemoryFdKHR(m_vkDevice, &getFdInfo, &memFd) != VK_SUCCESS) - { - cerr << "ERROR: VulkanView: Failed to get memory FD" << endl; - cleanupSharedImage(slot); - return nullptr; - } -#endif - - // 2. Create Shared Semaphores - VkExportSemaphoreCreateInfo exportSemInfo = {}; - exportSemInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; -#ifdef PLATFORM_WINDOWS - exportSemInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT; -#else - exportSemInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; -#endif - - VkSemaphoreCreateInfo semInfo = {}; - semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - semInfo.pNext = &exportSemInfo; - - if (vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_shared[slot].glReady) != VK_SUCCESS - || vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_shared[slot].vkReady) != VK_SUCCESS) - { - cerr << "ERROR: VulkanView: Failed to create shared semaphores" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - // Export the GL<->Vulkan sync semaphores as external handles. -#ifdef PLATFORM_WINDOWS - auto pfnGetSemaphoreWin32HandleKHR = deviceProc(m_vkDevice, "vkGetSemaphoreWin32HandleKHR"); - if (!pfnGetSemaphoreWin32HandleKHR) - { - cerr << "ERROR: VulkanView: vkGetSemaphoreWin32HandleKHR not found" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - VkSemaphoreGetWin32HandleInfoKHR getSemHandleInfo = {}; - getSemHandleInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR; - getSemHandleInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT; - - HANDLE glReadyHandle = nullptr; - HANDLE vkReadyHandle = nullptr; - - getSemHandleInfo.semaphore = m_shared[slot].glReady; - if (pfnGetSemaphoreWin32HandleKHR(m_vkDevice, &getSemHandleInfo, &glReadyHandle) != VK_SUCCESS || !glReadyHandle) - { - cerr << "ERROR: VulkanView: Failed to get glReady semaphore HANDLE" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - getSemHandleInfo.semaphore = m_shared[slot].vkReady; - if (pfnGetSemaphoreWin32HandleKHR(m_vkDevice, &getSemHandleInfo, &vkReadyHandle) != VK_SUCCESS || !vkReadyHandle) - { - cerr << "ERROR: VulkanView: Failed to get vkReady semaphore HANDLE" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - info.memoryHandle = memHandle; - info.size = memReqs.size; - info.width = w; - info.height = h; - info.glReadySemaphoreHandle = glReadyHandle; - info.vkReadySemaphoreHandle = vkReadyHandle; -#else - auto pfnGetSemaphoreFdKHR = deviceProc(m_vkDevice, "vkGetSemaphoreFdKHR"); - if (!pfnGetSemaphoreFdKHR) - { - cerr << "ERROR: VulkanView: vkGetSemaphoreFdKHR not found" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - VkSemaphoreGetFdInfoKHR getSemFdInfo = {}; - getSemFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; - getSemFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; - - int glReadyFd = -1; - int vkReadyFd = -1; - - getSemFdInfo.semaphore = m_shared[slot].glReady; - if (pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &glReadyFd) != VK_SUCCESS || glReadyFd < 0) - { - cerr << "ERROR: VulkanView: Failed to get glReady semaphore FD" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - getSemFdInfo.semaphore = m_shared[slot].vkReady; - if (pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &vkReadyFd) != VK_SUCCESS || vkReadyFd < 0) - { - cerr << "ERROR: VulkanView: Failed to get vkReady semaphore FD" << endl; - cleanupSharedImage(slot); - return nullptr; - } - - info.memoryFd = memFd; - info.size = memReqs.size; - info.width = w; - info.height = h; - info.glReadySemaphoreFd = glReadyFd; - info.vkReadySemaphoreFd = vkReadyFd; -#endif - - // Transition the shared image to TRANSFER_SRC optimal initially - VkCommandBuffer cb = m_vkCommandBuffers[0]; - vkResetCommandBuffer(cb, 0); - - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cb, &beginInfo); - - imageBarrier(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, - VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - - vkEndCommandBuffer(cb); - - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cb; - - vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); - VkResult layoutSubmitResult = vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_frameSync[slot].fence); - if (layoutSubmitResult != VK_SUCCESS) - { - if (layoutSubmitResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - cleanupSharedImage(slot); - return nullptr; - } - vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); - - // Signal vkReady initially so GL can start writing to it - VkSubmitInfo signalInfo = {}; - signalInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - signalInfo.signalSemaphoreCount = 1; - signalInfo.pSignalSemaphores = &m_shared[slot].vkReady; - VkResult signalResult = vkQueueSubmit(m_vkQueue, 1, &signalInfo, VK_NULL_HANDLE); - if (signalResult != VK_SUCCESS) - { - if (signalResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - cleanupSharedImage(slot); - return nullptr; - } - - // Commit the new capacity only now that the (re)build fully succeeded. - m_shared[slot].capacityW = capW; - m_shared[slot].capacityH = capH; - - return &info; - } - - //-------------------------------------------------------------------------- - // presentSharedImage - //-------------------------------------------------------------------------- - - void VulkanView::presentSharedImage() - { - const uint32_t slot = m_currentFrame; - const SharedImageInfo& info = m_shared[slot].info; - - if (!m_vkDevice || !m_shared[slot].image || !m_vkSwapchain) - { - return; - } - - // Start-of-frame throttle: wait for this slot's previous frame to finish - // before reusing its acquire semaphore and per-frame resources. This - // replaces the old end-of-frame block; with FIFO acquire back-pressure it - // is what paces the loop to display refresh while still allowing - // FRAMES_IN_FLIGHT frames outstanding. - vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); - - // Acquire image - uint32_t imageIndex; - VkResult result = vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, std::numeric_limits::max(), - m_frameSync[slot].imageAvailable, VK_NULL_HANDLE, &imageIndex); - if (result == VK_ERROR_OUT_OF_DATE_KHR) - { - // The GL side already signaled glReady[slot]/waited vkReady[slot] this - // frame; rebalance the pair before bailing so the next frame on this - // slot can't desync. - drainSharedSemaphores(slot); - handleSwapchainOutOfDate(); - return; - } - if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) - { - drainSharedSemaphores(slot); - if (result == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - - // If this swapchain image is still owned by another in-flight frame, wait - // for that frame's fence before rendering into it, then mark the image as - // now owned by this frame. - if (m_imagesInFlight[imageIndex] != VK_NULL_HANDLE) - { - vkWaitForFences(m_vkDevice, 1, &m_imagesInFlight[imageIndex], VK_TRUE, std::numeric_limits::max()); - } - m_imagesInFlight[imageIndex] = m_frameSync[slot].fence; - - // Reset the frame fence only now, right before the submit that re-signals it. - vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); - - VkCommandBuffer cb = m_vkCommandBuffers[imageIndex]; - vkResetCommandBuffer(cb, 0); - - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cb, &beginInfo); - - // Transition shared image from COLOR_ATTACHMENT_OPTIMAL to TRANSFER_SRC_OPTIMAL - imageBarrier(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, - VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - - // Transition swapchain image to transfer dst - imageBarrier(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - - // Transfer the shared image (always A2B10G10R10, == GL_RGB10_A2) to the - // swapchain image. When the swapchain is also A2B10G10R10 the layouts - // match and a raw copy is correct and cheapest. When the swapchain is - // A2R10G10B10 a raw copy would swap red and blue, so use a blit instead: - // vkCmdBlitImage converts per component (R->R, G->G, B->B) between the - // two formats. Whether the (linear-tiled) shared image can be a blit - // source is checked at shared-image creation; if not, that path is - // refused and syncBuffers() uses the CPU fallback instead. - if (m_vkSwapchainFormat == VK_FORMAT_A2B10G10R10_UNORM_PACK32) - { - VkImageCopy region = {}; - region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.srcSubresource.layerCount = 1; - region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.dstSubresource.layerCount = 1; - region.extent = {static_cast(info.width), static_cast(info.height), 1}; - - vkCmdCopyImage(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_vkSwapchainImages[imageIndex], - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - } - else - { - VkImageBlit blit = {}; - blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - blit.srcSubresource.layerCount = 1; - blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - blit.dstSubresource.layerCount = 1; - blit.srcOffsets[0] = {0, 0, 0}; - blit.srcOffsets[1] = {info.width, info.height, 1}; - blit.dstOffsets[0] = {0, 0, 0}; - blit.dstOffsets[1] = {info.width, info.height, 1}; - - vkCmdBlitImage(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_vkSwapchainImages[imageIndex], - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); - } - - // Transition swapchain image to present - imageBarrier(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); - - vkEndCommandBuffer(cb); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - - // Wait for GL to finish writing (glReady) AND swapchain image to be available - const std::array waitSemaphores = {m_shared[slot].glReady, m_frameSync[slot].imageAvailable}; - const std::array waitStages = {VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; - submitInfo.waitSemaphoreCount = static_cast(waitSemaphores.size()); - submitInfo.pWaitSemaphores = waitSemaphores.data(); - submitInfo.pWaitDstStageMask = waitStages.data(); - - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cb; - - // Signal the image's renderFinished (present waits on it) AND vkReady (so - // GL can write the next frame into this slot's shared image). - const std::array signalSemaphores = {m_vkRenderFinished[imageIndex], m_shared[slot].vkReady}; - submitInfo.signalSemaphoreCount = static_cast(signalSemaphores.size()); - submitInfo.pSignalSemaphores = signalSemaphores.data(); - - VkResult submitResult = vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_frameSync[slot].fence); - if (submitResult != VK_SUCCESS) - { - if (submitResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - - // The frame is committed to the GPU; advance the ring now so the next - // frame uses the other slot. imageIndex/slot below are locals, so this is - // safe before the present call. - m_currentFrame = (m_currentFrame + 1) % FRAMES_IN_FLIGHT; - - // Present, waiting on the image's own renderFinished semaphore. - VkPresentInfoKHR presentInfo = {}; - presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - presentInfo.waitSemaphoreCount = 1; - presentInfo.pWaitSemaphores = &m_vkRenderFinished[imageIndex]; - const std::array swapchains = {m_vkSwapchain}; - presentInfo.swapchainCount = static_cast(swapchains.size()); - presentInfo.pSwapchains = swapchains.data(); - presentInfo.pImageIndices = &imageIndex; - - VkResult presentResult = vkQueuePresentKHR(m_vkQueue, &presentInfo); - // Recreate only on OUT_OF_DATE. VK_SUBOPTIMAL_KHR still presents fine and - // can be reported persistently by some X11/RADV compositors; recreating - // on it every frame caused a swapchain-recreate loop that starved the Qt - // event loop (dead input, no fullscreen). Real resizes report OUT_OF_DATE. - // The submit above is tracked by m_frameSync[slot].fence (waited at the start of - // the next use of this slot), so no end-of-frame block is needed here. - if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) - { - handleSwapchainOutOfDate(); - return; - } - if (presentResult != VK_SUCCESS) - { - if (presentResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - } - - //-------------------------------------------------------------------------- - // presentPixelData - //-------------------------------------------------------------------------- - - void VulkanView::presentPixelData(const void* pixels, int w, int h) - { - const uint32_t slot = m_currentFrame; - - if (!m_vkDevice) - { - return; - } - - if (!m_vkSwapchain || m_vkSwapchainExtent.width != static_cast(w) - || m_vkSwapchainExtent.height != static_cast(h)) - { - // Warm recreate via oldSwapchain (createSwapchain retires the old one). - if (!createSwapchain()) - { - return; - } - } - - // Start-of-frame throttle (matches presentSharedImage): wait for this - // slot's previous frame to finish before reusing its staging buffer, - // acquire semaphore and command resources. - vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); - - size_t size = w * h * 4; - - // Recreate this slot's staging buffer if needed - if (size > m_staging[slot].size) - { - if (m_staging[slot].buffer) - { - vkDestroyBuffer(m_vkDevice, m_staging[slot].buffer, nullptr); - } - if (m_staging[slot].memory) - { - vkFreeMemory(m_vkDevice, m_staging[slot].memory, nullptr); - } - - VkBufferCreateInfo bufferInfo = {}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.size = size; - bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - vkCreateBuffer(m_vkDevice, &bufferInfo, nullptr, &m_staging[slot].buffer); - - VkMemoryRequirements memRequirements; - vkGetBufferMemoryRequirements(m_vkDevice, m_staging[slot].buffer, &memRequirements); - - VkMemoryAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memRequirements.size; - const std::optional memoryTypeIndex = - findMemoryType(m_vkPhysicalDevice, memRequirements.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - if (!memoryTypeIndex) - { - cerr << "ERROR: VulkanView: No host-visible memory type for staging buffer" << endl; - return; - } - allocInfo.memoryTypeIndex = *memoryTypeIndex; - - vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_staging[slot].memory); - vkBindBufferMemory(m_vkDevice, m_staging[slot].buffer, m_staging[slot].memory, 0); - - m_staging[slot].size = size; - } - - // Copy to staging buffer - void* data; - vkMapMemory(m_vkDevice, m_staging[slot].memory, 0, size, 0, &data); - memcpy(data, pixels, size); - vkUnmapMemory(m_vkDevice, m_staging[slot].memory); - - // Acquire image - uint32_t imageIndex; - VkResult result = vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, std::numeric_limits::max(), - m_frameSync[slot].imageAvailable, VK_NULL_HANDLE, &imageIndex); - if (result == VK_ERROR_OUT_OF_DATE_KHR) - { - handleSwapchainOutOfDate(); - return; - } - if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) - { - if (result == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - - // If this swapchain image is still owned by another in-flight frame, wait - // for its fence, then mark it owned by this frame. - if (m_imagesInFlight[imageIndex] != VK_NULL_HANDLE) - { - vkWaitForFences(m_vkDevice, 1, &m_imagesInFlight[imageIndex], VK_TRUE, std::numeric_limits::max()); - } - m_imagesInFlight[imageIndex] = m_frameSync[slot].fence; - - vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); - - VkCommandBuffer cb = m_vkCommandBuffers[imageIndex]; - vkResetCommandBuffer(cb, 0); - - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cb, &beginInfo); - - // Transition image to transfer dst - imageBarrier(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - - // Copy buffer to image - VkBufferImageCopy region = {}; - region.bufferOffset = 0; - region.bufferRowLength = 0; - region.bufferImageHeight = 0; - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.mipLevel = 0; - region.imageSubresource.baseArrayLayer = 0; - region.imageSubresource.layerCount = 1; - region.imageOffset = {0, 0, 0}; - region.imageExtent = {static_cast(w), static_cast(h), 1}; - - vkCmdCopyBufferToImage(cb, m_staging[slot].buffer, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, - ®ion); - - // Transition image to present - imageBarrier(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); - - vkEndCommandBuffer(cb); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - const std::array waitSemaphores = {m_frameSync[slot].imageAvailable}; - const std::array waitStages = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; - submitInfo.waitSemaphoreCount = static_cast(waitSemaphores.size()); - submitInfo.pWaitSemaphores = waitSemaphores.data(); - submitInfo.pWaitDstStageMask = waitStages.data(); - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cb; - const std::array signalSemaphores = {m_vkRenderFinished[imageIndex]}; - submitInfo.signalSemaphoreCount = static_cast(signalSemaphores.size()); - submitInfo.pSignalSemaphores = signalSemaphores.data(); - - VkResult submitResult = vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_frameSync[slot].fence); - if (submitResult != VK_SUCCESS) - { - if (submitResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - - // Frame committed; advance the ring (imageIndex/slot below are locals). - m_currentFrame = (m_currentFrame + 1) % FRAMES_IN_FLIGHT; - - // Present, waiting on the image's own renderFinished semaphore. - VkPresentInfoKHR presentInfo = {}; - presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - presentInfo.waitSemaphoreCount = 1; - presentInfo.pWaitSemaphores = signalSemaphores.data(); - const std::array swapchains = {m_vkSwapchain}; - presentInfo.swapchainCount = static_cast(swapchains.size()); - presentInfo.pSwapchains = swapchains.data(); - presentInfo.pImageIndices = &imageIndex; - - VkResult presentResult = vkQueuePresentKHR(m_vkQueue, &presentInfo); - // Recreate only on OUT_OF_DATE. VK_SUBOPTIMAL_KHR still presents fine and - // can be reported persistently by some X11/RADV compositors; recreating - // on it every frame caused a swapchain-recreate loop that starved the Qt - // event loop (dead input, no fullscreen). Real resizes report OUT_OF_DATE. - // The submit is tracked by m_frameSync[slot].fence (waited at the next use of this - // slot), so no end-of-frame block is needed here. - if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) - { - handleSwapchainOutOfDate(); - return; - } - if (presentResult != VK_SUCCESS) - { - if (presentResult == VK_ERROR_DEVICE_LOST) - { - requestGLFallback(); - } - return; - } - } - - //-------------------------------------------------------------------------- - - void VulkanView::requestUpdate() - { - if (m_stopProcessingEvents) - { - return; - } - // Coalesce: only queue a render if one isn't already pending. The flag is - // cleared at the start of render(), so a resize arriving mid-render - // schedules exactly one follow-up render at the newest size. - if (m_updatePending) - return; - m_updatePending = true; - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); - } - - void VulkanView::render() - { - m_updatePending = false; - - if (m_stopProcessingEvents) - { - return; - } - - IPCore::Session* session = m_doc ? m_doc->session() : nullptr; - if (!session) - return; - - if (m_doc && session && m_videoDevice) - { - m_videoDevice->makeCurrent(); - - if (m_userActive && m_activityTimer.elapsed() > 1.0) - { - if (m_doc->mainPopup() && !m_doc->mainPopup()->isVisible() && m_eventWidget && m_eventWidget->hasFocus()) - { - TwkApp::ActivityChangeEvent aevent("user-inactive", m_videoDevice.get()); - m_videoDevice->sendEvent(aevent); - m_userActive = false; - } - } - - int x = 0, y = 0; - absolutePosition(x, y); - m_videoDevice->setAbsolutePosition(x, y); - - session->render(); - - if (!m_postFirstNonEmptyRender && session->postFirstNonEmptyRender()) - { - m_postFirstNonEmptyRender = true; - if (!session->isFullScreen()) - { - m_doc->resizeToFit(false, false); - m_doc->center(); - } - } - - m_firstPaintCompleted = true; - } - - if (m_stopProcessingEvents) - { - return; - } - - if (session) - { - if (session->outputVideoDevice() && session->outputVideoDevice() != videoDevice()) - { - session->outputVideoDevice()->syncBuffers(); - } - else - { - m_videoDevice->syncBuffers(); - } - } - - if (session) - { - session->addSyncSample(); - session->postRender(); - } - - m_eventProcessingTimer.start(); - } - - //-------------------------------------------------------------------------- - // QWidget overrides - //-------------------------------------------------------------------------- - - void VulkanView::showEvent(QShowEvent* event) - { - if (!m_initialized) - initialize(); - requestUpdate(); - QWidget::showEvent(event); - } - - void VulkanView::resizeEvent(QResizeEvent* event) - { - if (m_doc) - m_doc->viewSizeChanged(event->size().width(), event->size().height()); - QWidget::resizeEvent(event); - - // WA_PaintOnScreen means Qt won't repaint this native surface on resize, - // so drive a render now to recreate the swapchain at the new size and - // present immediately (instead of waiting for a mouse Enter event). - // Coalesced so a fast drag doesn't queue one heavy recreate per event. - if (!m_stopProcessingEvents) - { - requestUpdate(); - } - } - - void VulkanView::paintEvent(QPaintEvent* event) - { - if (m_stopProcessingEvents) - { - return; - } - - if (m_doc && m_doc->session() && m_doc->session()->outputVideoDevice()) - { - m_doc->session()->outputVideoDevice()->syncBuffers(); - } - else if (m_videoDevice) - { - m_videoDevice->syncBuffers(); - } - } - - //-------------------------------------------------------------------------- - // eventProcessingTimeout slot - //-------------------------------------------------------------------------- - - void VulkanView::eventProcessingTimeout() - { - if (m_doc && m_doc->session()) - m_doc->session()->userGenericEvent("per-render-event-processing", ""); - } - - //-------------------------------------------------------------------------- - // event() - //-------------------------------------------------------------------------- - - bool VulkanView::event(QEvent* event) - { - bool keyevent = false; - Rv::Session* session = m_doc ? m_doc->session() : nullptr; - - if (m_stopProcessingEvents) - { - event->accept(); - return true; - } - - if (event->type() == QEvent::WindowActivate) - m_activationTimer.start(); - - float activationTime = 0.0f; - 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) - { - m_activityTimer.stop(); - m_activityTimer.start(); - - if (!m_userActive) - { - TwkApp::ActivityChangeEvent aevent("user-active", m_videoDevice.get()); - 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(); - // fall-through - case QEvent::Enter: - if (m_eventWidget) - m_eventWidget->setFocus(Qt::MouseFocusReason); - break; - default: - break; - } - - if (event->type() == QEvent::Resize) - { - QResizeEvent* e = static_cast(event); - 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 QWidget::event(event); - } - - if (event->type() == QEvent::UpdateRequest) - { - render(); - return true; - } - - if (!m_videoDevice || !m_videoDevice->hasTranslator()) - { - return QWidget::event(event); - } - - auto resetTranslator = [this]() - { - m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0f, 1.0f); - m_videoDevice->translator().setRelativeDomain(width(), height()); - }; - - 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 = static_cast(width()); - const float h = static_cast(height()); - const float ow = static_cast(odv->width()); - const float oh = static_cast(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.0f; - const float xscale = yscale; - const float xoffset = -(w * yscale - ow) / 2.0f; - m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); - } - else - { - const float xscale = ow / w; - const float xoffset = 0.0f; - const float yscale = xscale; - const float yoffset = -(xscale * h - oh) / 2.0f; - m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); - } - } - else - { - resetTranslator(); - } - } - else - { - resetTranslator(); - } - } - else - { - resetTranslator(); - } - - if (session) - session->setEventVideoDevice(videoDevice()); - - if (m_videoDevice->translator().sendQTEvent(event, activationTime)) - { - event->accept(); - return true; - } - else - { - return QWidget::event(event); - } - } - - //-------------------------------------------------------------------------- - // eventFilter() - //-------------------------------------------------------------------------- - - bool VulkanView::eventFilter(QObject* object, QEvent* event) - { - if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease || event->type() == QEvent::Shortcut - || event->type() == QEvent::ShortcutOverride) - { - 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(); - } - - Session* session = m_doc ? m_doc->session() : nullptr; - if (session) - { - session->setEventVideoDevice(videoDevice()); - if (m_videoDevice->translator().sendQTEvent(event)) - { - event->accept(); - return true; - } - } - - event->accept(); - return true; - } - - return false; - } + bool VulkanView::supports10BitPresentation() { return VulkanWindow::supports10BitPresentation(); } } // namespace Rv diff --git a/src/lib/app/RvCommon/VulkanWindow.cpp b/src/lib/app/RvCommon/VulkanWindow.cpp new file mode 100644 index 000000000..c78f26b01 --- /dev/null +++ b/src/lib/app/RvCommon/VulkanWindow.cpp @@ -0,0 +1,3100 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if defined(PLATFORM_LINUX) || defined(PLATFORM_WINDOWS) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef PLATFORM_WINDOWS +// Keep from pulling in , which collides with Qt's +// . +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#endif + +// +// Environment overrides, all reported in the startup record: +// +// RV_VULKAN_FORCE_CPU_PRESENT skip zero-copy interop; CPU readback. +// RV_VULKAN_FORCE_TILING optimal | linear; refused if the driver +// does not report it exportable. +// RV_VULKAN_FORCE_NO_DEDICATED suppress preferred dedicated allocation; +// refused for DEDICATED_ONLY. Alias: +// RV_VULKAN_DISABLE_DEDICATED_ALLOCATION. +// + +namespace Rv +{ + using namespace std; + + // A best-effort output forces one blocking present once it is this stale. + static constexpr double kMaxStaleSeconds = 0.1; + + // -debug gpu frame-time accumulators, fed only by control viewports. They + // are shared: with several documents open the totals are combined. + static unsigned int s_diagFrames = 0; + static double s_diagRenderMs = 0.0; + static double s_diagMainPresentMs = 0.0; + static double s_diagOutPresentMs = 0.0; + static double s_diagFenceWaitMs = 0.0; + static double s_diagAcquireMs = 0.0; + static double s_diagLoopMs = 0.0; + static TwkUtil::Timer s_diagLoopTimer; + // Post-present work: in the frame period but not in "total". + static double s_diagPostRenderMs = 0.0; + // handler: time in the pointer handler. eventToRender: age of the newest + // pointer event when its frame starts rendering. + static double s_diagPointerHandlerMs = 0.0; + static unsigned int s_diagPointerEvents = 0; + static double s_diagPointerAgeMs = 0.0; + static unsigned int s_diagPointerAgeSamples = 0; + static TwkUtil::Timer s_diagPointerTimer; + static bool s_diagPointerPending = false; + + // eventToRetire: end-to-end latency, from the pointer event to its + // frame's GPU retirement. Needs an absolute clock since retirement lags + // by several frames. + static TwkUtil::Timer s_diagClock; + + namespace + { + double diagNow() + { + if (!s_diagClock.isRunning()) + { + s_diagClock.start(); + } + return s_diagClock.elapsed(); + } + } // namespace + + // Event time for the frame being rendered (-1 if none), handed to its + // slot at submit. + static double s_diagFrameEventTime = -1.0; + static std::array s_diagSlotEventTime{}; + static std::array s_diagSlotArmed{}; + static double s_diagEventToRetireMs = 0.0; + static unsigned int s_diagEventToRetireSamples = 0; + + using namespace TwkApp; + using namespace IPCore; + + namespace + { + // R/B order is handled where pixels are packed or blitted. Many RADV + // surfaces only advertise A2R10G10B10. + bool isTenBitFormat(VkFormat format) + { + return format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; + } + + std::optional findGraphicsPresentQueue(VkPhysicalDevice device, VkSurfaceKHR surface) + { + uint32_t familyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(device, &familyCount, nullptr); + std::vector families(familyCount); + vkGetPhysicalDeviceQueueFamilyProperties(device, &familyCount, families.data()); + + for (uint32_t i = 0; i < familyCount; ++i) + { + VkBool32 presentSupport = VK_FALSE; + if (vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport) == VK_SUCCESS + && (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) + { + return i; + } + } + return std::nullopt; + } + + bool surfaceHasTenBitFormat(VkPhysicalDevice device, VkSurfaceKHR surface) + { + uint32_t formatCount = 0; + if (vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr) != VK_SUCCESS || formatCount == 0) + { + return false; + } + + std::vector formats(formatCount); + if (vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, formats.data()) != VK_SUCCESS) + { + return false; + } + + return std::any_of(formats.begin(), formats.end(), + [](const VkSurfaceFormatKHR& format) { return isTenBitFormat(format.format); }); + } + + bool deviceHasExtension(VkPhysicalDevice device, const char* name) + { + uint32_t extensionCount = 0; + if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr) != VK_SUCCESS) + { + return false; + } + + std::vector extensions(extensionCount); + if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, extensions.data()) != VK_SUCCESS) + { + return false; + } + + return std::any_of(extensions.begin(), extensions.end(), + [name](const VkExtensionProperties& extension) { return strcmp(extension.extensionName, name) == 0; }); + } + + // synchronization2 is a required 1.3 feature, so the version is enough. + bool deviceSupportsVulkan13(VkPhysicalDevice device) + { + VkPhysicalDeviceProperties props = {}; + vkGetPhysicalDeviceProperties(device, &props); + return props.apiVersion >= VK_API_VERSION_1_3; + } + + constexpr std::string_view formatName(VkFormat format) + { + switch (format) + { + case VK_FORMAT_B8G8R8A8_UNORM: + return "B8G8R8A8_UNORM"; + case VK_FORMAT_B8G8R8A8_SRGB: + return "B8G8R8A8_SRGB"; + case VK_FORMAT_R8G8B8A8_UNORM: + return "R8G8B8A8_UNORM"; + case VK_FORMAT_R8G8B8A8_SRGB: + return "R8G8B8A8_SRGB"; + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + return "A2B10G10R10_UNORM_PACK32"; + case VK_FORMAT_A2R10G10B10_UNORM_PACK32: + return "A2R10G10B10_UNORM_PACK32"; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return "R16G16B16A16_SFLOAT"; + default: + return "(other)"; + } + } + } // namespace + + //-------------------------------------------------------------------------- + // VulkanWindow implementation + //-------------------------------------------------------------------------- + + VulkanWindow::VulkanWindow(RvDocument* doc, bool noResize) + : QWindow() + , m_doc(doc) + , m_postFirstNonEmptyRender(noResize) + { + setSurfaceType(QSurface::VulkanSurface); + + QSurfaceFormat fmt; + fmt.setRedBufferSize(10); + fmt.setGreenBufferSize(10); + fmt.setBlueBufferSize(10); + fmt.setAlphaBufferSize(2); + setFormat(fmt); + + m_activityTimer.start(); + + m_eventProcessingTimer.setSingleShot(true); + connect(&m_eventProcessingTimer, SIGNAL(timeout()), this, SLOT(eventProcessingTimeout())); + } + + VulkanWindow::~VulkanWindow() + { + m_videoDevice = nullptr; + + // Usually a no-op: the SurfaceAboutToBeDestroyed handler in event() + // already released everything. + releaseVulkanResources(); + } + + //-------------------------------------------------------------------------- + + void VulkanWindow::stopProcessingEvents() { m_stopProcessingEvents = true; } + + void VulkanWindow::absolutePosition(int& x, int& y) const + { + QPoint gp = mapToGlobal(QPoint(0, 0)); + x = gp.x(); + y = gp.y(); + } + + float VulkanWindow::devicePixelRatioF() const { return static_cast(QWindow::devicePixelRatio()); } + + bool VulkanWindow::physicalDeviceMatchesUUID(const unsigned char* uuid, size_t size) const + { + if (!m_vkPhysicalDevice || !uuid || size != VK_UUID_SIZE) + { + return false; + } + + VkPhysicalDeviceIDProperties idProperties = {}; + idProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES; + + VkPhysicalDeviceProperties2 properties = {}; + properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + properties.pNext = &idProperties; + vkGetPhysicalDeviceProperties2(m_vkPhysicalDevice, &properties); + + return std::equal(idProperties.deviceUUID, idProperties.deviceUUID + VK_UUID_SIZE, uuid); + } + + //-------------------------------------------------------------------------- + // Vulkan Initialisation + //-------------------------------------------------------------------------- + + void VulkanWindow::initialize() + { + if (m_initialized) + { + return; + } + + if (!initVulkan()) + { + cerr << "ERROR: VulkanWindow: initVulkan failed; falling back to OpenGL" << endl; + requestGLFallback(); + return; + } + + m_initialized = true; + m_initializedHandle = handle(); + + // Session init must not run from here: loading packages can create a + // QWebEngineView, which destroys this window while it is on the stack. + // RvApplication::newSessionFromFiles() does it after show() instead. + } + + // + // One process-lifetime QVulkanInstance, shared by the 10-bit probe and + // every VulkanWindow, never destroyed: destroying a VkInstance shortly + // before another init corrupts RADV's X11 WSI state and segfaults in + // vkGetPhysicalDeviceSurfaceSupportKHR. + // + namespace + { + QVulkanInstance* sharedVulkanInstance() + { + static QVulkanInstance* instance = []() -> QVulkanInstance* + { + auto inst = std::make_unique(); + // 1.3 for synchronization2 (vkQueueSubmit2 / vkCmdPipelineBarrier2). + inst->setApiVersion(QVersionNumber(1, 3)); + if (!inst->create()) + { + cerr << "ERROR: VulkanWindow: shared QVulkanInstance create failed" << endl; + return nullptr; + } + return inst.release(); + }(); + return instance; + } + } // namespace + + bool VulkanWindow::supports10BitPresentation() + { + // A fixed hardware property: probe once per process. + static const bool cached = []() -> bool + { + QVulkanInstance* qtVkInst = sharedVulkanInstance(); + if (!qtVkInst) + { + return false; + } + + VkInstance instance = qtVkInst->vkInstance(); + if (instance == VK_NULL_HANDLE) + { + return false; + } + + // Leaked for the same RADV WSI reason as sharedVulkanInstance(). + static QWindow* dummyWindow = []() -> QWindow* + { + auto* window = new QWindow(); + window->setSurfaceType(QSurface::VulkanSurface); + window->create(); + return window; + }(); + dummyWindow->setVulkanInstance(qtVkInst); + + VkSurfaceKHR dummySurface = qtVkInst->surfaceForWindow(dummyWindow); + if (!dummySurface) + { + cerr << "ERROR: VulkanWindow: supports10BitPresentation: failed to create dummy surface" << endl; + return false; + } + + uint32_t deviceCount = 0; + vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); + if (deviceCount == 0) + { + cerr << "ERROR: VulkanWindow: supports10BitPresentation: vkEnumeratePhysicalDevices returned 0 devices" << endl; + return false; + } + std::vector devices(deviceCount); + vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); + + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: supports10BitPresentation: probing " << deviceCount << " physical device(s)" << endl; + } + + bool any10bit = false; + for (uint32_t di = 0; di < devices.size(); ++di) + { + VkPhysicalDevice dev = devices[di]; + + const bool vulkan13 = deviceSupportsVulkan13(dev); + const bool canPresent = + deviceHasExtension(dev, VK_KHR_SWAPCHAIN_EXTENSION_NAME) && findGraphicsPresentQueue(dev, dummySurface).has_value(); + const bool has10bit = canPresent && surfaceHasTenBitFormat(dev, dummySurface); + any10bit = any10bit || (vulkan13 && has10bit); + + VkPhysicalDeviceProperties props = {}; + vkGetPhysicalDeviceProperties(dev, &props); + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: device[" << di << "] '" << props.deviceName + << "': vulkan1.3=" << (vulkan13 ? "YES" : "NO") << " graphics+present=" << (canPresent ? "YES" : "NO") + << " 10-bit surface format=" << (has10bit ? "YES" : "NO") << endl; + } + } + + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: supports10BitPresentation: returning " << (any10bit ? "true" : "false") << endl; + } + return any10bit; + }(); + + return cached; + } + + bool VulkanWindow::initVulkan() + { + QVulkanInstance* qtVkInst = sharedVulkanInstance(); + if (!qtVkInst) + { + cerr << "ERROR: VulkanWindow: shared QVulkanInstance unavailable" << endl; + return false; + } + + m_vkInstance = qtVkInst->vkInstance(); + + // Re-creating after a reparent can get here before the platform window. + if (!handle()) + { + create(); + } + + setVulkanInstance(qtVkInst); + + m_vkSurface = qtVkInst->surfaceForWindow(this); + if (!m_vkSurface) + { + cerr << "ERROR: VulkanWindow: Failed to create Vulkan surface" << endl; + return false; + } + + uint32_t deviceCount = 0; + vkEnumeratePhysicalDevices(m_vkInstance, &deviceCount, nullptr); + if (deviceCount == 0) + { + return false; + } + std::vector devices(deviceCount); + vkEnumeratePhysicalDevices(m_vkInstance, &deviceCount, devices.data()); + + m_vkPhysicalDevice = VK_NULL_HANDLE; + bool foundQueue = false; + m_queueFamilyIndex = 0; + + for (VkPhysicalDevice dev : devices) + { + if (!deviceSupportsVulkan13(dev)) + { + continue; + } + const std::optional queueFamily = findGraphicsPresentQueue(dev, m_vkSurface); + if (queueFamily && surfaceHasTenBitFormat(dev, m_vkSurface)) + { + m_vkPhysicalDevice = dev; + m_queueFamilyIndex = *queueFamily; + foundQueue = true; + break; + } + } + + if (!foundQueue) + { + cerr << "ERROR: VulkanWindow: initVulkan: no Vulkan 1.3 device with graphics, present and 10-bit surface support" << endl; + return false; + } + + { + // Unconditional: compared against GL_RENDERER to spot hybrid-GPU + // machines. + VkPhysicalDeviceProperties props = {}; + vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &props); + cout << "INFO: VulkanWindow: initVulkan: picked physical device '" << props.deviceName << "' (of " << deviceCount + << " available)" << endl; + } + + float queuePriority = 1.0f; + VkDeviceQueueCreateInfo queueCreateInfo = {}; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = m_queueFamilyIndex; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + + std::vector deviceExtensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; + if (!deviceHasExtension(m_vkPhysicalDevice, VK_KHR_SWAPCHAIN_EXTENSION_NAME)) + { + cerr << "ERROR: VulkanWindow: selected device does not support VK_KHR_swapchain." << endl; + return false; + } + +#ifdef PLATFORM_WINDOWS + const char* externalMemoryExtension = VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME; + const char* externalSemaphoreExtension = VK_KHR_EXTERNAL_SEMAPHORE_WIN32_EXTENSION_NAME; +#else + const char* externalMemoryExtension = VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; + const char* externalSemaphoreExtension = VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME; +#endif + m_externalInteropSupported = deviceHasExtension(m_vkPhysicalDevice, externalMemoryExtension) + && deviceHasExtension(m_vkPhysicalDevice, externalSemaphoreExtension); + if (m_externalInteropSupported) + { + deviceExtensions.push_back(externalMemoryExtension); + deviceExtensions.push_back(externalSemaphoreExtension); + } + else if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: external memory/semaphore extensions unavailable; using CPU fallback." << endl; + } + + VkPhysicalDeviceVulkan13Features features13 = {}; + features13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + features13.synchronization2 = VK_TRUE; + + VkDeviceCreateInfo createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + createInfo.pNext = &features13; + createInfo.pQueueCreateInfos = &queueCreateInfo; + createInfo.queueCreateInfoCount = 1; + createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + createInfo.ppEnabledExtensionNames = deviceExtensions.data(); + + if (vkCreateDevice(m_vkPhysicalDevice, &createInfo, nullptr, &m_vkDevice) != VK_SUCCESS) + { + return false; + } + + vkGetDeviceQueue(m_vkDevice, m_queueFamilyIndex, 0, &m_vkQueue); + + negotiateInteropConfig(); + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: initVulkan: interop negotiation ran (once per device); result=" + << (m_interopConfig.supported ? "supported" : "unsupported") << endl; + } + + auto failInit = [this]() + { + cleanupVulkan(); + return false; + }; + + VkCommandPoolCreateInfo poolInfo = {}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + poolInfo.queueFamilyIndex = m_queueFamilyIndex; + if (vkCreateCommandPool(m_vkDevice, &poolInfo, nullptr, &m_vkCommandPool) != VK_SUCCESS) + { + return failInit(); + } + + // Per-image renderFinished semaphores are created in createSwapchain(). + VkSemaphoreCreateInfo semaphoreInfo = {}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + // Signaled so the first wait on a slot passes. + VkFenceCreateInfo fenceInfo = {}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + for (FrameSync& frame : m_frameSync) + { + if (vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &frame.imageAvailable) != VK_SUCCESS) + { + return failInit(); + } + if (vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &frame.fence) != VK_SUCCESS) + { + return failInit(); + } + } + + return true; + } + + // Queue fallbackVulkanToGLView on the next event-loop tick (at most once). + void VulkanWindow::requestGLFallback() + { + if (m_glFallbackRequested || !m_doc || m_stopProcessingEvents || m_doc->isClosing()) + { + return; + } + m_glFallbackRequested = true; + + // Logged explicitly since OpenGL forgoes 10-bit, even if a record was + // already emitted for an earlier path. + m_recordEmitted = false; + reportPresentPath(PresentPath::OpenGL, + m_presentPathReason.empty() ? std::string("Vulkan presentation could not be established") : m_presentPathReason); + + QTimer::singleShot(0, m_doc, [doc = m_doc]() { doc->fallbackVulkanToGLView(); }); + } + + // Skip swapchain work during close or zero-size resize. + bool VulkanWindow::presentationAllowed() const + { + if (m_stopProcessingEvents) + { + return false; + } + if (width() <= 0 || height() <= 0) + { + return false; + } + if (m_doc && m_doc->isClosing()) + { + return false; + } + return true; + } + + // Rebuild all Vulkan state after the native surface is lost. + void VulkanWindow::handleSurfaceLost() + { + // Qt replaces the platform window when the top-level QWidgetWindow is + // recreated (e.g. inserting a QWebEngineView). The VkSurfaceKHR itself + // is stale, so a swapchain recreate is not enough. + cout << "INFO: VulkanWindow: platform window recreated; rebuilding Vulkan surface" << endl; + + releaseVulkanResources(); + } + + void VulkanWindow::releaseVulkanResources() + { + // Drop the GL imports first so GL never aliases memory freed below. + if (m_videoDevice) + { + m_videoDevice->releaseSharedGLObjects(); + + // The next initVulkan() may pick a different physical device. + m_videoDevice->resetInteropDeviceMatch(); + } + + for (uint32_t i = 0; i < kFramesInFlight; ++i) + { + cleanupSharedImage(i); + } + cleanupSwapchain(); + cleanupVulkan(); + + // Owned by QVulkanInstance; not destroyed here. + m_vkSurface = VK_NULL_HANDLE; + m_vkPhysicalDevice = VK_NULL_HANDLE; + m_vkSwapchainFormat = VK_FORMAT_UNDEFINED; + m_vkSwapchainExtent = {}; + m_vkSwapchainImages.clear(); + m_currentFrame = 0; + + m_interopNegotiated = false; + m_interopConfig = {}; + m_sharedImageUnusable = false; + + m_initialized = false; + m_initializedHandle = nullptr; + } + + void VulkanWindow::handleSwapchainOutOfDate() + { + if (!m_vkDevice || !presentationAllowed()) + { + return; + } + + // The shared image is content-sized and the acquire semaphores are + // per frame and may still be referenced by queued submissions, so only + // the swapchain is recreated. + if (!createSwapchain()) + { + requestGLFallback(); + } + } + + void VulkanWindow::cleanupVulkan() + { + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + for (FrameSync& frame : m_frameSync) + { + if (frame.imageAvailable) + { + vkDestroySemaphore(m_vkDevice, frame.imageAvailable, nullptr); + frame.imageAvailable = VK_NULL_HANDLE; + } + if (frame.fence) + { + vkDestroyFence(m_vkDevice, frame.fence, nullptr); + frame.fence = VK_NULL_HANDLE; + } + } + + if (m_vkCommandPool) + { + vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr); + m_vkCommandPool = VK_NULL_HANDLE; + } + + vkDestroyDevice(m_vkDevice, nullptr); + m_vkDevice = VK_NULL_HANDLE; + } + m_vkQueue = VK_NULL_HANDLE; + m_externalInteropSupported = false; + // The VkSurfaceKHR is owned by QVulkanInstance; do not destroy it here. + } + + // + // FIFO everywhere. For measurement, RV_VULKAN_PRESENT_MODE (control + // viewport) and RV_VULKAN_OUTPUT_PRESENT_MODE (presentation output) take + // fifo | relaxed | mailbox | immediate. + // + namespace + { + constexpr std::string_view presentModeName(VkPresentModeKHR mode) + { + switch (mode) + { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "IMMEDIATE"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "MAILBOX"; + case VK_PRESENT_MODE_FIFO_KHR: + return "FIFO"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "FIFO_RELAXED"; + default: + return "(other)"; + } + } + + // name may be null (unset environment variable). + std::optional presentModeFromName(const char* name) + { + if (!name) + { + return std::nullopt; + } + const std::string_view modeName(name); + if (modeName == "fifo") + { + return VK_PRESENT_MODE_FIFO_KHR; + } + if (modeName == "relaxed") + { + return VK_PRESENT_MODE_FIFO_RELAXED_KHR; + } + if (modeName == "mailbox") + { + return VK_PRESENT_MODE_MAILBOX_KHR; + } + if (modeName == "immediate") + { + return VK_PRESENT_MODE_IMMEDIATE_KHR; + } + return std::nullopt; + } + + VkPresentModeKHR choosePresentMode(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, bool passiveOutput) + { + uint32_t count = 0; + vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &count, nullptr); + std::vector available(count); + if (count) + { + vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &count, available.data()); + } + + const auto supported = [&](VkPresentModeKHR mode) + { return std::find(available.begin(), available.end(), mode) != available.end(); }; + + const std::optional forced = + presentModeFromName(getenv(passiveOutput ? "RV_VULKAN_OUTPUT_PRESENT_MODE" : "RV_VULKAN_PRESENT_MODE")); + if (forced && !supported(*forced)) + { + cout << "WARNING: VulkanWindow: requested present mode " << presentModeName(*forced) << " is unsupported; using FIFO" + << endl; + return VK_PRESENT_MODE_FIFO_KHR; + } + + return forced.value_or(VK_PRESENT_MODE_FIFO_KHR); + } + } // namespace + + bool VulkanWindow::createSwapchain() + { + if (!m_vkDevice || !m_vkSurface) + { + return false; + } + + if (!presentationAllowed()) + { + return false; + } + + VkSurfaceCapabilitiesKHR capabilities; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_vkPhysicalDevice, m_vkSurface, &capabilities) != VK_SUCCESS) + { + requestGLFallback(); + return false; + } + + uint32_t formatCount = 0; + if (vkGetPhysicalDeviceSurfaceFormatsKHR(m_vkPhysicalDevice, m_vkSurface, &formatCount, nullptr) != VK_SUCCESS || formatCount == 0) + { + requestGLFallback(); + return false; + } + std::vector formats(formatCount); + if (vkGetPhysicalDeviceSurfaceFormatsKHR(m_vkPhysicalDevice, m_vkSurface, &formatCount, formats.data()) != VK_SUCCESS) + { + requestGLFallback(); + return false; + } + + // Unconditional, once per window: the colour space and order of each + // 10-bit entry are vendor-specific. + if (!m_loggedSurfaceFormatList) + { + m_loggedSurfaceFormatList = true; + + cout << "INFO: VulkanWindow: createSwapchain: surface offers " << formatCount << " format(s):" << endl; + for (uint32_t i = 0; i < formats.size(); ++i) + { + cout << "INFO: VulkanWindow: [" << i << "] format=" << formats[i].format << " (" << formatName(formats[i].format) + << ") colorSpace=" << formats[i].colorSpace << endl; + } + } + + VkSurfaceFormatKHR surfaceFormat = formats[0]; + bool found10bit = false; + // Prefer A2B10G10R10 (== GL_RGB10_A2) so the transfer is a plain copy. + // RV emits sRGB, so require SRGB_NONLINEAR: in HDR mode NVIDIA lists a + // 10-bit HDR10_ST2084 entry first, which renders the viewport black. + const auto findTenBit = [&formats](VkFormat wanted, bool requireSrgbNonlinear, VkSurfaceFormatKHR& out) -> bool + { + for (const auto& fmt : formats) + { + if (fmt.format != wanted) + { + continue; + } + if (requireSrgbNonlinear && fmt.colorSpace != VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + continue; + } + out = fmt; + return true; + } + return false; + }; + + for (const VkFormat wanted : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) + { + if (findTenBit(wanted, true, surfaceFormat)) + { + found10bit = true; + break; + } + } + + // No SRGB_NONLINEAR pairing: keep 10-bit anyway and warn below. + if (!found10bit) + { + for (const VkFormat wanted : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) + { + if (findTenBit(wanted, false, surfaceFormat)) + { + found10bit = true; + break; + } + } + } + + if (found10bit) + { + // Unconditional, but only on change (runs on every resize). + if (surfaceFormat.format != m_loggedSurfaceFormat.format || surfaceFormat.colorSpace != m_loggedSurfaceFormat.colorSpace) + { + m_loggedSurfaceFormat = surfaceFormat; + + cout << "INFO: VulkanWindow: createSwapchain: chose " << formatName(surfaceFormat.format) + << " colorSpace=" << surfaceFormat.colorSpace + << (surfaceFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ? " (SRGB_NONLINEAR)" : " (NOT SRGB_NONLINEAR)") + << " (10-bit OK)" << endl; + + if (surfaceFormat.colorSpace != VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + cout << "WARNING: VulkanWindow: no 10-bit surface format is paired with SRGB_NONLINEAR on this surface. RV emits " + "sRGB, so the image may look wrong (dark or washed out)." + << endl; + } + } + } + else + { + cout << "WARNING: VulkanWindow: Real surface lacks a 10-bit format (A2B10G10R10/A2R10G10B10); requesting OpenGL fallback" + << endl; + requestGLFallback(); + return false; + } + + m_vkSwapchainFormat = surfaceFormat.format; + m_sharedImageUnusable = false; + + m_vkSwapchainExtent = capabilities.currentExtent; + if (m_vkSwapchainExtent.width == std::numeric_limits::max()) + { + const qreal dpr = devicePixelRatio(); + const uint32_t pixelWidth = static_cast(std::max(1.0, width() * dpr)); + const uint32_t pixelHeight = static_cast(std::max(1.0, height() * dpr)); + m_vkSwapchainExtent = {std::clamp(pixelWidth, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(pixelHeight, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)}; + } + if (m_vkSwapchainExtent.width == 0 || m_vkSwapchainExtent.height == 0) + { + requestGLFallback(); + return false; + } + + const VkPresentModeKHR presentMode = choosePresentMode(m_vkPhysicalDevice, m_vkSurface, /*passiveOutput*/ m_doc == nullptr); + + uint32_t imageCount = capabilities.minImageCount + 1; + // MAILBOX needs three images (scanout, queued, rendering) or acquire + // blocks. + if (presentMode == VK_PRESENT_MODE_MAILBOX_KHR && imageCount < 3) + { + imageCount = 3; + } + if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) + { + imageCount = capabilities.maxImageCount; + } + + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: createSwapchain: " << (m_doc ? "control viewport" : "presentation output") + << ": presentMode=" << presentModeName(presentMode) << " images=" << imageCount + << " (surface min=" << capabilities.minImageCount << " max=" << capabilities.maxImageCount << ")" << endl; + } + + VkSwapchainCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + createInfo.surface = m_vkSurface; + createInfo.minImageCount = imageCount; + createInfo.imageFormat = surfaceFormat.format; + createInfo.imageColorSpace = surfaceFormat.colorSpace; + createInfo.imageExtent = m_vkSwapchainExtent; + createInfo.imageArrayLayers = 1; + if (!(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT)) + { + cout << "WARNING: VulkanWindow: surface does not support transfer-destination swapchain images; requesting OpenGL fallback" + << endl; + requestGLFallback(); + return false; + } + createInfo.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + createInfo.preTransform = capabilities.currentTransform; + constexpr std::array compositeAlphaPreference = { + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, + }; + createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + for (const VkCompositeAlphaFlagBitsKHR alpha : compositeAlphaPreference) + { + if (capabilities.supportedCompositeAlpha & alpha) + { + createInfo.compositeAlpha = alpha; + break; + } + } + createInfo.presentMode = presentMode; + createInfo.clipped = VK_TRUE; + // Lets the driver reuse the retiring swapchain's resources on resize. + createInfo.oldSwapchain = m_vkSwapchain; + + // A failed create leaves the existing swapchain intact. + VkSwapchainKHR newSwapchain = VK_NULL_HANDLE; + if (vkCreateSwapchainKHR(m_vkDevice, &createInfo, nullptr, &newSwapchain) != VK_SUCCESS) + { + requestGLFallback(); + return false; + } + + if (m_vkSwapchain != VK_NULL_HANDLE) + { + vkDeviceWaitIdle(m_vkDevice); + if (!m_vkCommandBuffers.empty()) + { + vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, static_cast(m_vkCommandBuffers.size()), + m_vkCommandBuffers.data()); + m_vkCommandBuffers.clear(); + } + vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr); + } + m_vkSwapchain = newSwapchain; + + vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, nullptr); + m_vkSwapchainImages.resize(imageCount); + vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, m_vkSwapchainImages.data()); + + m_vkCommandBuffers.resize(imageCount); + VkCommandBufferAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = m_vkCommandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = static_cast(m_vkCommandBuffers.size()); + if (vkAllocateCommandBuffers(m_vkDevice, &allocInfo, m_vkCommandBuffers.data()) != VK_SUCCESS) + { + cleanupSwapchain(); + requestGLFallback(); + return false; + } + + // The device is idle here, so old renderFinished semaphores are safe + // to destroy. + for (VkSemaphore sem : m_vkRenderFinished) + { + if (sem) + { + vkDestroySemaphore(m_vkDevice, sem, nullptr); + } + } + m_vkRenderFinished.assign(imageCount, VK_NULL_HANDLE); + VkSemaphoreCreateInfo rfInfo = {}; + rfInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + for (uint32_t i = 0; i < imageCount; ++i) + { + if (vkCreateSemaphore(m_vkDevice, &rfInfo, nullptr, &m_vkRenderFinished[i]) != VK_SUCCESS) + { + cleanupSwapchain(); + requestGLFallback(); + return false; + } + } + m_imagesInFlight.assign(imageCount, VK_NULL_HANDLE); + + return true; + } + + void VulkanWindow::cleanupSwapchain() + { + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + for (VkSemaphore sem : m_vkRenderFinished) + { + if (sem) + { + vkDestroySemaphore(m_vkDevice, sem, nullptr); + } + } + m_vkRenderFinished.clear(); + m_imagesInFlight.clear(); + + for (StagingBuffer& staging : m_staging) + { + if (staging.buffer) + { + vkDestroyBuffer(m_vkDevice, staging.buffer, nullptr); + staging.buffer = VK_NULL_HANDLE; + } + if (staging.memory) + { + vkFreeMemory(m_vkDevice, staging.memory, nullptr); + staging.memory = VK_NULL_HANDLE; + } + staging.size = 0; + } + + if (!m_vkCommandBuffers.empty()) + { + vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, static_cast(m_vkCommandBuffers.size()), + m_vkCommandBuffers.data()); + m_vkCommandBuffers.clear(); + } + + if (m_vkSwapchain) + { + vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr); + m_vkSwapchain = VK_NULL_HANDLE; + } + } + } + + namespace + { + std::optional findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) + { + VkPhysicalDeviceMemoryProperties memProperties; + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) + { + if ((typeFilter & (1u << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) + { + return i; + } + } + return std::nullopt; + } + + // Device-level entry point lookup, cast to its PFN type. + template Fn deviceProc(VkDevice device, const char* name) + { + return reinterpret_cast(vkGetDeviceProcAddr(device, name)); + } + + // Single-subresource colour image layout transition. + void transitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, + VkAccessFlags2 srcAccessMask, VkAccessFlags2 dstAccessMask, VkPipelineStageFlags2 srcStageMask, + VkPipelineStageFlags2 dstStageMask) + { + VkImageMemoryBarrier2 barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + barrier.srcStageMask = srcStageMask; + barrier.dstStageMask = dstStageMask; + barrier.oldLayout = oldLayout; + barrier.newLayout = newLayout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = srcAccessMask; + barrier.dstAccessMask = dstAccessMask; + + VkDependencyInfo dependency = {}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &barrier; + + vkCmdPipelineBarrier2(commandBuffer, &dependency); + } + + VkSemaphoreSubmitInfo semaphoreSubmit(VkSemaphore semaphore, VkPipelineStageFlags2 stageMask) + { + VkSemaphoreSubmitInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + info.semaphore = semaphore; + info.stageMask = stageMask; + return info; + } + + // Signal semaphores use ALL_COMMANDS, matching legacy vkQueueSubmit. + VkSemaphoreSubmitInfo signalSubmit(VkSemaphore semaphore) + { + return semaphoreSubmit(semaphore, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); + } + + // One vkQueueSubmit2 batch; commandBuffer may be VK_NULL_HANDLE. + VkResult queueSubmit2(VkQueue queue, std::initializer_list waits, VkCommandBuffer commandBuffer, + std::initializer_list signalInfos, VkFence fence) + { + VkCommandBufferSubmitInfo commandBufferInfo = {}; + commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + commandBufferInfo.commandBuffer = commandBuffer; + + VkSubmitInfo2 submit = {}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + submit.waitSemaphoreInfoCount = static_cast(waits.size()); + submit.pWaitSemaphoreInfos = waits.begin(); + submit.commandBufferInfoCount = commandBuffer != VK_NULL_HANDLE ? 1 : 0; + submit.pCommandBufferInfos = &commandBufferInfo; + submit.signalSemaphoreInfoCount = static_cast(signalInfos.size()); + submit.pSignalSemaphoreInfos = signalInfos.begin(); + + return vkQueueSubmit2(queue, 1, &submit, fence); + } + + bool envFlagSet(const char* name) { return getenv(name) != nullptr; } + + constexpr std::string_view tilingName(VkImageTiling tiling) + { + switch (tiling) + { + case VK_IMAGE_TILING_OPTIMAL: + return "OPTIMAL"; + case VK_IMAGE_TILING_LINEAR: + return "LINEAR"; + default: + return "(other)"; + } + } + + constexpr std::string_view colorSpaceName(VkColorSpaceKHR colorSpace) + { + switch (colorSpace) + { + case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: + return "SRGB_NONLINEAR"; + case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: + return "EXTENDED_SRGB_LINEAR"; + case VK_COLOR_SPACE_EXTENDED_SRGB_NONLINEAR_EXT: + return "EXTENDED_SRGB_NONLINEAR"; + case VK_COLOR_SPACE_HDR10_ST2084_EXT: + return "HDR10_ST2084"; + case VK_COLOR_SPACE_HDR10_HLG_EXT: + return "HDR10_HLG"; + case VK_COLOR_SPACE_BT2020_LINEAR_EXT: + return "BT2020_LINEAR"; + case VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT: + return "DISPLAY_P3_NONLINEAR"; + case VK_COLOR_SPACE_PASS_THROUGH_EXT: + return "PASS_THROUGH"; + default: + return "(other)"; + } + } + + // nullopt when RV_VULKAN_FORCE_TILING is unset or unrecognized. + std::optional forcedTilingRequested() + { + const char* value = getenv("RV_VULKAN_FORCE_TILING"); + if (!value) + { + return std::nullopt; + } + + std::string lowered(value); + std::transform(lowered.begin(), lowered.end(), lowered.begin(), + [](unsigned char ch) { return static_cast(::tolower(ch)); }); + + if (lowered == "optimal") + { + return VK_IMAGE_TILING_OPTIMAL; + } + if (lowered == "linear") + { + return VK_IMAGE_TILING_LINEAR; + } + + cout << "WARNING: VulkanWindow: RV_VULKAN_FORCE_TILING='" << value << "' is not recognized (expected 'optimal' or 'linear'); " + << "ignoring it and using the negotiated tiling" << endl; + return std::nullopt; + } + + bool isNvidiaPhysicalDevice(VkPhysicalDevice dev) + { + VkPhysicalDeviceProperties props = {}; + vkGetPhysicalDeviceProperties(dev, &props); + return props.vendorID == 0x10DE; + } + + bool nvidiaInteropWorkaroundDisabled() + { + static const bool disabled = getenv("RV_VULKAN_DISABLE_NVIDIA_INTEROP_WORKAROUND") != nullptr; + return disabled; + } + + bool dedicatedAllocationDisabled() + { + static const bool disabled = + getenv("RV_VULKAN_FORCE_NO_DEDICATED") != nullptr || getenv("RV_VULKAN_DISABLE_DEDICATED_ALLOCATION") != nullptr; + return disabled; + } + + // Frames the control viewport may keep in flight. Default 1: depth is + // input latency, and the loop is GPU-bound so a second frame buys no + // throughput. RV_VULKAN_MAX_FRAMES_IN_FLIGHT=2 restores the deeper ring. + unsigned int maxFramesInFlight() + { + static const unsigned int depth = [] + { + const unsigned int kDefault = 1; + const char* value = getenv("RV_VULKAN_MAX_FRAMES_IN_FLIGHT"); + if (!value) + { + return kDefault; + } + const int requested = atoi(value); + if (requested < 1 || requested > static_cast(VulkanWindow::kFramesInFlight)) + { + cout << "WARNING: VulkanWindow: RV_VULKAN_MAX_FRAMES_IN_FLIGHT must be 1.." << VulkanWindow::kFramesInFlight + << "; using " << kDefault << endl; + return kDefault; + } + return static_cast(requested); + }(); + return depth; + } + + // Vendor preference: OPTIMAL on NVIDIA, LINEAR elsewhere. NVIDIA 550+ + // returns blank pixels to GL for LINEAR shared images >= ~2 MiB + // (forum thread #349436); its GL and Vulkan share one driver, so the + // optimal layout matches on import. Under Mesa, GL and Vulkan are + // different drivers and OPTIMAL renders tile garbage (RADV PHOENIX2) + // even when reported exportable; making it usable there needs + // VK_EXT_image_drm_format_modifier. + bool useOptimalTilingForInterop(VkPhysicalDevice dev) { return !nvidiaInteropWorkaroundDisabled() && isNvidiaPhysicalDevice(dev); } + } // namespace + + void VulkanWindow::negotiateInteropConfig() + { + if (m_interopNegotiated) + { + return; + } + m_interopNegotiated = true; + + InteropConfig cfg; + cfg.format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; // == GL_RGB10_A2 + // Must cover GL's use too (it renders into the image): with only + // TRANSFER_SRC the driver may pick a compressed layout GL cannot read. + cfg.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + +#ifdef PLATFORM_WINDOWS + const VkExternalMemoryHandleTypeFlagBits handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + const VkExternalMemoryHandleTypeFlagBits handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; +#endif + + // Exportability alone does not guarantee a usable import, so try the + // vendor's preferred tiling first. See useOptimalTilingForInterop(). + const bool preferOptimal = useOptimalTilingForInterop(m_vkPhysicalDevice); + const std::array candidates = {preferOptimal ? VK_IMAGE_TILING_OPTIMAL : VK_IMAGE_TILING_LINEAR, + preferOptimal ? VK_IMAGE_TILING_LINEAR : VK_IMAGE_TILING_OPTIMAL}; + + auto probeTiling = [&](VkImageTiling tiling, VkExternalMemoryFeatureFlags& features) -> bool + { + VkPhysicalDeviceExternalImageFormatInfo extInfo = {}; + extInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO; + extInfo.handleType = handleType; + + VkPhysicalDeviceImageFormatInfo2 fmtInfo = {}; + fmtInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; + fmtInfo.pNext = &extInfo; + fmtInfo.format = cfg.format; + fmtInfo.type = VK_IMAGE_TYPE_2D; + fmtInfo.tiling = tiling; + fmtInfo.usage = cfg.usage; + fmtInfo.flags = 0; + + VkExternalImageFormatProperties extProps = {}; + extProps.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES; + + VkImageFormatProperties2 props = {}; + props.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; + props.pNext = &extProps; + + const VkResult result = vkGetPhysicalDeviceImageFormatProperties2(m_vkPhysicalDevice, &fmtInfo, &props); + features = extProps.externalMemoryProperties.externalMemoryFeatures; + + if (result != VK_SUCCESS) + { + return false; + } + + return (extProps.externalMemoryProperties.compatibleHandleTypes & handleType) != 0 + && (features & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0 + && (features & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0; + }; + + bool found = false; + for (VkImageTiling tiling : candidates) + { + VkExternalMemoryFeatureFlags features = 0; + const bool exportable = probeTiling(tiling, features); + + ostringstream entry; + entry << tilingName(tiling) << ": "; + + if (!exportable) + { + entry << "not exportable+importable for this handle type" + << " (features=0x" << std::hex << features << std::dec << ")"; + cfg.candidateLog.push_back(entry.str()); + continue; + } + + // Only the floor; the per-image decision is in getSharedImageInfo(). + const bool dedicatedOnly = (features & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; + + cfg.supported = true; + cfg.tiling = tiling; + cfg.externalFeatures = features; + cfg.dedicatedAllocation = dedicatedOnly; + cfg.probedTiling = tiling; + cfg.probedDedicated = dedicatedOnly; + + entry << "exportable (features=0x" << std::hex << features << std::dec << ") -- selected"; + cfg.candidateLog.push_back(entry.str()); + found = true; + break; + } + + if (!found) + { + cfg.supported = false; + cfg.rejectReason = "no candidate tiling is exportable at A2B10G10R10 with " + "COLOR_ATTACHMENT|TRANSFER_SRC usage"; + m_interopConfig = cfg; + return; + } + + // Overrides last, so the record reports both values. + const std::optional forcedTiling = forcedTilingRequested(); + if (forcedTiling && *forcedTiling != cfg.tiling) + { + VkExternalMemoryFeatureFlags features = 0; + if (probeTiling(*forcedTiling, features)) + { + cfg.tilingOverridden = true; + cfg.tiling = *forcedTiling; + cfg.externalFeatures = features; + cfg.dedicatedAllocation = (features & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; + } + else + { + cout << "WARNING: VulkanWindow: RV_VULKAN_FORCE_TILING=" << tilingName(*forcedTiling) + << " refused -- the driver does not report it as exportable; using the negotiated " << tilingName(cfg.tiling) << endl; + } + } + + if (dedicatedAllocationDisabled() && cfg.dedicatedAllocation) + { + if ((cfg.externalFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0) + { + cout << "WARNING: VulkanWindow: RV_VULKAN_FORCE_NO_DEDICATED refused -- the driver reports " + << "DEDICATED_ONLY for this configuration, which is a requirement rather than a preference" << endl; + } + else + { + cfg.dedicatedOverridden = true; + cfg.dedicatedAllocation = false; + } + } + else if (dedicatedAllocationDisabled()) + { + // getSharedImageInfo() still needs to know the override is active. + cfg.dedicatedOverridden = true; + } + + m_interopConfig = cfg; + } + + void VulkanWindow::reportPresentPath(PresentPath path, const std::string& reason) + { + m_presentPath = path; + m_presentPathReason = reason; + emitPresentationRecord(); + } + + void VulkanWindow::reportGLImportState(VkImageTiling tiling, bool dedicated) + { + m_glImportTiling = tiling; + m_glImportDedicated = dedicated; + m_glImportReported = true; + } + + void VulkanWindow::emitPresentationRecord() + { + if (m_recordEmitted) + { + return; + } + m_recordEmitted = true; + + VkPhysicalDeviceProperties props = {}; + if (m_vkPhysicalDevice != VK_NULL_HANDLE) + { + vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &props); + } + + std::string_view pathName = "undetermined"; + switch (m_presentPath) + { + case PresentPath::ZeroCopy: + pathName = "GPU zero-copy interop (10-bit)"; + break; + case PresentPath::CpuReadback: + pathName = "CPU readback (10-bit, slower)"; + break; + case PresentPath::OpenGL: + pathName = "OpenGL (Vulkan abandoned; not 10-bit)"; + break; + case PresentPath::Undetermined: + break; + } + + ostringstream report; + report << "INFO: RV Vulkan presentation report\n"; + report << "INFO: Role : " << (isPassiveOutput() ? "presentation output" : "control viewport") << "\n"; + report << "INFO: GPU : " << (m_vkPhysicalDevice != VK_NULL_HANDLE ? props.deviceName : "(none)") << " vendorID=0x" + << std::hex << props.vendorID << std::dec << " driverVersion=" << props.driverVersion + << " apiVersion=" << VK_VERSION_MAJOR(props.apiVersion) << "." << VK_VERSION_MINOR(props.apiVersion) << "." + << VK_VERSION_PATCH(props.apiVersion) << "\n"; + report << "INFO: Present path : " << pathName << "\n"; + if (!m_presentPathReason.empty()) + { + report << "INFO: Reason : " << m_presentPathReason << "\n"; + } + report << "INFO: Swapchain : " << formatName(m_vkSwapchainFormat) << " / " + << colorSpaceName(m_loggedSurfaceFormat.colorSpace) << "\n"; + + const InteropConfig& config = m_interopConfig; + if (config.supported) + { + report << "INFO: Shared image : " << formatName(config.format) << " tiling=" << tilingName(config.tiling) + << " usage=COLOR_ATTACHMENT|TRANSFER_SRC\n"; + report << "INFO: Dedicated alloc: " << (config.dedicatedAllocation ? "yes" : "no") << " (driver externalMemoryFeatures=0x" + << std::hex << config.externalFeatures << std::dec + << (config.externalFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT ? " DEDICATED_ONLY" : "") << ")\n"; + if (config.tilingOverridden) + { + report << "INFO: Tiling override: RV_VULKAN_FORCE_TILING forced " << tilingName(config.tiling) << "; negotiation chose " + << tilingName(config.probedTiling) << "\n"; + } + if (config.dedicatedOverridden) + { + report << "INFO: Dedicated ovr : RV_VULKAN_FORCE_NO_DEDICATED suppressed dedicated allocation; negotiation chose " + << (config.probedDedicated ? "yes" : "no") << "\n"; + } + if (m_glImportReported) + { + const bool agree = m_glImportTiling == config.tiling; + report << "INFO: GL import : tiling=" << tilingName(m_glImportTiling) + << " dedicated=" << (m_glImportDedicated ? "yes" : "no") << " -- " + << (agree ? "tiling matches the Vulkan export" + : "TILING DISAGREES WITH THE VULKAN EXPORT (expect a corrupted image)") + << "\n"; + } + } + else + { + report << "INFO: Shared image : not used -- " + << (config.rejectReason.empty() ? "interop not negotiated" : config.rejectReason) << "\n"; + } + + for (const std::string& entry : config.candidateLog) + { + report << "INFO: Probe candidate: " << entry << "\n"; + } + + if (envFlagSet("RV_VULKAN_FORCE_CPU_PRESENT")) + { + report << "INFO: Override : RV_VULKAN_FORCE_CPU_PRESENT is set\n"; + } + + cout << report.str() << flush; + } + + void VulkanWindow::cleanupSharedImage(uint32_t slot) + { + SharedImageInfo& info = m_shared[slot].info; + + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + if (m_shared[slot].image) + { + vkDestroyImage(m_vkDevice, m_shared[slot].image, nullptr); + m_shared[slot].image = VK_NULL_HANDLE; + } + if (m_shared[slot].memory) + { + vkFreeMemory(m_vkDevice, m_shared[slot].memory, nullptr); + m_shared[slot].memory = VK_NULL_HANDLE; + } + if (m_shared[slot].glReadySemaphore) + { + vkDestroySemaphore(m_vkDevice, m_shared[slot].glReadySemaphore, nullptr); + m_shared[slot].glReadySemaphore = VK_NULL_HANDLE; + } + if (m_shared[slot].vkReadySemaphore) + { + vkDestroySemaphore(m_vkDevice, m_shared[slot].vkReadySemaphore, nullptr); + m_shared[slot].vkReadySemaphore = VK_NULL_HANDLE; + } + } + +#ifdef PLATFORM_WINDOWS + if (info.memoryHandle) + { + ::CloseHandle(static_cast(info.memoryHandle)); + info.memoryHandle = nullptr; + } + if (info.glReadySemaphoreHandle) + { + ::CloseHandle(static_cast(info.glReadySemaphoreHandle)); + info.glReadySemaphoreHandle = nullptr; + } + if (info.vkReadySemaphoreHandle) + { + ::CloseHandle(static_cast(info.vkReadySemaphoreHandle)); + info.vkReadySemaphoreHandle = nullptr; + } +#else + if (info.memoryFd != -1) + { + ::close(info.memoryFd); + info.memoryFd = -1; + } + if (info.glReadySemaphoreFd != -1) + { + ::close(info.glReadySemaphoreFd); + info.glReadySemaphoreFd = -1; + } + if (info.vkReadySemaphoreFd != -1) + { + ::close(info.vkReadySemaphoreFd); + info.vkReadySemaphoreFd = -1; + } +#endif + info.width = 0; + info.height = 0; + info.size = 0; + info.capacityHeight = 0; + info.tiling = VK_IMAGE_TILING_LINEAR; + info.dedicatedAllocation = false; + m_shared[slot].capacityW = 0; + m_shared[slot].capacityH = 0; + } + + // + // Skip frames under load (as Qt does for the OpenGL output) instead of + // deepening the GPU queue the control viewport then waits on. Gates on + // this device's own GPU work; the swapchain queue always has room. + // + bool VulkanWindow::canPresentNow() + { + if (!isPassiveOutput()) + { + return true; + } + + if (!m_vkDevice || !m_vkSwapchain) + { + return true; + } + + // Forward progress: under sustained load, force one blocking present + // rather than freezing the output. + if (!m_lastPresentTimer.isRunning() || m_lastPresentTimer.elapsed() > kMaxStaleSeconds) + { + return true; + } + + // Every in-flight frame must have retired, not only this slot's. + std::array fences{}; + for (uint32_t i = 0; i < kFramesInFlight; ++i) + { + fences[i] = m_frameSync[i].fence; + } + + const VkResult result = vkWaitForFences(m_vkDevice, kFramesInFlight, fences.data(), VK_TRUE, 0); + if (result == VK_SUCCESS) + { + return true; + } + + requestBestEffortRetry(); + return false; + } + + // Re-present a skipped best-effort frame; requestUpdate() coalesces, so at + // most one retry is pending. + void VulkanWindow::requestBestEffortRetry() + { + if (!m_stopProcessingEvents && isExposed()) + { + requestUpdate(); + } + } + + void VulkanWindow::drainSharedSemaphores(uint32_t slot) + { + if (!m_vkDevice || !m_shared[slot].glReadySemaphore || !m_shared[slot].vkReadySemaphore) + { + return; + } + + const VkResult result = + queueSubmit2(m_vkQueue, {semaphoreSubmit(m_shared[slot].glReadySemaphore, VK_PIPELINE_STAGE_2_TRANSFER_BIT)}, VK_NULL_HANDLE, + {signalSubmit(m_shared[slot].vkReadySemaphore)}, VK_NULL_HANDLE); + if (result == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + } + + void VulkanWindow::recoverFailedSubmit(uint32_t slot, VkSemaphore waitSemaphore) + { + // Consume the wait semaphore (if any) and signal the fence. + const VkResult result = waitSemaphore != VK_NULL_HANDLE + ? queueSubmit2(m_vkQueue, {semaphoreSubmit(waitSemaphore, VK_PIPELINE_STAGE_2_TRANSFER_BIT)}, + VK_NULL_HANDLE, {}, m_frameSync[slot].fence) + : queueSubmit2(m_vkQueue, {}, VK_NULL_HANDLE, {}, m_frameSync[slot].fence); + if (result != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: could not recover from a failed submit" << endl; + } + + // An unexpected submit failure means Vulkan is unreliable here. + requestGLFallback(); + } + + // Compare against the surface extent, not the requested size: + // createSwapchain() can only match the surface, so a mismatched request + // would recreate the swapchain every frame. + bool VulkanWindow::ensureSwapchainMatchesSurface() + { + VkExtent2D surfaceExtent = m_vkSwapchainExtent; + VkSurfaceCapabilitiesKHR caps = {}; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_vkPhysicalDevice, m_vkSurface, &caps) == VK_SUCCESS + && caps.currentExtent.width != std::numeric_limits::max()) + { + surfaceExtent = caps.currentExtent; + } + + if (!m_vkSwapchain || m_vkSwapchainExtent.width != surfaceExtent.width || m_vkSwapchainExtent.height != surfaceExtent.height) + { + return createSwapchain(); + } + return true; + } + + const VulkanWindow::SharedImageInfo* VulkanWindow::getSharedImageInfo(int w, int h) + { + if (!m_vkDevice || !m_externalInteropSupported || m_sharedImageUnusable) + { + return nullptr; + } + + if (!m_interopConfig.supported) + { + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: getSharedImageInfo: interop unavailable (" << m_interopConfig.rejectReason + << "); using the CPU readback path" << endl; + } + return nullptr; + } + + const uint32_t slot = m_currentFrame; + SharedImageInfo& info = m_shared[slot].info; + + if (!ensureSwapchainMatchesSurface()) + { + return nullptr; + } + + // Within capacity: reuse the export, update the used sub-region. + if (m_shared[slot].image && w <= m_shared[slot].capacityW && h <= m_shared[slot].capacityH) + { + info.width = w; + info.height = h; + return &info; + } + + // Grow to at least this window's screen (not the primary: an output + // lives on a second display) so drag-to-fullscreen allocates once. + int screenW = 0; + int screenH = 0; + if (QScreen* scr = screen() ? screen() : QGuiApplication::primaryScreen()) + { + const qreal dpr = scr->devicePixelRatio(); + screenW = static_cast(scr->geometry().width() * dpr); + screenH = static_cast(scr->geometry().height() * dpr); + } + const int capW = std::max({w, screenW, m_shared[slot].capacityW}); + const int capH = std::max({h, screenH, m_shared[slot].capacityH}); + + const bool optimalTiling = m_interopConfig.tiling == VK_IMAGE_TILING_OPTIMAL; + + // A format mismatch needs a blit; without blit support, use the CPU + // fallback. + if (m_vkSwapchainFormat != VK_FORMAT_A2B10G10R10_UNORM_PACK32) + { + VkFormatProperties srcProps = {}; + VkFormatProperties dstProps = {}; + vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, VK_FORMAT_A2B10G10R10_UNORM_PACK32, &srcProps); + vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, m_vkSwapchainFormat, &dstProps); + const VkFormatFeatureFlags srcFeatures = optimalTiling ? srcProps.optimalTilingFeatures : srcProps.linearTilingFeatures; + const bool blitOk = + (srcFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) && (dstProps.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT); + if (!blitOk) + { + if (ImageRenderer::debugGpu()) + { + cout << "INFO: VulkanWindow: GPU interop unavailable for " << formatName(m_vkSwapchainFormat) + << " swapchain (blit unsupported); using CPU fallback." << endl; + } + m_sharedImageUnusable = true; + return nullptr; + } + } + + cleanupSharedImage(slot); + + // Unconditional: fires about once per slot per session. + cout << "INFO: VulkanWindow: getSharedImageInfo: (re)allocating shared image slot " << slot << " capacity " << capW << "x" << capH + << " for request " << w << "x" << h << " tiling=" << tilingName(m_interopConfig.tiling) << endl; + + VkExternalMemoryImageCreateInfo extMemInfo = {}; + extMemInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; +#ifdef PLATFORM_WINDOWS + extMemInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + extMemInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; +#endif + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.pNext = &extMemInfo; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + // Matches GL_RGB10_A2 whatever the swapchain format; an A2R10G10B10 + // swapchain is reconciled by a blit in presentSharedImage(). + imageInfo.format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + imageInfo.extent = {static_cast(capW), static_cast(capH), 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = m_interopConfig.tiling; + imageInfo.usage = m_interopConfig.usage; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + if (vkCreateImage(m_vkDevice, &imageInfo, nullptr, &m_shared[slot].image) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to create shared image" << endl; + return nullptr; + } + + // vkGetImageSubresourceLayout is only valid for LINEAR tiling. + if (optimalTiling) + { + info.strideWidth = capW; + } + else + { + // Match the GL texture stride to a padded rowPitch. + VkImageSubresource subresource = {}; + subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + subresource.mipLevel = 0; + subresource.arrayLayer = 0; + VkSubresourceLayout layout; + vkGetImageSubresourceLayout(m_vkDevice, m_shared[slot].image, &subresource, &layout); + + if (layout.rowPitch % 4 != 0) + { + // Not an integer pixel width; use the CPU fallback. + cleanupSharedImage(slot); + m_sharedImageUnusable = true; + return nullptr; + } + info.strideWidth = static_cast(layout.rowPitch / 4); + } + info.capacityHeight = capH; + info.tiling = m_interopConfig.tiling; + + // Some drivers (AMD on Windows) require dedicated memory for external + // images; binding non-dedicated memory silently yields an all-zero GL + // texture. + VkMemoryDedicatedRequirements dedicatedReqs = {}; + dedicatedReqs.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS; + + VkMemoryRequirements2 memReqs2 = {}; + memReqs2.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2; + memReqs2.pNext = &dedicatedReqs; + + VkImageMemoryRequirementsInfo2 memReqsInfo = {}; + memReqsInfo.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2; + memReqsInfo.image = m_shared[slot].image; + + vkGetImageMemoryRequirements2(m_vkDevice, &memReqsInfo, &memReqs2); + + const VkMemoryRequirements& memReqs = memReqs2.memoryRequirements; + + // The single decision GL mirrors. An override may relax a preference, + // never a requirement. + bool useDedicated = + m_interopConfig.dedicatedAllocation || dedicatedReqs.requiresDedicatedAllocation || dedicatedReqs.prefersDedicatedAllocation; + if (dedicatedAllocationDisabled() && !dedicatedReqs.requiresDedicatedAllocation + && (m_interopConfig.externalFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) == 0) + { + useDedicated = false; + } + + info.dedicatedAllocation = useDedicated; + + cout << "INFO: VulkanWindow: getSharedImageInfo: shared image slot " << slot + << " memory = " << (useDedicated ? "dedicated" : "non-dedicated") + << " (driver requires=" << (dedicatedReqs.requiresDedicatedAllocation ? "yes" : "no") + << " prefers=" << (dedicatedReqs.prefersDedicatedAllocation ? "yes" : "no") << ")" << endl; + + // pNext chain, built back to front: + // allocInfo -> exportAllocInfo [-> dedicatedAllocInfo] [-> exportWin32Info] + void* chain = nullptr; + +#ifdef PLATFORM_WINDOWS + // Required by the spec for OPAQUE_WIN32 handles. + VkExportMemoryWin32HandleInfoKHR exportWin32Info = {}; + exportWin32Info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_WIN32_HANDLE_INFO_KHR; + exportWin32Info.pNext = chain; + exportWin32Info.pAttributes = nullptr; // default security attributes + exportWin32Info.dwAccess = GENERIC_ALL; + exportWin32Info.name = nullptr; // unnamed: shared within this process only + chain = &exportWin32Info; +#endif + + VkMemoryDedicatedAllocateInfo dedicatedAllocInfo = {}; + if (useDedicated) + { + dedicatedAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + dedicatedAllocInfo.pNext = chain; + dedicatedAllocInfo.image = m_shared[slot].image; + dedicatedAllocInfo.buffer = VK_NULL_HANDLE; + chain = &dedicatedAllocInfo; + } + + VkExportMemoryAllocateInfo exportAllocInfo = {}; + exportAllocInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + exportAllocInfo.pNext = chain; +#ifdef PLATFORM_WINDOWS + exportAllocInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + exportAllocInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; +#endif + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.pNext = &exportAllocInfo; + allocInfo.allocationSize = memReqs.size; + const std::optional memoryTypeIndex = + findMemoryType(m_vkPhysicalDevice, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (!memoryTypeIndex) + { + cerr << "ERROR: VulkanWindow: No device-local memory type for shared image" << endl; + cleanupSharedImage(slot); + return nullptr; + } + allocInfo.memoryTypeIndex = *memoryTypeIndex; + + if (vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_shared[slot].memory) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to allocate shared image memory" << endl; + cleanupSharedImage(slot); + return nullptr; + } + + if (vkBindImageMemory(m_vkDevice, m_shared[slot].image, m_shared[slot].memory, 0) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to bind shared image memory" << endl; + cleanupSharedImage(slot); + return nullptr; + } + +#ifdef PLATFORM_WINDOWS + auto pfnGetMemoryWin32HandleKHR = deviceProc(m_vkDevice, "vkGetMemoryWin32HandleKHR"); + if (!pfnGetMemoryWin32HandleKHR) + { + cerr << "ERROR: VulkanWindow: vkGetMemoryWin32HandleKHR not found" << endl; + cleanupSharedImage(slot); + return nullptr; + } + + VkMemoryGetWin32HandleInfoKHR getHandleInfo = {}; + getHandleInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_WIN32_HANDLE_INFO_KHR; + getHandleInfo.memory = m_shared[slot].memory; + getHandleInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT; + + HANDLE memHandle = nullptr; + if (pfnGetMemoryWin32HandleKHR(m_vkDevice, &getHandleInfo, &memHandle) != VK_SUCCESS || !memHandle) + { + cerr << "ERROR: VulkanWindow: Failed to get memory HANDLE" << endl; + cleanupSharedImage(slot); + return nullptr; + } + // Stored at once so cleanupSharedImage() closes it on a later failure. + info.memoryHandle = memHandle; +#else + auto pfnGetMemoryFdKHR = deviceProc(m_vkDevice, "vkGetMemoryFdKHR"); + if (!pfnGetMemoryFdKHR) + { + cerr << "ERROR: VulkanWindow: vkGetMemoryFdKHR not found" << endl; + cleanupSharedImage(slot); + return nullptr; + } + + VkMemoryGetFdInfoKHR getFdInfo = {}; + getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + getFdInfo.memory = m_shared[slot].memory; + getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + int memFd = -1; + if (pfnGetMemoryFdKHR(m_vkDevice, &getFdInfo, &memFd) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to get memory FD" << endl; + cleanupSharedImage(slot); + return nullptr; + } + // Stored at once so cleanupSharedImage() closes it on a later failure. + info.memoryFd = memFd; +#endif + + VkExportSemaphoreCreateInfo exportSemInfo = {}; + exportSemInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; +#ifdef PLATFORM_WINDOWS + exportSemInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + exportSemInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; +#endif + + VkSemaphoreCreateInfo semInfo = {}; + semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semInfo.pNext = &exportSemInfo; + + if (vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_shared[slot].glReadySemaphore) != VK_SUCCESS + || vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_shared[slot].vkReadySemaphore) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to create shared semaphores" << endl; + cleanupSharedImage(slot); + return nullptr; + } + +#ifdef PLATFORM_WINDOWS + auto pfnGetSemaphoreWin32HandleKHR = deviceProc(m_vkDevice, "vkGetSemaphoreWin32HandleKHR"); + if (!pfnGetSemaphoreWin32HandleKHR) + { + cerr << "ERROR: VulkanWindow: vkGetSemaphoreWin32HandleKHR not found" << endl; + cleanupSharedImage(slot); + return nullptr; + } + + VkSemaphoreGetWin32HandleInfoKHR getSemHandleInfo = {}; + getSemHandleInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR; + getSemHandleInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT; + + HANDLE glReadyHandle = nullptr; + HANDLE vkReadyHandle = nullptr; + + getSemHandleInfo.semaphore = m_shared[slot].glReadySemaphore; + if (pfnGetSemaphoreWin32HandleKHR(m_vkDevice, &getSemHandleInfo, &glReadyHandle) != VK_SUCCESS || !glReadyHandle) + { + cerr << "ERROR: VulkanWindow: Failed to get glReady semaphore HANDLE" << endl; + cleanupSharedImage(slot); + return nullptr; + } + info.glReadySemaphoreHandle = glReadyHandle; + + getSemHandleInfo.semaphore = m_shared[slot].vkReadySemaphore; + if (pfnGetSemaphoreWin32HandleKHR(m_vkDevice, &getSemHandleInfo, &vkReadyHandle) != VK_SUCCESS || !vkReadyHandle) + { + cerr << "ERROR: VulkanWindow: Failed to get vkReady semaphore HANDLE" << endl; + cleanupSharedImage(slot); + return nullptr; + } + info.vkReadySemaphoreHandle = vkReadyHandle; + + info.size = memReqs.size; + info.width = w; + info.height = h; +#else + auto pfnGetSemaphoreFdKHR = deviceProc(m_vkDevice, "vkGetSemaphoreFdKHR"); + if (!pfnGetSemaphoreFdKHR) + { + cerr << "ERROR: VulkanWindow: vkGetSemaphoreFdKHR not found" << endl; + cleanupSharedImage(slot); + return nullptr; + } + + VkSemaphoreGetFdInfoKHR getSemFdInfo = {}; + getSemFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; + getSemFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + + int glReadyFd = -1; + int vkReadyFd = -1; + + getSemFdInfo.semaphore = m_shared[slot].glReadySemaphore; + if (pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &glReadyFd) != VK_SUCCESS || glReadyFd < 0) + { + cerr << "ERROR: VulkanWindow: Failed to get glReady semaphore FD" << endl; + cleanupSharedImage(slot); + return nullptr; + } + info.glReadySemaphoreFd = glReadyFd; + + getSemFdInfo.semaphore = m_shared[slot].vkReadySemaphore; + if (pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &vkReadyFd) != VK_SUCCESS || vkReadyFd < 0) + { + cerr << "ERROR: VulkanWindow: Failed to get vkReady semaphore FD" << endl; + cleanupSharedImage(slot); + return nullptr; + } + info.vkReadySemaphoreFd = vkReadyFd; + + info.size = memReqs.size; + info.width = w; + info.height = h; +#endif + + VkCommandBuffer cb = m_vkCommandBuffers[0]; + vkResetCommandBuffer(cb, 0); + + VkCommandBufferBeginInfo beginInfo = {}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &beginInfo); + + transitionImageLayout(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_2_NONE, + VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_NONE, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + + vkEndCommandBuffer(cb); + + vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); + const VkResult layoutSubmitResult = queueSubmit2(m_vkQueue, {}, cb, {}, m_frameSync[slot].fence); + if (layoutSubmitResult != VK_SUCCESS) + { + recoverFailedSubmit(slot, VK_NULL_HANDLE); + cleanupSharedImage(slot); + return nullptr; + } + vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); + + // Signal vkReady so GL can write the first frame. + const VkResult signalResult = + queueSubmit2(m_vkQueue, {}, VK_NULL_HANDLE, {signalSubmit(m_shared[slot].vkReadySemaphore)}, VK_NULL_HANDLE); + if (signalResult != VK_SUCCESS) + { + if (signalResult == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + cleanupSharedImage(slot); + return nullptr; + } + + m_shared[slot].capacityW = capW; + m_shared[slot].capacityH = capH; + + return &info; + } + + //-------------------------------------------------------------------------- + // presentSharedImage + //-------------------------------------------------------------------------- + + void VulkanWindow::presentSharedImage() + { + const uint32_t slot = m_currentFrame; + const SharedImageInfo& info = m_shared[slot].info; + + if (!m_vkDevice || !m_shared[slot].image || !m_vkSwapchain) + { + return; + } + + // Fence wait vs acquire timing tells GPU from vblank back-pressure. + const bool diagPresent = IPCore::ImageRenderer::debugGpu() && m_doc; + Timer diagTimer; + + // The fence wait and acquire pace the control viewport to refresh. A + // passive output polls instead, so a second display's vblank never + // enters the loop; a skipped frame keeps the previous image. + const bool bestEffort = isPassiveOutput(); + const bool forceProgress = bestEffort && (!m_lastPresentTimer.isRunning() || m_lastPresentTimer.elapsed() > kMaxStaleSeconds); + const uint64_t waitTimeout = (!bestEffort || forceProgress) ? std::numeric_limits::max() : 0; + + if (diagPresent) + { + diagTimer.start(); + } + + VkResult fenceResult = vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, waitTimeout); + + if (diagPresent) + { + s_diagFenceWaitMs += diagTimer.elapsed() * 1000.0; + } + + if (fenceResult == VK_TIMEOUT) + { + // The slot is not advanced: the next frame retries it. + drainSharedSemaphores(slot); + requestBestEffortRetry(); + return; + } + if (fenceResult != VK_SUCCESS) + { + requestGLFallback(); + return; + } + + uint32_t imageIndex; + if (diagPresent) + { + diagTimer.start(); + } + + VkResult result = + vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, waitTimeout, m_frameSync[slot].imageAvailable, VK_NULL_HANDLE, &imageIndex); + + if (diagPresent) + { + s_diagAcquireMs += diagTimer.elapsed() * 1000.0; + } + + if (result == VK_NOT_READY || result == VK_TIMEOUT) + { + // Skip here, not after a successful acquire: a failed acquire + // leaves the semaphore unsignaled. + drainSharedSemaphores(slot); + requestBestEffortRetry(); + return; + } + + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + drainSharedSemaphores(slot); + handleSwapchainOutOfDate(); + return; + } + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) + { + drainSharedSemaphores(slot); + if (result == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + return; + } + + if (m_imagesInFlight[imageIndex] != VK_NULL_HANDLE) + { + vkWaitForFences(m_vkDevice, 1, &m_imagesInFlight[imageIndex], VK_TRUE, std::numeric_limits::max()); + } + m_imagesInFlight[imageIndex] = m_frameSync[slot].fence; + + vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); + + VkCommandBuffer cb = m_vkCommandBuffers[imageIndex]; + vkResetCommandBuffer(cb, 0); + + VkCommandBufferBeginInfo beginInfo = {}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &beginInfo); + + // Source stage TRANSFER chains the transition after the glReady wait. + transitionImageLayout(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_2_NONE, VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT); + + // Transition swapchain image to transfer dst + transitionImageLayout(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_ACCESS_2_NONE, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT); + + // Same format: raw copy. A2R10G10B10: a raw copy would swap R and B, + // so blit (per-component conversion). The destination is bounded by + // the swapchain: a stale devicePixelRatio can inflate the request. + if (m_vkSwapchainFormat == VK_FORMAT_A2B10G10R10_UNORM_PACK32) + { + // vkCmdCopyImage cannot scale, so clamp to the overlapping region. + VkImageCopy region = {}; + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.srcSubresource.layerCount = 1; + region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.dstSubresource.layerCount = 1; + region.extent = {std::min(static_cast(info.width), m_vkSwapchainExtent.width), + std::min(static_cast(info.height), m_vkSwapchainExtent.height), 1}; + + vkCmdCopyImage(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_vkSwapchainImages[imageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + } + else + { + VkImageBlit blit = {}; + blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blit.srcSubresource.layerCount = 1; + blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blit.dstSubresource.layerCount = 1; + blit.srcOffsets[0] = {0, 0, 0}; + blit.srcOffsets[1] = {info.width, info.height, 1}; + blit.dstOffsets[0] = {0, 0, 0}; + blit.dstOffsets[1] = {static_cast(m_vkSwapchainExtent.width), static_cast(m_vkSwapchainExtent.height), 1}; + + vkCmdBlitImage(cb, m_shared[slot].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_vkSwapchainImages[imageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); + } + + transitionImageLayout(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_NONE); + + vkEndCommandBuffer(cb); + + // TRANSFER, not COLOR_ATTACHMENT_OUTPUT: both images are first touched + // by TRANSFER-stage transitions. + const VkResult submitResult = queueSubmit2( + m_vkQueue, + {semaphoreSubmit(m_shared[slot].glReadySemaphore, VK_PIPELINE_STAGE_2_TRANSFER_BIT), + semaphoreSubmit(m_frameSync[slot].imageAvailable, VK_PIPELINE_STAGE_2_TRANSFER_BIT)}, + cb, {signalSubmit(m_vkRenderFinished[imageIndex]), signalSubmit(m_shared[slot].vkReadySemaphore)}, m_frameSync[slot].fence); + if (submitResult != VK_SUCCESS) + { + drainSharedSemaphores(slot); + recoverFailedSubmit(slot, m_frameSync[slot].imageAvailable); + return; + } + + if (m_doc && s_diagFrameEventTime >= 0.0) + { + s_diagSlotEventTime[slot] = s_diagFrameEventTime; + s_diagSlotArmed[slot] = true; + s_diagFrameEventTime = -1.0; + } + + m_currentFrame = (m_currentFrame + 1) % kFramesInFlight; + + VkPresentInfoKHR presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &m_vkRenderFinished[imageIndex]; + const std::array swapchains = {m_vkSwapchain}; + presentInfo.swapchainCount = static_cast(swapchains.size()); + presentInfo.pSwapchains = swapchains.data(); + presentInfo.pImageIndices = &imageIndex; + + VkResult presentResult = vkQueuePresentKHR(m_vkQueue, &presentInfo); + + m_lastPresentTimer.stop(); + m_lastPresentTimer.start(); + + // See maxFramesInFlight(). After the present so the driver gets the + // frame early; never for the passive output. + if (maxFramesInFlight() == 1 && !isPassiveOutput()) + { + vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); + } + // Recreate only on OUT_OF_DATE: some X11/RADV compositors report + // SUBOPTIMAL persistently. + if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) + { + handleSwapchainOutOfDate(); + return; + } + if (presentResult != VK_SUCCESS && presentResult != VK_SUBOPTIMAL_KHR) + { + if (presentResult == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + return; + } + } + + //-------------------------------------------------------------------------- + // presentPixelData + //-------------------------------------------------------------------------- + + void VulkanWindow::presentPixelData(const void* pixels, int w, int h) + { + const uint32_t slot = m_currentFrame; + + if (!m_vkDevice) + { + return; + } + + const bool diagPresent = IPCore::ImageRenderer::debugGpu() && m_doc; + Timer diagTimer; + + if (w <= 0 || h <= 0 || !ensureSwapchainMatchesSurface()) + { + return; + } + + // Same best-effort throttle as presentSharedImage(). + const bool bestEffort = isPassiveOutput(); + const bool forceProgress = bestEffort && (!m_lastPresentTimer.isRunning() || m_lastPresentTimer.elapsed() > kMaxStaleSeconds); + const uint64_t waitTimeout = (!bestEffort || forceProgress) ? std::numeric_limits::max() : 0; + + if (diagPresent) + { + diagTimer.start(); + } + const VkResult fenceResult = vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, waitTimeout); + if (diagPresent) + { + s_diagFenceWaitMs += diagTimer.elapsed() * 1000.0; + } + + if (fenceResult == VK_TIMEOUT) + { + requestBestEffortRetry(); + return; + } + if (fenceResult != VK_SUCCESS) + { + requestGLFallback(); + return; + } + + const size_t size = static_cast(w) * static_cast(h) * 4; + + if (size > m_staging[slot].size) + { + if (m_staging[slot].buffer) + { + vkDestroyBuffer(m_vkDevice, m_staging[slot].buffer, nullptr); + m_staging[slot].buffer = VK_NULL_HANDLE; + } + if (m_staging[slot].memory) + { + vkFreeMemory(m_vkDevice, m_staging[slot].memory, nullptr); + m_staging[slot].memory = VK_NULL_HANDLE; + } + m_staging[slot].size = 0; + + VkBufferCreateInfo bufferInfo = {}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(m_vkDevice, &bufferInfo, nullptr, &m_staging[slot].buffer) != VK_SUCCESS) + { + m_staging[slot].buffer = VK_NULL_HANDLE; + cerr << "ERROR: VulkanWindow: Failed to create staging buffer" << endl; + return; + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(m_vkDevice, m_staging[slot].buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + const std::optional memoryTypeIndex = + findMemoryType(m_vkPhysicalDevice, memRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (!memoryTypeIndex) + { + cerr << "ERROR: VulkanWindow: No host-visible memory type for staging buffer" << endl; + return; + } + allocInfo.memoryTypeIndex = *memoryTypeIndex; + + if (vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_staging[slot].memory) != VK_SUCCESS) + { + m_staging[slot].memory = VK_NULL_HANDLE; + cerr << "ERROR: VulkanWindow: Failed to allocate staging buffer memory" << endl; + return; + } + if (vkBindBufferMemory(m_vkDevice, m_staging[slot].buffer, m_staging[slot].memory, 0) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to bind staging buffer memory" << endl; + return; + } + + m_staging[slot].size = size; + } + + void* data = nullptr; + if (vkMapMemory(m_vkDevice, m_staging[slot].memory, 0, size, 0, &data) != VK_SUCCESS) + { + cerr << "ERROR: VulkanWindow: Failed to map staging buffer memory" << endl; + return; + } + memcpy(data, pixels, size); + vkUnmapMemory(m_vkDevice, m_staging[slot].memory); + + uint32_t imageIndex; + if (diagPresent) + { + diagTimer.start(); + } + VkResult result = + vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, waitTimeout, m_frameSync[slot].imageAvailable, VK_NULL_HANDLE, &imageIndex); + if (diagPresent) + { + s_diagAcquireMs += diagTimer.elapsed() * 1000.0; + } + if (result == VK_NOT_READY || result == VK_TIMEOUT) + { + requestBestEffortRetry(); + return; + } + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + handleSwapchainOutOfDate(); + return; + } + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) + { + if (result == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + return; + } + + if (m_imagesInFlight[imageIndex] != VK_NULL_HANDLE) + { + vkWaitForFences(m_vkDevice, 1, &m_imagesInFlight[imageIndex], VK_TRUE, std::numeric_limits::max()); + } + m_imagesInFlight[imageIndex] = m_frameSync[slot].fence; + + vkResetFences(m_vkDevice, 1, &m_frameSync[slot].fence); + + VkCommandBuffer cb = m_vkCommandBuffers[imageIndex]; + vkResetCommandBuffer(cb, 0); + + VkCommandBufferBeginInfo beginInfo = {}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &beginInfo); + + transitionImageLayout(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_ACCESS_2_NONE, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT); + + // The buffer is w x h, but the destination is bounded by the swapchain. + VkBufferImageCopy region = {}; + region.bufferOffset = 0; + region.bufferRowLength = static_cast(w); + region.bufferImageHeight = static_cast(h); + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = {0, 0, 0}; + region.imageExtent = {std::min(static_cast(w), m_vkSwapchainExtent.width), + std::min(static_cast(h), m_vkSwapchainExtent.height), 1}; + + vkCmdCopyBufferToImage(cb, m_staging[slot].buffer, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, + ®ion); + + transitionImageLayout(cb, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_NONE); + + vkEndCommandBuffer(cb); + + const VkResult submitResult = + queueSubmit2(m_vkQueue, {semaphoreSubmit(m_frameSync[slot].imageAvailable, VK_PIPELINE_STAGE_2_TRANSFER_BIT)}, cb, + {signalSubmit(m_vkRenderFinished[imageIndex])}, m_frameSync[slot].fence); + if (submitResult != VK_SUCCESS) + { + recoverFailedSubmit(slot, m_frameSync[slot].imageAvailable); + return; + } + + if (m_doc && s_diagFrameEventTime >= 0.0) + { + s_diagSlotEventTime[slot] = s_diagFrameEventTime; + s_diagSlotArmed[slot] = true; + s_diagFrameEventTime = -1.0; + } + + m_currentFrame = (m_currentFrame + 1) % kFramesInFlight; + + VkPresentInfoKHR presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &m_vkRenderFinished[imageIndex]; + const std::array swapchains = {m_vkSwapchain}; + presentInfo.swapchainCount = static_cast(swapchains.size()); + presentInfo.pSwapchains = swapchains.data(); + presentInfo.pImageIndices = &imageIndex; + + VkResult presentResult = vkQueuePresentKHR(m_vkQueue, &presentInfo); + + m_lastPresentTimer.stop(); + m_lastPresentTimer.start(); + + // See maxFramesInFlight(). After the present so the driver gets the + // frame early; never for the passive output. + if (maxFramesInFlight() == 1 && !isPassiveOutput()) + { + vkWaitForFences(m_vkDevice, 1, &m_frameSync[slot].fence, VK_TRUE, std::numeric_limits::max()); + } + // See presentSharedImage() on SUBOPTIMAL. + if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) + { + handleSwapchainOutOfDate(); + return; + } + if (presentResult != VK_SUCCESS && presentResult != VK_SUBOPTIMAL_KHR) + { + if (presentResult == VK_ERROR_DEVICE_LOST) + { + requestGLFallback(); + } + return; + } + } + + //-------------------------------------------------------------------------- + + void VulkanWindow::render() + { + if (m_stopProcessingEvents) + { + return; + } + + // resizeEvent() can request an update before the first expose. + if (!m_initialized) + { + return; + } + + // A passive output only gets its own UpdateRequest from + // requestBestEffortRetry(): re-present what is already composited. + if (isPassiveOutput()) + { + if (m_videoDevice) + { + m_videoDevice->syncBuffers(); + } + return; + } + + IPCore::Session* session = m_doc ? m_doc->session() : nullptr; + if (!session) + { + return; + } + + if (IPCore::ImageRenderer::debugGpu()) + { + if (s_diagLoopTimer.isRunning()) + { + s_diagLoopMs += s_diagLoopTimer.elapsed() * 1000.0; + } + s_diagLoopTimer.start(); + + if (s_diagPointerPending) + { + s_diagPointerAgeMs += s_diagPointerTimer.elapsed() * 1000.0; + ++s_diagPointerAgeSamples; + s_diagPointerPending = false; + s_diagFrameEventTime = diagNow() - s_diagPointerTimer.elapsed(); + } + + // Close out retired slots (non-blocking; one frame of quantisation). + for (uint32_t i = 0; i < kFramesInFlight; ++i) + { + if (s_diagSlotArmed[i] && m_vkDevice && m_frameSync[i].fence + && vkGetFenceStatus(m_vkDevice, m_frameSync[i].fence) == VK_SUCCESS) + { + s_diagEventToRetireMs += (diagNow() - s_diagSlotEventTime[i]) * 1000.0; + ++s_diagEventToRetireSamples; + s_diagSlotArmed[i] = false; + } + } + } + + if (m_doc && session && m_videoDevice) + { + m_videoDevice->makeCurrent(); + + if (m_userActive && m_activityTimer.elapsed() > 1.0) + { + if (m_doc->mainPopup() && !m_doc->mainPopup()->isVisible() && m_eventWidget && m_eventWidget->hasFocus()) + { + TwkApp::ActivityChangeEvent aevent("user-inactive", m_videoDevice); + m_videoDevice->sendEvent(aevent); + m_userActive = false; + } + } + + int x = 0, y = 0; + absolutePosition(x, y); + m_videoDevice->setAbsolutePosition(x, y); + + const bool diagTiming = IPCore::ImageRenderer::debugGpu(); + Timer diagTimer; + if (diagTiming) + { + diagTimer.start(); + } + + session->render(); + + if (diagTiming) + { + s_diagRenderMs += diagTimer.elapsed() * 1000.0; + } + + if (!m_postFirstNonEmptyRender && session->postFirstNonEmptyRender()) + { + m_postFirstNonEmptyRender = true; + if (!session->isFullScreen()) + { + m_doc->resizeToFit(false, false); + m_doc->center(); + } + } + + m_firstPaintCompleted = true; + } + + if (m_stopProcessingEvents) + { + return; + } + + if (session && m_videoDevice) + { + // Always present the control viewport, even with a separate output + // device: unlike GL, nothing else composites it. + const bool diagPresent = IPCore::ImageRenderer::debugGpu(); + Timer diagPresentTimer; + if (diagPresent) + { + diagPresentTimer.start(); + } + + m_videoDevice->syncBuffers(); + + if (diagPresent) + { + s_diagMainPresentMs += diagPresentTimer.elapsed() * 1000.0; + } + + if (session->outputVideoDevice() && session->outputVideoDevice() != videoDevice()) + { + if (diagPresent) + { + diagPresentTimer.start(); + } + + session->outputVideoDevice()->syncBuffers(); + + if (diagPresent) + { + s_diagOutPresentMs += diagPresentTimer.elapsed() * 1000.0; + } + + // The output made its own GL context current; restore ours. + m_videoDevice->makeCurrent(); + } + } + + if (session) + { + const bool diagPost = IPCore::ImageRenderer::debugGpu(); + Timer diagPostTimer; + if (diagPost) + { + diagPostTimer.start(); + } + + session->addSyncSample(); + session->postRender(); + + if (diagPost) + { + s_diagPostRenderMs += diagPostTimer.elapsed() * 1000.0; + } + } + + // Averaged -debug gpu breakdown every 60 frames. + if (IPCore::ImageRenderer::debugGpu() && m_doc) + { + if (++s_diagFrames >= 60) + { + const double frames = static_cast(s_diagFrames); + const double loopMs = s_diagLoopMs / frames; + cout << "INFO: VulkanWindow frame avg over " << s_diagFrames << " [depth=" << maxFramesInFlight() + << " tiling=" << tilingName(m_shared[0].info.tiling) << "]" + << ": session->render()=" << (s_diagRenderMs / frames) << "ms mainPresent=" << (s_diagMainPresentMs / frames) + << "ms outputPresent=" << (s_diagOutPresentMs / frames) + << "ms total=" << ((s_diagRenderMs + s_diagMainPresentMs + s_diagOutPresentMs) / frames) + << "ms [mainPresent breakdown: fenceWait=" << (s_diagFenceWaitMs / frames) + << "ms acquire=" << (s_diagAcquireMs / frames) << "ms]" + << " postRender=" << (s_diagPostRenderMs / frames) << "ms frameInterval=" << loopMs << "ms (" + << (loopMs > 0.0 ? 1000.0 / loopMs : 0.0) << " fps)" + << " pointer: events=" << s_diagPointerEvents + << " handler=" << (s_diagPointerEvents ? s_diagPointerHandlerMs / s_diagPointerEvents : 0.0) + << "ms eventToRender=" << (s_diagPointerAgeSamples ? s_diagPointerAgeMs / s_diagPointerAgeSamples : 0.0) + << "ms eventToRetire=" << (s_diagEventToRetireSamples ? s_diagEventToRetireMs / s_diagEventToRetireSamples : 0.0) + << "ms" << endl; + s_diagFrames = 0; + s_diagRenderMs = 0.0; + s_diagMainPresentMs = 0.0; + s_diagOutPresentMs = 0.0; + s_diagFenceWaitMs = 0.0; + s_diagAcquireMs = 0.0; + s_diagLoopMs = 0.0; + s_diagPostRenderMs = 0.0; + s_diagPointerHandlerMs = 0.0; + s_diagPointerEvents = 0; + s_diagPointerAgeMs = 0.0; + s_diagPointerAgeSamples = 0; + s_diagEventToRetireMs = 0.0; + s_diagEventToRetireSamples = 0; + } + } + + m_eventProcessingTimer.start(); + } + + //-------------------------------------------------------------------------- + // QWindow overrides + //-------------------------------------------------------------------------- + + void VulkanWindow::exposeEvent(QExposeEvent* event) + { + QWindow::exposeEvent(event); + + if (m_stopProcessingEvents || !isExposed()) + { + return; + } + + if (m_initialized && handle() != m_initializedHandle) + { + handleSurfaceLost(); + } + + if (!m_initialized) + { + initialize(); + } + + // Show the last composited frame instead of blank until the next + // main-view frame. + if (!m_doc && m_initialized && m_videoDevice) + { + m_videoDevice->syncBuffers(); + return; + } + + requestUpdate(); + } + + void VulkanWindow::resizeEvent(QResizeEvent* event) + { + if (m_doc) + { + m_doc->viewSizeChanged(event->size().width(), event->size().height()); + } + QWindow::resizeEvent(event); + + // Nothing else repaints this surface on resize; requestUpdate() coalesces. + if (!m_stopProcessingEvents) + { + requestUpdate(); + } + } + + //-------------------------------------------------------------------------- + // eventProcessingTimeout slot + //-------------------------------------------------------------------------- + + void VulkanWindow::eventProcessingTimeout() + { + if (m_doc && m_doc->session()) + { + m_doc->session()->userGenericEvent("per-render-event-processing", ""); + } + } + + //-------------------------------------------------------------------------- + // event() + //-------------------------------------------------------------------------- + + bool VulkanWindow::event(QEvent* event) + { + // + // Before every guard below: this is the last point the Vulkan objects + // can legally be destroyed. The VkSurfaceKHR dies with the + // QPlatformWindow, and QWindowContainer destroys it before deleting + // this window, so releasing later segfaults inside the driver. + // + if (event->type() == QEvent::PlatformSurface) + { + if (static_cast(event)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) + { + releaseVulkanResources(); + } + return QWindow::event(event); + } + + if (!m_videoDevice) + { + return QWindow::event(event); + } + + Rv::Session* session = m_doc ? m_doc->session() : nullptr; + + if (m_stopProcessingEvents) + { + event->accept(); + return true; + } + + if (event->type() == QEvent::WindowActivate) + { + m_activationTimer.start(); + } + + float activationTime = 0.0f; + 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) + { + 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)) + { + 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: + // Drop stale modifiers. A passive output has no translator. + if (m_videoDevice->hasTranslator()) + { + m_videoDevice->translator().resetModifiers(); + } + break; + + case QEvent::Enter: + // A widget focus change, not a window activation, so hovering + // never steals activation from another window. Skipped when + // already focused: a repeat FocusIn makes QWindowContainer move + // focus to the next widget in the tab chain. + if (QGuiApplication::focusWindow() != this && m_eventWidget) + { + m_eventWidget->setFocus(Qt::MouseFocusReason); + } + break; + + default: + break; + } + + if (event->type() == QEvent::Resize) + { + QResizeEvent* resize = static_cast(event); + if (!isVisible()) + { + return true; + } + if (resize->oldSize().width() != -1 && resize->oldSize().height() != -1) + { + ostringstream contents; + contents << resize->oldSize().width() << " " << resize->oldSize().height() << "|" << resize->size().width() << " " + << resize->size().height(); + if (m_doc && session) + { + session->userGenericEvent("view-resized", contents.str()); + } + } + return QWindow::event(event); + } + + if (event->type() == QEvent::UpdateRequest) + { + render(); + return true; + } + + if (!m_videoDevice || !m_videoDevice->hasTranslator()) + { + return QWindow::event(event); + } + + auto resetTranslator = [this]() + { + m_videoDevice->translator().setScaleAndOffset(0, 0, 1.0f, 1.0f); + m_videoDevice->translator().setRelativeDomain(width(), height()); + }; + + 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 = static_cast(width()); + const float h = static_cast(height()); + const float ow = static_cast(odv->width()); + const float oh = static_cast(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.0f; + const float xscale = yscale; + const float xoffset = -(w * yscale - ow) / 2.0f; + m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); + } + else + { + const float xscale = ow / w; + const float xoffset = 0.0f; + const float yscale = xscale; + const float yoffset = -(xscale * h - oh) / 2.0f; + m_videoDevice->translator().setScaleAndOffset(xoffset, yoffset, xscale, yscale); + } + } + else + { + resetTranslator(); + } + } + else + { + resetTranslator(); + } + } + else + { + resetTranslator(); + } + + if (session) + { + session->setEventVideoDevice(videoDevice()); + } + + // Dispatch into Mu is synchronous, so this times the handler. + const bool diagPointer = + IPCore::ImageRenderer::debugGpu() + && (event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonPress || event->type() == QEvent::TabletMove); + Timer diagPointerTimer; + if (diagPointer) + { + diagPointerTimer.start(); + s_diagPointerTimer.start(); + s_diagPointerPending = true; + } + + const bool handled = m_videoDevice->translator().sendQTEvent(event, activationTime); + + if (diagPointer) + { + s_diagPointerHandlerMs += diagPointerTimer.elapsed() * 1000.0; + ++s_diagPointerEvents; + } + + if (handled) + { + event->accept(); + return true; + } + else + { + return QWindow::event(event); + } + } + +} // namespace Rv + +#endif // PLATFORM_LINUX || PLATFORM_WINDOWS diff --git a/src/lib/app/RvPackage/PackageManager.cpp b/src/lib/app/RvPackage/PackageManager.cpp index 34eb7c68f..5f98a8bd0 100644 --- a/src/lib/app/RvPackage/PackageManager.cpp +++ b/src/lib/app/RvPackage/PackageManager.cpp @@ -2043,6 +2043,8 @@ namespace Rv { m_globalSettingsP->sync(); delete m_globalSettingsP; + // globalSettings() reallocates only when this is null. + m_globalSettingsP = nullptr; } } @@ -2195,17 +2197,17 @@ namespace Rv qs->setFallbacksEnabled(false); #ifdef PLATFORM_WINDOWS - // Qt's atomic write (temp-file + rename) can fail with AccessError when Windows - // security software holds RV.ini open at the moment of the rename. Writing - // directly to the file avoids that failure point. Available since Qt 5.13. + // The atomic temp-file rename fails with AccessError when security + // software holds RV.ini open. qs->setAtomicSyncRequired(false); #endif - // Qt IniFormat on Windows does not create the parent directory automatically. - // Create it here so sync() does not fail with AccessError (err: 1). + // IniFormat does not create the parent directory, and sync() fails without it. QDir settingsDir(QFileInfo(qs->fileName()).absolutePath()); if (!settingsDir.exists()) + { settingsDir.mkpath("."); + } if (qs->status() != QSettings::NoError) { diff --git a/src/lib/audio/QTAudioRenderer/QTAudioRenderer.cpp b/src/lib/audio/QTAudioRenderer/QTAudioRenderer.cpp index 11a01bf26..85da740e3 100644 --- a/src/lib/audio/QTAudioRenderer/QTAudioRenderer.cpp +++ b/src/lib/audio/QTAudioRenderer/QTAudioRenderer.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // // +#include #include #include @@ -238,6 +239,15 @@ namespace IPCore } #endif + // + // A thread without a running event loop is not playing, and blocking + // on it would never return. + // + if (!canBlockOnAudioThread()) + { + return; + } + QMetaObject::invokeMethod(m_audioOutput, "stopAudio", Qt::BlockingQueuedConnection); } @@ -298,6 +308,14 @@ namespace IPCore } #endif + // + // See emitStopAudio(). + // + if (!canBlockOnAudioThread()) + { + return; + } + QMetaObject::invokeMethod(m_ioDevice, "stopDevice", Qt::BlockingQueuedConnection); } @@ -330,7 +348,15 @@ namespace IPCore // so that the QTAudioOuput and QTAudioIODevice // is created within run()'s execution thread. if (!createAudioOutput()) + { + // + // No event loop will run here, so detachAudioOutputDevice() cannot + // marshal the deletion onto this thread. Delete on the owner now. + // + deleteAudioOutputObjects(); + return; + } exec(); } @@ -367,7 +393,7 @@ namespace IPCore return true; } - void QTAudioThread::detachAudioOutputDevice() + bool QTAudioThread::detachAudioOutputDevice() { if (AudioRenderer::debug) TwkUtil::Log("AUDIO") << "detachAudioOutputDevice"; @@ -382,20 +408,67 @@ namespace IPCore emitStopAudio(); } + // + // Delete the output objects on the audio thread that owns them: on + // Windows, deleting them cross-thread trips QObject's cross-thread + // sendEvent() assertion (fatal in Qt6 debug builds). + // + if ((m_audioOutput || m_ioDevice) && canBlockOnAudioThread()) + { + QObject* owner = m_audioOutput ? static_cast(m_audioOutput) : static_cast(m_ioDevice); + + QMetaObject::invokeMethod(owner, [this]() { deleteAudioOutputObjects(); }, Qt::BlockingQueuedConnection); + } + quit(); - wait(); + if (!waitForAudioThreadToFinish()) + { + // + // The objects still belong to the running thread; leak them + // rather than delete them cross-thread. + // + return false; + } - if (m_audioOutput) + // + // Last resort for anything the marshalled delete could not reach. + // + deleteAudioOutputObjects(); + return true; + } + + bool QTAudioThread::canBlockOnAudioThread() const + { + return isRunning() && eventDispatcher() != nullptr && QThread::currentThread() != this; + } + + bool QTAudioThread::waitForAudioThreadToFinish() + { + constexpr unsigned long audioThreadExitTimeoutMS = 5000; + + if (wait(audioThreadExitTimeoutMS)) { - delete m_audioOutput; - m_audioOutput = 0; + return true; } - if (m_ioDevice) + static std::atomic reported{false}; + + if (!reported.exchange(true)) { - delete m_ioDevice; - m_ioDevice = 0; + std::cerr << "WARNING: audio thread did not exit within " << audioThreadExitTimeoutMS << " ms; continuing shutdown without it" + << std::endl; } + + return false; + } + + void QTAudioThread::deleteAudioOutputObjects() + { + delete m_audioOutput; + m_audioOutput = nullptr; + + delete m_ioDevice; + m_ioDevice = nullptr; } // @@ -1025,7 +1098,18 @@ namespace IPCore { if (m_thread) { - delete m_thread; + // + // Destroying a QThread that is still running is fatal in Qt6, so + // a wedged audio thread is leaked, detached from its parent. + // + if (m_thread->detachAudioOutputDevice()) + { + delete m_thread; + } + else + { + m_thread->setParent(nullptr); + } } } diff --git a/src/lib/audio/QTAudioRenderer/QTAudioRenderer/QTAudioRenderer.h b/src/lib/audio/QTAudioRenderer/QTAudioRenderer/QTAudioRenderer.h index bf9012794..f5758b9c0 100644 --- a/src/lib/audio/QTAudioRenderer/QTAudioRenderer/QTAudioRenderer.h +++ b/src/lib/audio/QTAudioRenderer/QTAudioRenderer/QTAudioRenderer.h @@ -111,6 +111,12 @@ namespace IPCore void startMe(); + // + // Stop the device and the thread. Returns false if the thread did + // not exit in time; it must then be leaked, not destroyed. + // + bool detachAudioOutputDevice(); + size_t processedSamples() const; void setProcessedSamples(size_t n); @@ -165,7 +171,22 @@ namespace IPCore private: bool createAudioOutput(); - void detachAudioOutputDevice(); + // + // True if a BlockingQueuedConnection call to this thread can return: + // it is running an event loop and is not the calling thread. + // + bool canBlockOnAudioThread() const; + + // + // Bounded wait(); reports once and returns false if the bound is + // reached. + // + bool waitForAudioThreadToFinish(); + + // + // Delete the output objects and null them. Idempotent. + // + void deleteAudioOutputObjects(); private: QMutex m_mutex; diff --git a/src/lib/geometry/TwkContainer/TwkContainer/Property.h b/src/lib/geometry/TwkContainer/TwkContainer/Property.h index 92f5a663b..1fb630cf1 100644 --- a/src/lib/geometry/TwkContainer/TwkContainer/Property.h +++ b/src/lib/geometry/TwkContainer/TwkContainer/Property.h @@ -517,10 +517,19 @@ namespace TwkContainer } } + // + // front() on an empty container is undefined behaviour (and asserts in + // MSVC debug builds), so an empty property returns null. + // template typename TypedProperty::const_value_pointer TypedProperty::data() const { + if (m_container.empty()) + { + return nullptr; + } + return &(m_container.front()); } @@ -528,6 +537,11 @@ namespace TwkContainer typename TypedProperty::value_pointer TypedProperty::data() { + if (m_container.empty()) + { + return nullptr; + } + return &(m_container.front()); } diff --git a/src/lib/graphics/TwkGLF/CMakeLists.txt b/src/lib/graphics/TwkGLF/CMakeLists.txt index f782294d4..51d7b41de 100644 --- a/src/lib/graphics/TwkGLF/CMakeLists.txt +++ b/src/lib/graphics/TwkGLF/CMakeLists.txt @@ -14,6 +14,7 @@ SET(_target SET(_sources GLVideoDevice.cpp GL.cpp + GLContextScope.cpp GLFBO.cpp GLVBO.cpp GLFence.cpp diff --git a/src/lib/graphics/TwkGLF/GL.cpp b/src/lib/graphics/TwkGLF/GL.cpp index ec520ce54..49827d5be 100644 --- a/src/lib/graphics/TwkGLF/GL.cpp +++ b/src/lib/graphics/TwkGLF/GL.cpp @@ -12,6 +12,8 @@ using namespace std; #include +#include + namespace { @@ -211,8 +213,49 @@ namespace TwkGLF } // namespace TwkGLF +// +// Qt only knows about contexts it made current; FBOVideoDevice binds its own +// natively, so also ask the platform. A GL call with no context current is +// undefined (macOS crashes), so glGetString() is only used on Linux, where +// GLVND returns null. +// +bool twkGlAnyContextIsCurrent() +{ + if (QOpenGLContext::currentContext() != nullptr) + { + return true; + } +#if defined(PLATFORM_DARWIN) + return CGLGetCurrentContext() != nullptr; +#elif defined(PLATFORM_WINDOWS) + return wglGetCurrentContext() != nullptr; +#else + return glGetString(GL_VERSION) != nullptr; +#endif +} + bool twkGlPrintError(std::string_view file, std::string_view function, const int line, const std::string_view msg) { + // + // With no context current, glGetError() is meaningless (Windows returns + // GL_INVALID_OPERATION for every call). Report once per episode instead, + // and reset when a context comes back. + // + static std::atomic noContextReported{false}; + + if (!twkGlAnyContextIsCurrent()) + { + if (!noContextReported.exchange(true)) + { + std::cerr << "GL_ERROR: " << shorterPath(file).data() << "::" << function.data() << ":" << line + << " [no current GL context -- this GL call, and any until a context is made current, did nothing]" << std::endl; + } + + return false; + } + + noContextReported = false; + if (GLuint err = glGetError()) { std::cerr << "GL_ERROR: " << shorterPath(file).data() << "::" << function.data() << ":" << line << " [" << TwkGLF::errorString(err) diff --git a/src/lib/graphics/TwkGLF/GLContextScope.cpp b/src/lib/graphics/TwkGLF/GLContextScope.cpp new file mode 100644 index 000000000..50bfda919 --- /dev/null +++ b/src/lib/graphics/TwkGLF/GLContextScope.cpp @@ -0,0 +1,178 @@ +// +// Copyright (c) 2025 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace TwkGLF +{ + + namespace + { + + // + // The fallback teardown context. Intentionally leaked: it is needed + // while the application is being torn down, so there is no safe + // point to free it. + // + QOpenGLContext* s_fallbackContext = nullptr; + QOffscreenSurface* s_fallbackSurface = nullptr; + bool s_fallbackAttempted = false; + + void reportNoContext(const char* why) + { + // Atomic: teardown can run off the GUI thread. + static std::atomic reported{false}; + + if (!reported.exchange(true)) + { + std::cerr << "WARNING: no GL context available for teardown (" << why + << "); GL objects destroyed without one will leak in the driver" << std::endl; + } + } + + // + // Build the fallback context, once. + // + bool createFallbackContext() + { + s_fallbackAttempted = true; + + // + // Must share with the global group: deleting a name from outside + // its share group silently does nothing. + // + QOpenGLContext* share = QOpenGLContext::globalShareContext(); + + if (!share) + { + reportNoContext("no global share context; Qt::AA_ShareOpenGLContexts is not set"); + return false; + } + + auto context = std::make_unique(); + context->setShareContext(share); + context->setFormat(share->format()); + + if (!context->create() || !context->shareContext()) + { + reportNoContext("shared context creation failed"); + return false; + } + + auto surface = std::make_unique(); + surface->setFormat(context->format()); + surface->create(); + + if (!surface->isValid()) + { + reportNoContext("offscreen surface creation failed"); + return false; + } + + s_fallbackContext = context.release(); + s_fallbackSurface = surface.release(); + + return true; + } + + bool makeFallbackCurrent() + { + // + // QOffscreenSurface::create() is GUI-thread only. + // + QCoreApplication* app = QCoreApplication::instance(); + + if (!qobject_cast(app)) + { + reportNoContext("no QGuiApplication"); + return false; + } + + if (QThread::currentThread() != app->thread()) + { + reportNoContext("not on the GUI thread"); + return false; + } + + if (!s_fallbackAttempted && !createFallbackContext()) + { + return false; + } + + if (!s_fallbackContext) + { + reportNoContext("fallback context unavailable"); + return false; + } + + if (!s_fallbackContext->makeCurrent(s_fallbackSurface)) + { + reportNoContext("fallback context could not be made current"); + return false; + } + + return true; + } + + } // namespace + + GLContextScope::GLContextScope(const GLVideoDevice* device) + { + // + // Never displace a live context, including a natively bound one. + // + if (twkGlAnyContextIsCurrent()) + { + return; + } + + if (device) + { + device->makeCurrent(); + + if (twkGlAnyContextIsCurrent()) + { + m_acquired = true; + return; + } + } + + if (makeFallbackCurrent()) + { + m_acquired = true; + } + } + + GLContextScope::~GLContextScope() + { + if (!m_acquired) + { + return; + } + + // + // Nothing was current on entry. A context bound natively by the + // device is invisible to Qt and is left to the device. + // + if (QOpenGLContext* current = QOpenGLContext::currentContext()) + { + current->doneCurrent(); + } + } + +} // namespace TwkGLF diff --git a/src/lib/graphics/TwkGLF/GLFBO.cpp b/src/lib/graphics/TwkGLF/GLFBO.cpp index 3f739a1c1..938570e95 100644 --- a/src/lib/graphics/TwkGLF/GLFBO.cpp +++ b/src/lib/graphics/TwkGLF/GLFBO.cpp @@ -14,6 +14,8 @@ #include #include +#include + /// #define NDEBUG namespace TwkGLF @@ -83,7 +85,26 @@ namespace TwkGLF GLFBO::~GLFBO() { - if (m_id && m_ownsFBOHandle) + // + // A GLFBO wrapping a device's bound framebuffer owns no GL names and + // needs no context to destroy. + // + const bool ownsHandles = (m_id != 0 && m_ownsFBOHandle); + const bool issuesGL = ownsHandles || (m_pbo != 0); + + // + // With no context current the deletes below are silent no-ops. Report + // the leak here (without asserting) and skip the GL calls. + // + bool canIssueGL = true; + + if (issuesGL && !twkGlAnyContextIsCurrent()) + { + twkGlPrintError(__FILE__, __FUNCTION__, __LINE__, ""); + canIssueGL = false; + } + + if (canIssueGL && ownsHandles) { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); TWK_GLDEBUG; @@ -115,11 +136,20 @@ namespace TwkGLF if (m_pbo) { - if (m_fence) + // + // A fence wait with no context current can never complete. + // + if (m_fence && canIssueGL) + { m_fence->wait(); + } delete m_fence; - glDeleteBuffers(1, &m_pbo); - TWK_GLDEBUG; + + if (canIssueGL) + { + glDeleteBuffers(1, &m_pbo); + TWK_GLDEBUG; + } } } @@ -474,6 +504,19 @@ namespace TwkGLF } } + bool GLFBO::isComplete() const + { + // + // GLFBO must be bound for glCheckFramebufferStatus() + // + bind(); + + const GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); + TWK_GLDEBUG; + + return status == GL_FRAMEBUFFER_COMPLETE_EXT; + } + void GLFBO::bindColorTexture(size_t i) const { assert(i < m_attachments.size()); @@ -531,6 +574,7 @@ namespace TwkGLF destinationGLFBO->bind(GL_DRAW_FRAMEBUFFER_EXT); glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + TWK_GLDEBUG; HOP_CALL(glFinish();) } diff --git a/src/lib/graphics/TwkGLF/GLPixelBufferObjectPool.cpp b/src/lib/graphics/TwkGLF/GLPixelBufferObjectPool.cpp index 86315af39..f1319d7a6 100644 --- a/src/lib/graphics/TwkGLF/GLPixelBufferObjectPool.cpp +++ b/src/lib/graphics/TwkGLF/GLPixelBufferObjectPool.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -500,6 +501,25 @@ namespace TwkGLF _cleanupNoLock(_usedPool); } + // + // Release every buffer while a context is still obtainable: the pools + // are statics whose destructors run after Qt is gone. Emptying the + // containers keeps that destructor from walking them again. + // + void clear() + { + std::unique_lock guard(_mutex); + + _cleanupNoLock(_freePool); + _cleanupNoLock(_usedPool); + + _freePool.clear(); + _usedPool.clear(); + + _allocSize = 0; + _allocNbBuffers = 0; + } + void setSoftMaxSize(size_t softMaxSize) { std::unique_lock guard(_mutex); @@ -825,6 +845,7 @@ namespace TwkGLF if (gPoolToGPUInitialized) { gPoolToGPUInitialized = false; + gPoolToGPU.clear(); } } else @@ -832,6 +853,7 @@ namespace TwkGLF if (gPoolFromGPUInitialized) { gPoolFromGPUInitialized = false; + gPoolFromGPU.clear(); } } } @@ -922,11 +944,20 @@ namespace TwkGLF // void UninitPBOPools() { + // + // Called after the event loop returns, when no view context is left. + // + const GLContextScope contextScope; + if (prefetchUsePBOs) + { PBOWrap::uninitPBOPool(GLPixelBufferObject::TO_GPU); + } if (writeBehindUsePBOs) + { PBOWrap::uninitPBOPool(GLPixelBufferObject::FROM_GPU); + } } } // namespace TwkGLF diff --git a/src/lib/graphics/TwkGLF/TwkGLF/GL.h b/src/lib/graphics/TwkGLF/TwkGLF/GL.h index f26b09cda..b3714edfb 100644 --- a/src/lib/graphics/TwkGLF/TwkGLF/GL.h +++ b/src/lib/graphics/TwkGLF/TwkGLF/GL.h @@ -7,6 +7,8 @@ //****************************************************************************** #ifndef __TwkGLF__GL__h__ #define __TwkGLF__GL__h__ +#include + #include #include #include @@ -99,10 +101,20 @@ struct GLPushMatrix // DEBUG macro // +// +// Is any GL context current, Qt's or a natively-bound one? +// +bool twkGlAnyContextIsCurrent(); + +// +// Declared in every build: GLFBO's destructor uses it to report a missing +// context in release builds too. TWK_GLDEBUG still compiles out under NDEBUG. +// +bool twkGlPrintError(std::string_view file, std::string_view function, const int line, std::string_view msg); + #ifdef NDEBUG #define TWK_GLDEBUG ; #else -bool twkGlPrintError(std::string_view file, std::string_view function, const int line, std::string_view msg); #define TWK_GLDEBUG twkGlPrintError(__FILE__, __FUNCTION__, __LINE__, ""); #define TWK_GLDEBUG_MSG(msg) twkGlPrintError(__FILE__, __FUNCTION__, __LINE__, msg); #endif diff --git a/src/lib/graphics/TwkGLF/TwkGLF/GLContextScope.h b/src/lib/graphics/TwkGLF/TwkGLF/GLContextScope.h new file mode 100644 index 000000000..dbe4d05aa --- /dev/null +++ b/src/lib/graphics/TwkGLF/TwkGLF/GLContextScope.h @@ -0,0 +1,51 @@ +// +// Copyright (c) 2025 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// +#ifndef __TwkGLF__GLContextScope__h__ +#define __TwkGLF__GLContextScope__h__ + +namespace TwkGLF +{ + class GLVideoDevice; + + // + // GLContextScope + // + // Guarantees that a GL context is current for the lifetime of the scope, + // so that GL deletes in teardown paths are not silent no-ops. + // + // Construction resolves a context in this order: + // + // 1. A context is already current: do nothing (a pointer compare). + // 2. A GLVideoDevice was supplied: makeCurrent() on it. + // 3. Otherwise: a process-lifetime fallback context in RV's global + // share group. + // + // Destruction makes nothing current again if this scope acquired. If no + // context can be resolved, the scope reports once and does nothing; it + // never throws. + // + // Limitation: an already-current context is kept even when it is not the + // supplied device's, since a natively bound context cannot be restored + // afterwards. That is correct for shared objects (textures, buffers, + // programs) but not for FBOs, which belong to the context that made them. + // + class GLContextScope + { + public: + explicit GLContextScope(const GLVideoDevice* device = nullptr); + ~GLContextScope(); + + GLContextScope(const GLContextScope&) = delete; + GLContextScope& operator=(const GLContextScope&) = delete; + + private: + bool m_acquired{false}; // did this scope make something current? + }; + +} // namespace TwkGLF + +#endif // __TwkGLF__GLContextScope__h__ diff --git a/src/lib/graphics/TwkGLF/TwkGLF/GLFBO.h b/src/lib/graphics/TwkGLF/TwkGLF/GLFBO.h index 55b2c9c18..5805ae46c 100644 --- a/src/lib/graphics/TwkGLF/TwkGLF/GLFBO.h +++ b/src/lib/graphics/TwkGLF/TwkGLF/GLFBO.h @@ -239,6 +239,12 @@ namespace TwkGLF void check() const; + // + // Non-throwing completeness test, for FBOs built around foreign + // attachments that can come out incomplete without any call failing. + // + bool isComplete() const; + // // Copy uses glBlitFramebuffer to do the work. The entire image // is copyed from the window of one to the other (so if aspect diff --git a/src/lib/ip/IPCore/IPCore/IPGraph.h b/src/lib/ip/IPCore/IPCore/IPGraph.h index 714c8d0c9..f62556ee4 100644 --- a/src/lib/ip/IPCore/IPCore/IPGraph.h +++ b/src/lib/ip/IPCore/IPCore/IPGraph.h @@ -332,6 +332,13 @@ namespace IPCore void setPhysicalDevices(const VideoModules&); + // + // Re-point the existing DisplayGroupIPNodes at a rebuilt set of + // physical devices, keeping each group's colour pipeline. Use this + // instead of setPhysicalDevices() when the monitors did not change. + // + void refreshPhysicalDevices(const VideoModules&); + // // Create a NodeValidation object on the stack. A node validation // context will become current. When complete the former context @@ -763,6 +770,7 @@ namespace IPCore void promoteFBsInFrameRange(int beg, int mid, int end, TwkUtil::Timer t); void setPhysicalDevicesInternal(const VideoModules&); + void refreshPhysicalDevicesInternal(const VideoModules&); void dispatchCachingThreadsSafely(); diff --git a/src/lib/ip/IPCore/IPGraph.cpp b/src/lib/ip/IPCore/IPGraph.cpp index 9998e2d94..b57d49a2c 100644 --- a/src/lib/ip/IPCore/IPGraph.cpp +++ b/src/lib/ip/IPCore/IPGraph.cpp @@ -746,6 +746,164 @@ namespace IPCore m_rootNode->appendInput(displayGroup); } + void IPGraph::refreshPhysicalDevices(const VideoModules& modules) + { + beginGraphEdit(); + refreshPhysicalDevicesInternal(modules); + endGraphEdit(); + } + + void IPGraph::refreshPhysicalDevicesInternal(const VideoModules& modules) + { + // + // Unlike setPhysicalDevicesInternal(), keep the existing display groups + // so their colour pipelines survive. Groups are matched to devices by + // (module name, device name), which is stable while the monitors are. + // + + std::vector devices; + + for (const auto& module : modules) + { + const TwkApp::VideoModule::VideoDevices& mdevices = module->devices(); + devices.insert(devices.end(), mdevices.begin(), mdevices.end()); + } + + // + // Nothing to preserve. + // + if (devices.empty() || m_displayGroups.empty()) + { + setPhysicalDevicesInternal(modules); + return; + } + + std::vector deviceMatched(devices.size(), false); + DisplayGroups survivors; + DisplayGroups doomed; + + for (DisplayGroupIPNode* group : m_displayGroups) + { + + if (group == m_defaultOutputGroup) + { + survivors.push_back(group); + continue; + } + + // + // Match on the stored names, never on physicalDevice(): that + // pointer refers to a device the caller has already destroyed. + // + const StringProperty* nameProp = group->property("device.name"); + const StringProperty* moduleProp = group->property("device.moduleName"); + const string groupName = (nameProp && !nameProp->empty()) ? nameProp->front() : ""; + const string groupModule = (moduleProp && !moduleProp->empty()) ? moduleProp->front() : ""; + + size_t match = devices.size(); + + for (size_t di = 0; di < devices.size(); di++) + { + if (deviceMatched[di]) + { + continue; + } + + const string deviceModule = devices[di]->module() ? devices[di]->module()->name() : ""; + + if (devices[di]->name() == groupName && deviceModule == groupModule) + { + match = di; + break; + } + } + + if (match == devices.size()) + { + // + // The group's device is gone (e.g. a monitor was unplugged). + // + if (m_rootNode->isInput(group)) + { + m_rootNode->removeInput(group); + } + group->willDelete(); + group->disconnectInputs(); + doomed.push_back(group); + continue; + } + + deviceMatched[match] = true; + group->setPhysicalVideoDevice(devices[match]); + + // + // Drop a dangling output pointer: findDisplayGroupByDevice() + // compares pointers and it could alias a new device. The control + // device is still alive, so keep it. + // + if (group->outputDevice() && group->outputDevice() != m_controlDevice) + { + const bool stillPresent = std::find(devices.begin(), devices.end(), group->outputDevice()) != devices.end(); + + if (!stillPresent) + { + group->setOutputVideoDevice(nullptr); + } + } + + survivors.push_back(group); + } + + // + // Publish the surviving set before deleting anything: ~DisplayGroupIPNode + // calls back into removeDisplayGroup(). + // + m_displayGroups = survivors; + + for (DisplayGroupIPNode* group : doomed) + { + delete group; + m_topologyChanged = true; + } + + // + // Unmatched devices are new. + // + for (size_t di = 0; di < devices.size(); di++) + { + if (deviceMatched[di]) + { + continue; + } + + size_t n = m_displayGroups.size(); + string name; + + do + { + ostringstream str; + str << "displayGroup" << n++; + name = str.str(); + } while (findNode(name)); + + m_displayGroups.push_back(newDisplayGroup(name, devices[di])); + } + + // + // The primary may have been deleted above, so re-establish the view + // connection and the root input for whichever group leads now. + // + if (DisplayGroupIPNode* displayGroup = primaryDisplayGroup()) + { + displayGroup->setInputs1(m_viewGroupNode); + + if (!m_rootNode->isInput(displayGroup)) + { + m_rootNode->appendInput(displayGroup); + } + } + } + void IPGraph::setPrimaryDisplayGroup(DisplayGroupIPNode* node) { DisplayGroups::iterator i = std::find(m_displayGroups.begin(), m_displayGroups.end(), node); @@ -798,7 +956,20 @@ namespace IPCore if (newDevice) { dnode->setOutputVideoDevice(newDevice); - dnode->setPhysicalVideoDevice(newDevice->physicalDevice()); + + // + // A view that has not rendered yet reports itself as its own + // physical device. Keep the group's monitor in that case, or + // its device.name no longer matches on refreshPhysicalDevices(). + // + if (const VideoDevice* physical = newDevice->physicalDevice()) + { + if (physical != newDevice) + { + dnode->setPhysicalVideoDevice(physical); + } + } + m_deviceChangedSignal(oldDevice, newDevice); } } diff --git a/src/lib/ip/IPCore/ImageFBO.cpp b/src/lib/ip/IPCore/ImageFBO.cpp index 1ce642624..b1da1c508 100644 --- a/src/lib/ip/IPCore/ImageFBO.cpp +++ b/src/lib/ip/IPCore/ImageFBO.cpp @@ -6,6 +6,7 @@ //****************************************************************************** #include +#include #include namespace @@ -238,6 +239,11 @@ namespace IPCore void ImageFBOManager::destroyImageFBO(ImageFBO* imageFBO) { + // + // Reached from destructors and event callbacks, not only renders. + // + const TwkGLF::GLContextScope contextScope; + m_totalSizeInBytes -= imageFBO->fbo()->totalSizeInBytes(); deleteFBOFence(imageFBO->fbo()); delete imageFBO->fbo(); @@ -545,6 +551,11 @@ namespace IPCore void ImageFBOManager::flushImageFBOs() { + // + // Acquire once for the whole flush rather than per FBO. + // + const TwkGLF::GLContextScope contextScope; + for (size_t i = 0; i < m_outputImageFBOs.size(); i++) destroyImageFBO(m_outputImageFBOs[i]); diff --git a/src/lib/ip/IPCore/ImageRenderer.cpp b/src/lib/ip/IPCore/ImageRenderer.cpp index 9808c99eb..de538aad9 100644 --- a/src/lib/ip/IPCore/ImageRenderer.cpp +++ b/src/lib/ip/IPCore/ImageRenderer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -182,6 +184,8 @@ namespace IPCore void ImageRenderer::Device::clearFBOs() { + const TwkGLF::GLContextScope contextScope(glDevice); + for (size_t i = 0; i < fboRingBuffer.size(); i++) { FBOVector& views = fboRingBuffer[i].views; @@ -478,6 +482,11 @@ namespace IPCore m_uploadThread.join(); } + // + // Everything from here down deletes GL objects. + // + const TwkGLF::GLContextScope contextScope(m_controlDevice.glDevice); + clearState(); // clean up @@ -610,6 +619,11 @@ namespace IPCore void ImageRenderer::clearState() { + // + // Covers flushProgramCache() too, not just flushImageFBOs(). + // + const TwkGLF::GLContextScope contextScope(m_controlDevice.glDevice); + clearRenderedImages(); // clear state will unbind the FBO currently bound @@ -1314,8 +1328,29 @@ namespace IPCore // unique device pair (controller and output). // + // + // m_outputDevice.glDevice is null for GLBindableVideoDevice outputs + // (presentation, AJA, NDI). Fall back to the control device, whose + // context owns the FBOs released below. + // if (m_outputDevice.glDevice) + { m_outputDevice.glDevice->makeCurrent(); + } + else if (m_controlDevice.glDevice) + { + m_controlDevice.glDevice->makeCurrent(); + } + else + { + static std::atomic reported{false}; + if (!reported.exchange(true)) + { + cerr << "ERROR: ImageRenderer::setOutputDevice: neither the output nor the control device is a GLVideoDevice; " + "the GL objects released below have no current context" + << endl; + } + } TWK_GLDEBUG; if (d) @@ -2414,7 +2449,7 @@ namespace IPCore // or waiting for the sync to complete before continuing. // // NOTE: I still think its possible to get stomped on -- you can - // tell if that's happen by setting m_debugGpu (-debug gpu in RV) + // tell if that has happened by setting m_debugGpu (-debug gpu in RV) // which will cause some debug code to clear to blue. If you see // blue flashing on the pres device that's the problem. // diff --git a/src/lib/ip/IPCore/Session.cpp b/src/lib/ip/IPCore/Session.cpp index ab35721ef..41c807aaa 100644 --- a/src/lib/ip/IPCore/Session.cpp +++ b/src/lib/ip/IPCore/Session.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1144,6 +1145,11 @@ namespace IPCore void Session::clearVideoDeviceCaches() { + // + // Runs from RenderContextChangeEvent and shutdown, outside a render. + // + const TwkGLF::GLContextScope contextScope(dynamic_cast(m_controlVideoDevice)); + if (m_controlVideoDevice) m_controlVideoDevice->clearCaches(); if (m_outputVideoDevice) @@ -1156,6 +1162,11 @@ namespace IPCore { if (d == m_outputVideoDevice || d == m_controlVideoDevice) { + // + // Arrives from a resize, outside a render. + // + const TwkGLF::GLContextScope contextScope(dynamic_cast(d)); + m_renderer->flushImageFBOs(); }