diff --git a/cmake/defaults/CYCOMMON.cmake b/cmake/defaults/CYCOMMON.cmake index 00e90e814..6656bfddf 100644 --- a/cmake/defaults/CYCOMMON.cmake +++ b/cmake/defaults/CYCOMMON.cmake @@ -149,6 +149,21 @@ SET(RV_DEPS_GLEW_VERSION_LIB "2.3.1" ) +# vulkan https://github.com/KhronosGroup/Vulkan-Headers + https://github.com/KhronosGroup/Vulkan-Loader +SET(RV_DEPS_VULKAN_VERSION + "1.4.350.0" +) +SET(RV_DEPS_VULKAN_HEADERS_DOWNLOAD_HASH + "74d68465ca2ef442397dc159edaa3b9c" +) +SET(RV_DEPS_VULKAN_LOADER_DOWNLOAD_HASH + "6ec91c673b48bbdffc923cce9d6a1a85" +) +# libvulkan.so SONAME major (libvulkan.so.1) +SET(RV_DEPS_VULKAN_VERSION_LIB + "1" +) + # imgui https://github.com/pthom/imgui # # Note this also depends on the following repositories: https://github.com/pthom/implot.git https://github.com/dpaulat/imgui-backend-qt.git diff --git a/cmake/dependencies/CMakeLists.txt b/cmake/dependencies/CMakeLists.txt index 8763f8fe3..f2dc182a2 100644 --- a/cmake/dependencies/CMakeLists.txt +++ b/cmake/dependencies/CMakeLists.txt @@ -106,6 +106,10 @@ IF(RV_TARGET_DARWIN INCLUDE(gc) INCLUDE(glew) ENDIF() +# Vulkan (headers + libvulkan.so loader) is always fetched on Linux. +IF(RV_TARGET_LINUX) + INCLUDE(vulkan) +ENDIF() INCLUDE(imath) INCLUDE(zlib) INCLUDE(ffmpeg) @@ -163,6 +167,9 @@ IF(RV_TARGET_DARWIN MESSAGE(STATUS "Using GC: ${RV_DEPS_GC_VERSION}") MESSAGE(STATUS "Using GLEW: ${RV_DEPS_GLEW_VERSION}") ENDIF() +IF(RV_TARGET_LINUX) + MESSAGE(STATUS "Using Vulkan: ${RV_DEPS_VULKAN_VERSION}") +ENDIF() MESSAGE(STATUS "Using Imath: ${RV_DEPS_IMATH_VERSION}") MESSAGE(STATUS "Using JpegTurbo: ${RV_DEPS_JPEGTURBO_VERSION}") MESSAGE(STATUS "Using OpenEXR: ${RV_DEPS_OPENEXR_VERSION}") diff --git a/cmake/dependencies/build/vulkan.cmake b/cmake/dependencies/build/vulkan.cmake new file mode 100644 index 000000000..fa54e7119 --- /dev/null +++ b/cmake/dependencies/build/vulkan.cmake @@ -0,0 +1,96 @@ +# +# Copyright (C) 2026 Autodesk, Inc. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Build Vulkan-Headers (header-only install) + Vulkan-Loader (libvulkan.so) from hash-pinned Khronos GitHub tarballs. Both install into the shared +# ${_install_dir}. Included by cmake/dependencies/vulkan.cmake when no system Vulkan package is found. +# +# Expects these variables from the caller (set by RV_CREATE_STANDARD_DEPS_VARIABLES): _target, _version, _install_dir, _include_dir, _lib_dir. And these +# dep-specific variables: _vulkan_lib, _vulkan_lib_name. +# +# The loader ships pre-generated sources and gates its Python codegen behind LOADER_CODEGEN (default OFF), so no Python is required at build time. It does +# require the Vulkan-Headers CMake package (find_package(VulkanHeaders CONFIG)), provided via CMAKE_PREFIX_PATH below. + +SET(_headers_url + "https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/vulkan-sdk-${_version}.tar.gz" +) +SET(_loader_url + "https://github.com/KhronosGroup/Vulkan-Loader/archive/refs/tags/vulkan-sdk-${_version}.tar.gz" +) + +# --- Vulkan-Headers: installs headers + the VulkanHeaders CMake config + the registry. --- +EXTERNALPROJECT_ADD( + ${_target}_headers + URL ${_headers_url} + URL_MD5 ${RV_DEPS_VULKAN_HEADERS_DOWNLOAD_HASH} + DOWNLOAD_NAME vulkan-headers-${_version}.tar.gz + DOWNLOAD_DIR ${RV_DEPS_DOWNLOAD_DIR} + SOURCE_DIR ${RV_DEPS_BASE_DIR}/${_target}/headers-src + BINARY_DIR ${RV_DEPS_BASE_DIR}/${_target}/headers-build + INSTALL_DIR ${_install_dir} + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${_install_dir} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DVULKAN_HEADERS_ENABLE_MODULE=OFF -DVULKAN_HEADERS_ENABLE_TESTS=OFF + BUILD_ALWAYS FALSE + USES_TERMINAL_BUILD TRUE +) + +# --- Vulkan-Loader: builds libvulkan.so against the installed headers. --- +# WSI: enable Xlib/XCB (RV is an X11 client); leave Wayland off to avoid a wayland-client build dependency. +EXTERNALPROJECT_ADD( + ${_target} + DEPENDS ${_target}_headers + URL ${_loader_url} + URL_MD5 ${RV_DEPS_VULKAN_LOADER_DOWNLOAD_HASH} + DOWNLOAD_NAME vulkan-loader-${_version}.tar.gz + DOWNLOAD_DIR ${RV_DEPS_DOWNLOAD_DIR} + SOURCE_DIR ${RV_DEPS_BASE_DIR}/${_target}/loader-src + BINARY_DIR ${RV_DEPS_BASE_DIR}/${_target}/loader-build + INSTALL_DIR ${_install_dir} + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${_install_dir} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_LIBDIR=lib + -DCMAKE_PREFIX_PATH=${_install_dir} + -DVULKAN_HEADERS_INSTALL_DIR=${_install_dir} + -DBUILD_TESTS=OFF + -DBUILD_WSI_XLIB_SUPPORT=ON + -DBUILD_WSI_XCB_SUPPORT=ON + -DBUILD_WSI_WAYLAND_SUPPORT=OFF + -DBUILD_WSI_XLIB_XRANDR_SUPPORT=OFF + BUILD_BYPRODUCTS ${_vulkan_lib} + BUILD_ALWAYS FALSE + USES_TERMINAL_BUILD TRUE +) + +IF(TARGET Vulkan::Vulkan) + # Qt's find_package(Qt6 Gui) -> WrapVulkanHeaders ran find_package(Vulkan) before us and created a global Vulkan::Vulkan pointing at the system loader. We + # can't redefine it (ADD_LIBRARY would error), so repoint it at our fetched build instead. This is invisible to Qt: it consumes only the Vulkan headers (via + # WrapVulkanHeaders) and dlopen()s the loader at runtime -- it never links Vulkan::Vulkan's IMPORTED_LOCATION. + MESSAGE(STATUS "Repointing existing Vulkan::Vulkan target at managed Vulkan ${_version}") + ADD_DEPENDENCIES(Vulkan::Vulkan ${_target}) + FILE(MAKE_DIRECTORY ${_include_dir}) + SET_TARGET_PROPERTIES( + Vulkan::Vulkan + PROPERTIES IMPORTED_LOCATION ${_vulkan_lib} + IMPORTED_SONAME ${_vulkan_lib_name} + INTERFACE_INCLUDE_DIRECTORIES ${_include_dir} + ) + SET(RV_DEPS_LIST + ${RV_DEPS_LIST} Vulkan::Vulkan + ) +ELSE() + RV_ADD_IMPORTED_LIBRARY( + NAME + Vulkan::Vulkan + TYPE + SHARED + LOCATION + ${_vulkan_lib} + SONAME + ${_vulkan_lib_name} + INCLUDE_DIRS + ${_include_dir} + DEPENDS + ${_target} + ADD_TO_DEPS_LIST + ) +ENDIF() diff --git a/cmake/dependencies/vulkan.cmake b/cmake/dependencies/vulkan.cmake new file mode 100644 index 000000000..1f1e54ace --- /dev/null +++ b/cmake/dependencies/vulkan.cmake @@ -0,0 +1,72 @@ +# +# Copyright (C) 2026 Autodesk, Inc. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Vulkan = Vulkan-Headers + Vulkan-Loader (libvulkan.so), fetched from hash-pinned Khronos GitHub tarballs. RV consumes only the headers () and +# the loader (linked via Vulkan::Vulkan; all API calls go through vkGetInstanceProcAddr/vkGetDeviceProcAddr) -- no validation layers or shader tools. Modeled on +# glew.cmake. +# + +# FORCE_LIB so _lib_dir is install/lib (we also pass -DCMAKE_INSTALL_LIBDIR=lib to the loader), giving a deterministic location across RHEL (lib64) and +# non-RHEL. +RV_CREATE_STANDARD_DEPS_VARIABLES("RV_DEPS_VULKAN" "${RV_DEPS_VULKAN_VERSION}" "" "" FORCE_LIB) + +# --- Try to find an installed/system package first (only when RV_DEPS_PREFER_INSTALLED=ON). --- +# Vulkan ships no CMake config; it is located via the built-in FindVulkan module (ALLOW_MODULE) or the `vulkan` pkg-config module. Omit VERSION (FindVulkan +# version reporting is unreliable). +RV_FIND_DEPENDENCY( + TARGET + ${_target} + PACKAGE + Vulkan + ALLOW_MODULE + PKG_CONFIG_NAME + vulkan + DEPS_LIST_TARGETS + Vulkan::Vulkan +) + +# --- Library naming (shared between find and build paths), à la glew's _glew_lib_name. --- +# We reference the SONAME symlink (libvulkan.so.1); the loader also installs the fully versioned real file (libvulkan.so.) and the libvulkan.so dev +# symlink alongside it. +SET(_vulkan_lib_name + ${CMAKE_SHARED_LIBRARY_PREFIX}vulkan${CMAKE_SHARED_LIBRARY_SUFFIX}.${RV_DEPS_VULKAN_VERSION_LIB} +) +SET(_vulkan_lib + ${_lib_dir}/${_vulkan_lib_name} +) + +IF(${_target}_FOUND) + # Found via RV_FIND_DEPENDENCY (RV_DEPS_PREFER_INSTALLED=ON). FindVulkan/pkg-config usually provides the Vulkan::Vulkan target; create it only if the + # pkg-config path did not. + IF(NOT TARGET Vulkan::Vulkan) + RV_ADD_IMPORTED_LIBRARY( + NAME + Vulkan::Vulkan + TYPE + SHARED + LOCATION + ${_vulkan_lib} + SONAME + ${_vulkan_lib_name} + INCLUDE_DIRS + ${_include_dir} + DEPENDS + ${_target} + ADD_TO_DEPS_LIST + ) + ENDIF() + + # Found path: use TARGET_LIBS to resolve the actual library path at build time. + RV_STAGE_DEPENDENCY_LIBS(TARGET ${_target} TARGET_LIBS Vulkan::Vulkan) +ELSE() + # --- Default: fetch headers + loader from Khronos and build from source. --- + # We always build our own hash-pinned Vulkan (like every other managed dependency) unless RV_DEPS_PREFER_INSTALLED=ON. Note that a global Vulkan::Vulkan + # target may already exist here: INCLUDE(qt6) runs before us and Qt's find_package(Qt6 Gui) -> WrapVulkanHeaders runs find_package(Vulkan), which (with + # CMAKE_FIND_PACKAGE_TARGETS_GLOBAL) creates a global Vulkan::Vulkan from a discoverable *system* loader. build/vulkan.cmake takes ownership of that target + # name (repointing it at our fetched build) rather than reusing the system loader. + INCLUDE(${CMAKE_CURRENT_LIST_DIR}/build/vulkan.cmake) + + RV_STAGE_DEPENDENCY_LIBS(TARGET ${_target} OUTPUTS ${RV_STAGE_LIB_DIR}/${_vulkan_lib_name}) +ENDIF() diff --git a/src/bin/apps/rv/main.cpp b/src/bin/apps/rv/main.cpp index a24e8e820..8a635fc97 100644 --- a/src/bin/apps/rv/main.cpp +++ b/src/bin/apps/rv/main.cpp @@ -368,6 +368,15 @@ int utf8Main(int argc, char* argv[]) // (RV Preferences/Rendering/Multithread GPU Upload) QApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + // Put every QOpenGLContext in one resource-sharing group. RV's GL contexts + // (GLView, video devices) already share manually via setShareContext, and + // shared resources like FTGL font-atlas textures rely on that invariant. On + // the Linux Vulkan presentation path there is no GLView to chain from, so + // the offscreen presentation context joins this global group instead (see + // QTVulkanVideoDevice::ensureGLContext). Otherwise, font glyph uploads land + // in a context where the atlas texture has no storage. + QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + TwkUtil::MemPool::initialize(); string altPrefsPath; diff --git a/src/lib/app/RvCommon/CMakeLists.txt b/src/lib/app/RvCommon/CMakeLists.txt index 64e324549..816bd95a5 100644 --- a/src/lib/app/RvCommon/CMakeLists.txt +++ b/src/lib/app/RvCommon/CMakeLists.txt @@ -80,6 +80,23 @@ IF(RV_TARGET_LINUX LIST(APPEND _sources ${_filename}) ENDIF() +IF(RV_TARGET_LINUX) + # Vulkan is always available on Linux (fetched via cmake/dependencies/vulkan.cmake). No VULKAN_SDK environment variable or setup-env.sh is required. + IF(NOT TARGET Vulkan::Vulkan) + MESSAGE(FATAL_ERROR "Vulkan::Vulkan target is missing on Linux; cmake/dependencies/vulkan.cmake should provide it") + ENDIF() + MESSAGE(STATUS "Linux Vulkan: using managed Vulkan ${RV_DEPS_VULKAN_VERSION}") + LIST( + APPEND + _sources + VulkanBuildProbe.cpp + VulkanView.cpp + QTVulkanVideoDevice.cpp + RvCommon/VulkanView.h + RvCommon/QTVulkanVideoDevice.h + ) +ENDIF() + FILE(GLOB _ui_sources ui/*.ui) IF(RV_VFX_PLATFORM STRGREATER_EQUAL CY2024) @@ -148,6 +165,9 @@ FILE( IF(NOT RV_TARGET_DARWIN) LIST(REMOVE_ITEM _files_to_moc "RvCommon/DisplayLink.h" "RvCommon/CGDesktopVideoDevice.h") ENDIF() +IF(NOT RV_TARGET_LINUX) + LIST(REMOVE_ITEM _files_to_moc "RvCommon/VulkanView.h" "RvCommon/QTVulkanVideoDevice.h") +ENDIF() FOREACH( file_to_moc @@ -450,6 +470,11 @@ IF(RV_TARGET_LINUX) ${_target} PRIVATE X11 ${_rvcommon_link_libraries} ) + + TARGET_LINK_LIBRARIES( + ${_target} + PRIVATE Vulkan::Vulkan + ) ENDIF() IF(RV_TARGET_WINDOWS) diff --git a/src/lib/app/RvCommon/GLView.cpp b/src/lib/app/RvCommon/GLView.cpp index 617f29581..9b8e87635 100644 --- a/src/lib/app/RvCommon/GLView.cpp +++ b/src/lib/app/RvCommon/GLView.cpp @@ -18,13 +18,17 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include +#include namespace Rv { @@ -35,6 +39,23 @@ namespace Rv namespace { + static string envOrUnset(const char* name) + { + const char* value = std::getenv(name); + return value ? value : ""; + } + + static 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(); + } class SyncBufferThreadData { @@ -229,6 +250,11 @@ namespace Rv fmt.setSwapInterval(vsync ? 1 : 0); +#ifdef PLATFORM_LINUX + if (ImageRenderer::reportGL()) + cout << "INFO: GLView requested QSurfaceFormat: " << formatSummary(fmt) << endl; +#endif + return fmt; } @@ -264,6 +290,50 @@ namespace Rv // NOTE_QT6: QGLFormat is deprecated. Using QSurfaceFormat now. QSurfaceFormat f = context()->format(); +#ifdef PLATFORM_LINUX + static bool baselineLogged = false; + if (ImageRenderer::reportGL() && !baselineLogged) + { + baselineLogged = true; + + QScreen* screen = this->screen(); + if (!screen) + screen = 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 widgetFormat = format(); + + cout << "INFO: GLView 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 (GLView args): rgba " << m_red << " " << m_green << " " << m_blue << " " + << m_alpha << endl; + cout << "INFO: QOpenGLWidget::format() (post-negotiation): " << formatSummary(widgetFormat) << endl; + cout << "INFO: Actual QOpenGLContext format: " << formatSummary(f) << endl; + if (screen) + { + cout << "INFO: Screen name: " << screen->name().toStdString() << ", depth: " << screen->depth() << endl; + } + else + { + cout << "INFO: Screen name: , depth: " << endl; + } + + cout << "INFO: GL vendor: " << (glVendor ? reinterpret_cast(glVendor) : "") << endl; + cout << "INFO: GL renderer: " << (glRenderer ? reinterpret_cast(glRenderer) : "") << endl; + cout << "INFO: GL version: " << (glVersion ? reinterpret_cast(glVersion) : "") << endl; + cout << "INFO: GLSL version: " << (glslVersion ? reinterpret_cast(glslVersion) : "") << endl; + 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: GLView runtime baseline end" << endl; + } +#endif + #ifndef PLATFORM_DARWIN // // Doesn't work on OS X diff --git a/src/lib/app/RvCommon/MuUICommands.cpp b/src/lib/app/RvCommon/MuUICommands.cpp index 6540b3bce..6aa729c34 100644 --- a/src/lib/app/RvCommon/MuUICommands.cpp +++ b/src/lib/app/RvCommon/MuUICommands.cpp @@ -473,7 +473,7 @@ namespace Rv s->receivingEvents(false); - QPoint p = rvDoc->view()->mapToGlobal(location); + QPoint p = rvDoc->viewWidget()->mapToGlobal(location); if (array) { @@ -500,11 +500,11 @@ namespace Rv if (const TwkApp::PointerEvent* pevent = dynamic_cast(e->event)) { - lp = QPoint(pevent->x(), rvDoc->view()->height() - pevent->y() - 1); + lp = QPoint(pevent->x(), rvDoc->viewWidget()->height() - pevent->y() - 1); } else { - lp = QPoint(0, rvDoc->view()->height() - 1); + lp = QPoint(0, rvDoc->viewWidget()->height() - 1); } popupMenuInternal(array, lp); @@ -518,7 +518,7 @@ namespace Rv int y = NODE_ARG(1, int); DynamicArray* array = NODE_ARG_OBJECT(2, DynamicArray); - QPoint lp(x, rvDoc->view()->height() - y - 1); + QPoint lp(x, rvDoc->viewWidget()->height() - y - 1); popupMenuInternal(array, lp); } @@ -653,7 +653,7 @@ namespace Rv rvDoc->setDocumentDisabled(false, true); bool result = dialog.exec(); - rvDoc->view()->setFocus(Qt::OtherFocusReason); + rvDoc->viewWidget()->setFocus(Qt::OtherFocusReason); rvDoc->setDocumentDisabled(false, false); if (result) @@ -776,7 +776,7 @@ namespace Rv rvDoc->setDocumentDisabled(false, true); bool result = dialog.exec(); - rvDoc->view()->setFocus(Qt::OtherFocusReason); + rvDoc->viewWidget()->setFocus(Qt::OtherFocusReason); rvDoc->setDocumentDisabled(false, false); if (result) @@ -885,7 +885,7 @@ namespace Rv { rvDoc->setDocumentDisabled(false, true); bool result = dialog.exec(); - rvDoc->view()->setFocus(Qt::OtherFocusReason); + rvDoc->viewWidget()->setFocus(Qt::OtherFocusReason); rvDoc->setDocumentDisabled(false, false); if (result) @@ -955,7 +955,7 @@ namespace Rv { Session* s = Session::currentSession(); RvDocument* rvDoc = (RvDocument*)s->opaquePointer(); - rvDoc->view()->setCursor(QCursor(Qt::CursorShape(NODE_ARG(0, int)))); + rvDoc->viewWidget()->setCursor(QCursor(Qt::CursorShape(NODE_ARG(0, int)))); } NODE_IMPLEMENTATION(alertPanel, int) @@ -1026,7 +1026,7 @@ namespace Rv else if (box.clickedButton() == q3 && b3) result = 2; - doc->view()->setFocus(Qt::OtherFocusReason); + doc->viewWidget()->setFocus(Qt::OtherFocusReason); NODE_RETURN(result); } @@ -1731,7 +1731,11 @@ namespace Rv MuLangContext* c = static_cast(p->context()); Session* s = Session::currentSession(); RvDocument* doc = reinterpret_cast(s->opaquePointer()); - QWidget* w = doc->view(); + // 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); @@ -2154,9 +2158,9 @@ namespace Rv const Session* s = Session::currentSession(); const RvDocument* doc = reinterpret_cast(s->opaquePointer()); - if (doc != nullptr && doc->view() != nullptr) + if (doc != nullptr && doc->viewWidget() != nullptr) { - devicePixelRatio = doc->view()->devicePixelRatio(); + devicePixelRatio = doc->viewWidget()->devicePixelRatio(); } NODE_RETURN(devicePixelRatio); diff --git a/src/lib/app/RvCommon/PyUICommands.cpp b/src/lib/app/RvCommon/PyUICommands.cpp index f32970634..939116aa7 100644 --- a/src/lib/app/RvCommon/PyUICommands.cpp +++ b/src/lib/app/RvCommon/PyUICommands.cpp @@ -241,7 +241,7 @@ namespace Rv s->receivingEvents(false); - QPoint p = rvDoc->view()->mapToGlobal(location); + QPoint p = rvDoc->viewWidget()->mapToGlobal(location); if (pylist) { @@ -273,11 +273,11 @@ namespace Rv if (const TwkApp::PointerEvent* pevent = dynamic_cast(event->event)) { - lp = QPoint(pevent->x(), rvDoc->view()->height() - pevent->y() - 1); + lp = QPoint(pevent->x(), rvDoc->viewWidget()->height() - pevent->y() - 1); } else { - lp = QPoint(0, rvDoc->view()->height() - 1); + lp = QPoint(0, rvDoc->viewWidget()->height() - 1); } popupMenuInternal(pylist, lp); @@ -299,7 +299,7 @@ namespace Rv return NULL; } - QPoint lp(x, rvDoc->view()->height() - y - 1); + QPoint lp(x, rvDoc->viewWidget()->height() - y - 1); popupMenuInternal(pylist, lp); diff --git a/src/lib/app/RvCommon/QTGLVideoDevice.cpp b/src/lib/app/RvCommon/QTGLVideoDevice.cpp index 50fd6e5e4..85411dfd2 100644 --- a/src/lib/app/RvCommon/QTGLVideoDevice.cpp +++ b/src/lib/app/RvCommon/QTGLVideoDevice.cpp @@ -138,29 +138,9 @@ namespace Rv { if (!isWorkerDevice()) { - 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(); - } + // 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(); } } diff --git a/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp b/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp new file mode 100644 index 000000000..3f0e5c1a9 --- /dev/null +++ b/src/lib/app/RvCommon/QTVulkanVideoDevice.cpp @@ -0,0 +1,534 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#ifdef PLATFORM_LINUX + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifndef GL_EXT_memory_object +#define GL_EXT_memory_object 1 +#define GL_TEXTURE_TILING_EXT 0x9580 +#define GL_DEDICATED_MEMORY_OBJECT_EXT 0x9581 +#define GL_NUM_TILING_TYPES_EXT 0x9582 +#define GL_TILING_TYPES_EXT 0x9583 +#define GL_OPTIMAL_TILING_EXT 0x9584 +#define GL_LINEAR_TILING_EXT 0x9585 +#define GL_NUM_DEVICE_UUIDS_EXT 0x9596 +#define GL_DEVICE_UUID_EXT 0x9597 +#define GL_DRIVER_UUID_EXT 0x9598 +#define GL_UUID_SIZE_EXT 16 +#endif + +#ifndef GL_EXT_memory_object_fd +#define GL_EXT_memory_object_fd 1 +#define GL_HANDLE_TYPE_OPAQUE_FD_EXT 0x9586 +#endif + +#ifndef GL_EXT_semaphore +#define GL_EXT_semaphore 1 +#define GL_NUM_SUPPORTED_SEMAPHORE_WAIT_LAYOUTS_EXT 0x958A +#define GL_SUPPORTED_SEMAPHORE_WAIT_LAYOUTS_EXT 0x958B +#define GL_NUM_SUPPORTED_SEMAPHORE_SIGNAL_LAYOUTS_EXT 0x958C +#define GL_SUPPORTED_SEMAPHORE_SIGNAL_LAYOUTS_EXT 0x958D +#define GL_LAYOUT_COLOR_ATTACHMENT_EXT 0x958E +#define GL_LAYOUT_DEPTH_STENCIL_ATTACHMENT_EXT 0x958F +#define GL_LAYOUT_DEPTH_STENCIL_READ_ONLY_EXT 0x9590 +#define GL_LAYOUT_SHADER_READ_ONLY_EXT 0x9591 +#define GL_LAYOUT_TRANSFER_SRC_EXT 0x9592 +#define GL_LAYOUT_TRANSFER_DST_EXT 0x9593 +#define GL_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_EXT 0x9530 +#define GL_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_EXT 0x9531 +#endif + +namespace Rv +{ + using namespace std; + using namespace TwkApp; + + QTVulkanVideoDevice::QTVulkanVideoDevice(VideoModule* module, const string& name, VulkanView* view, QWidget* eventWidget) + : TwkGLF::GLVideoDevice(module, name, VideoDevice::ImageOutput | VideoDevice::ProvidesSync | VideoDevice::SubWindow) + , m_view(view) + , m_eventWidget(eventWidget) + , m_translator(eventWidget ? new QTTranslator(this, eventWidget) : nullptr) + { + assert(view); + } + + QTVulkanVideoDevice::~QTVulkanVideoDevice() + { + // Delete the FBO and its colour texture while the GL context is current. + if (m_glContext && (m_fbo || m_fboColorTex || m_glMemoryObject)) + { + m_glContext->makeCurrent(m_offscreenSurface); + delete m_fbo; + m_fbo = nullptr; + if (m_fboColorTex) + { + glDeleteTextures(1, &m_fboColorTex); + m_fboColorTex = 0; + } + cleanupSharedGLObjects(); + m_glContext->doneCurrent(); + } + + delete m_offscreenSurface; + m_offscreenSurface = nullptr; + + delete m_glContext; + m_glContext = nullptr; + + delete m_translator; + } + + void QTVulkanVideoDevice::setEventWidget(QWidget* widget) + { + m_eventWidget = widget; + delete m_translator; + m_translator = widget ? new QTTranslator(this, widget) : nullptr; + } + + //-------------------------------------------------------------------------- + + void QTVulkanVideoDevice::ensureGLContext() const + { + if (!m_glContext) + { + QSurfaceFormat fmt; + fmt.setRenderableType(QSurfaceFormat::OpenGL); + fmt.setMajorVersion(2); + fmt.setMinorVersion(1); + + m_glContext = new QOpenGLContext(); + 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. + m_glContext->setShareContext(QOpenGLContext::globalShareContext()); + + if (!m_glContext->create()) + { + std::cerr << "[QTVulkanVideoDevice] QOpenGLContext::create() failed\n"; + delete m_glContext; + m_glContext = nullptr; + return; + } + + m_offscreenSurface = new QOffscreenSurface(); + m_offscreenSurface->setFormat(m_glContext->format()); + m_offscreenSurface->create(); + + if (!m_offscreenSurface->isValid()) + { + std::cerr << "[QTVulkanVideoDevice] QOffscreenSurface::create() failed\n"; + delete m_offscreenSurface; + m_offscreenSurface = nullptr; + delete m_glContext; + m_glContext = nullptr; + return; + } + + m_glContext->makeCurrent(m_offscreenSurface); + glewExperimental = GL_TRUE; + GLenum err = glewInit(); + if (err != GLEW_OK) + { + std::cerr << "[QTVulkanVideoDevice] glewInit failed: " << glewGetErrorString(err) << "\n"; + m_glContext->doneCurrent(); + delete m_offscreenSurface; + m_offscreenSurface = nullptr; + delete m_glContext; + m_glContext = nullptr; + return; + } + } + + if (!m_glContext->makeCurrent(m_offscreenSurface)) + { + std::cerr << "[QTVulkanVideoDevice] makeCurrent() failed\n"; + 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; + if (newW < 1) + newW = 128; + if (newH < 1) + newH = 128; + + if (!m_fbo || m_fboWidth != newW || m_fboHeight != newH) + { + delete m_fbo; + m_fbo = nullptr; + if (m_fboColorTex) + { + glDeleteTextures(1, &m_fboColorTex); + m_fboColorTex = 0; + } + + 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); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + + m_fbo = new TwkGLF::GLFBO(newW, newH, GL_RGBA16F_ARB); + m_fbo->attachColorTexture(GL_TEXTURE_RECTANGLE_ARB, m_fboColorTex); + + GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); + if (status != GL_FRAMEBUFFER_COMPLETE_EXT) + std::cerr << "[QTVulkanVideoDevice] FBO incomplete: 0x" << std::hex << status << std::dec << "\n"; + + m_fboWidth = newW; + m_fboHeight = newH; + } + + m_fbo->bind(); + + GLVideoDevice::makeCurrent(); + } + + //-------------------------------------------------------------------------- + + void QTVulkanVideoDevice::setAbsolutePosition(int x, int y) + { + if (x != m_x || y != m_y || m_refresh == -1.0f) + { + float refresh = -1.0f; + + int w = m_view ? m_view->width() : 0; + int h = m_view ? m_view->height() : 0; + int tx = x + w / 2; + int ty = y + h / 2; + + if (const TwkApp::VideoModule* mod = TwkApp::App()->primaryVideoModule()) + { + if (TwkApp::VideoDevice* d = mod->deviceFromPosition(tx, ty)) + { + setPhysicalDevice(d); + refresh = d->timing().hz; + + VideoDeviceContextChangeEvent event("video-device-changed", this, this, d); + sendEvent(event); + } + } + + 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; + m_y = y; + } + + void QTVulkanVideoDevice::setPhysicalDevice(VideoDevice* d) + { + TwkApp::VideoDevice::setPhysicalDevice(d); + + m_devicePixelRatio = 1.0f; + + static bool noQtHighDPISupport = (getenv("RV_NO_QT_HDPI_SUPPORT") != nullptr); + if (noQtHighDPISupport) + return; + + if (const DesktopVideoDevice* desktopDev = dynamic_cast(d)) + { + const QList screens = QGuiApplication::screens(); + if (desktopDev->qtScreen() < screens.size()) + m_devicePixelRatio = screens[desktopDev->qtScreen()]->devicePixelRatio(); + } + } + + float QTVulkanVideoDevice::devicePixelRatio() const + { + if (m_view) + return static_cast(m_view->devicePixelRatio()); + return m_devicePixelRatio; + } + + //-------------------------------------------------------------------------- + // GLVideoDevice API + //-------------------------------------------------------------------------- + + void QTVulkanVideoDevice::makeCurrent() const { ensureGLContext(); } + + TwkGLF::GLFBO* QTVulkanVideoDevice::defaultFBO() + { + ensureGLContext(); + return m_fbo; + } + + const TwkGLF::GLFBO* QTVulkanVideoDevice::defaultFBO() const + { + ensureGLContext(); + return m_fbo; + } + + std::string QTVulkanVideoDevice::hardwareIdentification() const { return "vulkan-hybrid"; } + + void QTVulkanVideoDevice::cleanupSharedGLObjects() const + { + if (m_glSharedTexture) + { + glDeleteTextures(1, &m_glSharedTexture); + m_glSharedTexture = 0; + } + if (m_glMemoryObject) + { + glDeleteMemoryObjectsEXT(1, &m_glMemoryObject); + m_glMemoryObject = 0; + } + if (m_glReadySemaphore) + { + glDeleteSemaphoresEXT(1, &m_glReadySemaphore); + m_glReadySemaphore = 0; + } + if (m_vkReadySemaphore) + { + glDeleteSemaphoresEXT(1, &m_vkReadySemaphore); + m_vkReadySemaphore = 0; + } + m_sharedWidth = 0; + m_sharedHeight = 0; + } + + //-------------------------------------------------------------------------- + // syncBuffers + //-------------------------------------------------------------------------- + + void QTVulkanVideoDevice::syncBuffers() const + { + if (!m_view) + return; + + if (!m_glContext || !m_fbo) + return; + + TwkGLF::GLFBO* fbo = m_fbo; + const int w = static_cast(fbo->width()); + const int h = static_cast(fbo->height()); + + if (w <= 0 || h <= 0) + return; + + if (!m_glContext->makeCurrent(m_offscreenSurface)) + return; + + // Get shared image info from VulkanView + const VulkanView::SharedImageInfo* sharedInfo = m_view->getSharedImageInfo(w, h); + if (!sharedInfo) + { + // Fallback to CPU readback if GPU interop fails. + // Packing below assumes A2B10G10R10_UNORM_PACK32 (= GL_RGB10_A2 bit layout). + // createSwapchain() guarantees this format is selected when GPU interop is used; + // a surface offering only A2R10G10B10 falls back to 8-bit and never reaches here. + assert(!m_view || m_view->swapchainFormat() == VK_FORMAT_A2B10G10R10_UNORM_PACK32 + || m_view->swapchainFormat() == VK_FORMAT_UNDEFINED); + fbo->bind(); + glFinish(); + const size_t floatCount = static_cast(w) * h * 4; + std::vector floatPx(floatCount); + glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, floatPx.data()); + fbo->unbind(); + + std::vector packed(static_cast(w) * h); + for (int y = 0; y < h; ++y) + { + const float* src = floatPx.data() + (size_t)(h - 1 - y) * w * 4; + uint32_t* dst = packed.data() + (size_t)y * w; + for (int x = 0; x < w; ++x, src += 4) + { + const float r = std::max(0.f, std::min(1.f, src[0])); + const float g = std::max(0.f, std::min(1.f, src[1])); + const float b = std::max(0.f, std::min(1.f, src[2])); + const uint32_t ri = static_cast(r * 1023.f + 0.5f) & 0x3FF; + const uint32_t gi = static_cast(g * 1023.f + 0.5f) & 0x3FF; + const uint32_t bi = static_cast(b * 1023.f + 0.5f) & 0x3FF; + dst[x] = (3u << 30) | (bi << 20) | (gi << 10) | ri; + } + } + m_view->presentPixelData(packed.data(), w, h); + return; + } + + // Check if we need to re-import the shared objects + if (m_sharedWidth != w || m_sharedHeight != h || !m_glMemoryObject) + { + cleanupSharedGLObjects(); + + glCreateMemoryObjectsEXT(1, &m_glMemoryObject); + // Duplicate the FD because glImportMemoryFdEXT takes ownership + int memFd = dup(sharedInfo->memoryFd); + glImportMemoryFdEXT(m_glMemoryObject, sharedInfo->size, GL_HANDLE_TYPE_OPAQUE_FD_EXT, memFd); + + glGenTextures(1, &m_glSharedTexture); + glBindTexture(GL_TEXTURE_2D, m_glSharedTexture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_TILING_EXT, GL_LINEAR_TILING_EXT); + + glTexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGB10_A2, sharedInfo->strideWidth, h, m_glMemoryObject, 0); + glBindTexture(GL_TEXTURE_2D, 0); + + glGenSemaphoresEXT(1, &m_glReadySemaphore); + int glReadyFd = dup(sharedInfo->glReadySemaphoreFd); + glImportSemaphoreFdEXT(m_glReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, glReadyFd); + + glGenSemaphoresEXT(1, &m_vkReadySemaphore); + int vkReadyFd = dup(sharedInfo->vkReadySemaphoreFd); + glImportSemaphoreFdEXT(m_vkReadySemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, vkReadyFd); + + m_sharedWidth = w; + m_sharedHeight = h; + } + + // Wait for Vulkan to be ready + GLuint waitDstLayouts[] = {GL_LAYOUT_COLOR_ATTACHMENT_EXT}; + glWaitSemaphoreEXT(m_vkReadySemaphore, 0, nullptr, 1, &m_glSharedTexture, waitDstLayouts); + + // Blit from FBO to shared texture + GLuint readFbo = fbo->fboID(); + GLuint drawFbo; + glGenFramebuffersEXT(1, &drawFbo); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, drawFbo); + glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_glSharedTexture, 0); + + glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, readFbo); + + // Note: GL origin is bottom-left, Vulkan origin is top-left. We need to flip Y. + glBlitFramebufferEXT(0, 0, w, h, 0, h, w, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, readFbo); // restore + glDeleteFramebuffersEXT(1, &drawFbo); + + // Signal Vulkan that GL is done + GLuint signalSrcLayouts[] = {GL_LAYOUT_TRANSFER_SRC_EXT}; + glSignalSemaphoreEXT(m_glReadySemaphore, 0, nullptr, 1, &m_glSharedTexture, signalSrcLayouts); + + glFlush(); + + // Tell VulkanView to present + m_view->presentSharedImage(); + } + + //-------------------------------------------------------------------------- + // VideoDevice API + //-------------------------------------------------------------------------- + + void QTVulkanVideoDevice::redraw() const + { + if (m_view) + { + QCoreApplication::postEvent(m_view, new QEvent(QEvent::UpdateRequest)); + } + } + + void QTVulkanVideoDevice::redrawImmediately() const { redraw(); } + + void QTVulkanVideoDevice::clearCaches() const {} + + VideoDevice::Resolution QTVulkanVideoDevice::resolution() const + { + if (!m_view) + { + 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); + } + + VideoDevice::Offset QTVulkanVideoDevice::offset() const { return Offset(m_x, m_y); } + + VideoDevice::Timing QTVulkanVideoDevice::timing() const { return Timing((m_refresh != -1.0f) ? m_refresh : 0.0f); } + + VideoDevice::VideoFormat QTVulkanVideoDevice::format() const + { + if (!m_view) + { + 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, + (m_refresh != -1.0f) ? m_refresh : 0.0f, hardwareIdentification()); + } + + size_t QTVulkanVideoDevice::width() const + { + if (!m_view) + { + return 0; + } + return static_cast(m_view->width() * m_view->devicePixelRatio() + 0.5f); + } + + size_t QTVulkanVideoDevice::height() const + { + if (!m_view) + { + return 0; + } + return static_cast(m_view->height() * m_view->devicePixelRatio() + 0.5f); + } + + void QTVulkanVideoDevice::open(const StringVector& /*args*/) + { + if (m_view) + { + m_view->show(); + } + m_isOpen = true; + } + + void QTVulkanVideoDevice::close() + { + if (m_view) + { + m_view->hide(); + } + m_isOpen = false; + } + + bool QTVulkanVideoDevice::isOpen() const + { + if (m_view) + { + return m_view->isVisible(); + } + return false; + } + +} // namespace Rv + +#endif // PLATFORM_LINUX diff --git a/src/lib/app/RvCommon/RvApplication.cpp b/src/lib/app/RvCommon/RvApplication.cpp index 567b72d11..7d8acdbc0 100644 --- a/src/lib/app/RvCommon/RvApplication.cpp +++ b/src/lib/app/RvCommon/RvApplication.cpp @@ -857,11 +857,19 @@ namespace Rv if (videoModules().empty()) { - doc->view()->makeCurrent(); + // With a non-OpenGL presentation backend view() returns null — no + // GL context to make current; presentation handles it per-frame. + if (doc->view()) + doc->view()->makeCurrent(); try { - addVideoModule(m_desktopModule = new DesktopVideoModule(0, doc->view()->videoDevice())); + // 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. + QTGLVideoDevice* shareDevice = doc->view() ? doc->view()->videoDevice() : nullptr; + addVideoModule(m_desktopModule = new DesktopVideoModule(0, shareDevice)); } catch (...) { @@ -887,7 +895,8 @@ namespace Rv // we're on (video device) so make sure the primary display group is // correct. // - doc->session()->graph().setPrimaryDisplayGroup(doc->view()->videoDevice()); + // 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) { @@ -918,7 +927,10 @@ namespace Rv if (!m->isOpen()) { RvDocument* doc = reinterpret_cast(documents().front()->opaquePointer()); - doc->view()->makeCurrent(); + // With a non-OpenGL presentation backend view() is null — no GL + // context to make current. + if (doc->view()) + doc->view()->makeCurrent(); m->open(); // // The open() may have added video devices, so make sure each @@ -1663,7 +1675,9 @@ namespace Rv #endif string optionArgs = setVideoDeviceStateFromSettings(d); - rvDoc->view()->videoDevice()->makeCurrent(); + // With a non-OpenGL presentation backend view() is null — skip GL makeCurrent. + if (rvDoc->view()) + rvDoc->view()->videoDevice()->makeCurrent(); try { diff --git a/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h b/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h new file mode 100644 index 000000000..1271b7ad1 --- /dev/null +++ b/src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h @@ -0,0 +1,109 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#pragma once + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QOpenGLContext; +class QOffscreenSurface; +class QWidget; +QT_END_NAMESPACE + +namespace Rv +{ + class VulkanView; + + // + // QTVulkanVideoDevice + // + // Wraps a VulkanView 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); + virtual ~QTVulkanVideoDevice(); + + VulkanView* vulkanView() const { return m_view; } + + QWidget* eventWidget() const { return m_eventWidget; } + + void setEventWidget(QWidget* widget); + + const QTTranslator& translator() const { return *m_translator; } + + bool hasTranslator() const { return m_translator != nullptr; } + + void setAbsolutePosition(int x, int y); + + // VideoDevice API + virtual void makeCurrent() const override; + virtual void syncBuffers() const override; + virtual void redraw() const override; + virtual void redrawImmediately() const override; + virtual void clearCaches() const override; + + virtual Resolution resolution() const override; + virtual Offset offset() const override; + virtual Timing timing() const override; + virtual VideoFormat format() const override; + + virtual size_t width() const override; + virtual size_t height() const override; + + virtual void open(const StringVector& args) override; + virtual void close() override; + virtual bool isOpen() const override; + + virtual float devicePixelRatio() const override; + + virtual void setPhysicalDevice(VideoDevice* d) override; + + // GLVideoDevice API + virtual TwkGLF::GLFBO* defaultFBO() override; + virtual const TwkGLF::GLFBO* defaultFBO() const override; + virtual std::string hardwareIdentification() const override; + + private: + // Ensure the QOpenGLContext + FBO exist and match the current view size. + // Makes the GL context current and binds the FBO on return. + void ensureGLContext() const; + + VulkanView* m_view; + QWidget* m_eventWidget; + QTTranslator* 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 QOpenGLContext* m_glContext{nullptr}; + mutable QOffscreenSurface* m_offscreenSurface{nullptr}; + mutable TwkGLF::GLFBO* m_fbo{nullptr}; + mutable GLuint m_fboColorTex{0}; // Texture attached to m_fbo; GLFBO does not own it + mutable int m_fboWidth{0}; + mutable int m_fboHeight{0}; + + // GPU Interop GL objects + mutable GLuint m_glMemoryObject{0}; + mutable GLuint m_glSharedTexture{0}; + mutable GLuint m_glReadySemaphore{0}; + mutable GLuint m_vkReadySemaphore{0}; + mutable int m_sharedWidth{0}; + mutable int m_sharedHeight{0}; + + void cleanupSharedGLObjects() const; + }; + +} // namespace Rv diff --git a/src/lib/app/RvCommon/RvCommon/RvDocument.h b/src/lib/app/RvCommon/RvCommon/RvDocument.h index 91634cc62..718701541 100644 --- a/src/lib/app/RvCommon/RvCommon/RvDocument.h +++ b/src/lib/app/RvCommon/RvCommon/RvDocument.h @@ -27,9 +27,17 @@ namespace TwkApp class Menu; } +namespace TwkGLF +{ + class GLVideoDevice; +} + namespace Rv { class GLView; +#if defined(PLATFORM_LINUX) + class VulkanView; +#endif class DiagnosticsView; class DesktopVideoModule; class DesktopVideoDevice; @@ -72,6 +80,20 @@ namespace Rv QMenu* mainPopup() const { return m_mainPopup; } GLView* view() const; + 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. + // + TwkGLF::GLVideoDevice* viewVideoDevice() const; + +#if defined(PLATFORM_LINUX) + VulkanView* vulkanView() const; +#endif const QAction* lastPopupAction() const { return m_lastPopupAction; } @@ -158,6 +180,10 @@ namespace Rv void rebuildGLView(bool stereo, bool vsync, bool dbl, int, int, int, int); + void setActiveViewContentSize(int w, int h); + void setActiveViewMinimumContentSize(int w, int h); + bool activeViewFirstPaintCompleted() const; + private: RvSession* m_session; QMenu* m_rvMenu; @@ -168,6 +194,10 @@ namespace Rv QDockWidget* m_diagnosticsDock; GLView* m_glView; GLView* m_oldGLView; +#if defined(PLATFORM_LINUX) + VulkanView* m_vulkanView; +#endif + QWidget* m_viewWidget; QWidget* m_viewContainerWidget; RvTopViewToolBar* m_topViewToolBar; RvBottomViewToolBar* m_bottomViewToolBar; diff --git a/src/lib/app/RvCommon/RvCommon/VulkanView.h b/src/lib/app/RvCommon/RvCommon/VulkanView.h new file mode 100644 index 000000000..2b45cac75 --- /dev/null +++ b/src/lib/app/RvCommon/RvCommon/VulkanView.h @@ -0,0 +1,181 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace Rv +{ + class RvDocument; + class QTVulkanVideoDevice; + + // + // VulkanView + // + // A QWidget subclass that presents 10-bit Vulkan images on Linux. + // 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. + // + class VulkanView : public QWidget + { + Q_OBJECT + + public: + typedef TwkUtil::Timer Timer; + + explicit VulkanView(RvDocument* doc, QWidget* parent = nullptr, bool noResize = true); + ~VulkanView(); + + QTVulkanVideoDevice* videoDevice() const { return m_videoDevice; } + + void setEventWidget(QWidget* widget); + + void stopProcessingEvents(); + + virtual bool event(QEvent* event) override; + virtual bool eventFilter(QObject* object, QEvent* event) override; + + bool firstPaintCompleted() const { return m_firstPaintCompleted; } + + void setContentSize(int w, int h) { m_csize = QSize(w, h); } + + void setMinimumContentSize(int w, int h) { m_msize = QSize(w, h); } + + QSize sizeHint() const override { return m_csize; } + + QSize minimumSizeHint() const override { return m_msize; } + + void absolutePosition(int& x, int& y) const; + + float devicePixelRatio() const; + + // Format of the active swapchain image (e.g. VK_FORMAT_A2B10G10R10_UNORM_PACK32). + // VK_FORMAT_UNDEFINED before the swapchain is created. + VkFormat swapchainFormat() const { return m_vkSwapchainFormat; } + + // + // Vulkan presentation — called by QTVulkanVideoDevice::syncBuffers(). + // + + // GPU Interop API + struct SharedImageInfo + { + int memoryFd; + size_t size; + int width; + int height; + int glReadySemaphoreFd; + int vkReadySemaphoreFd; + int strideWidth; + }; + + 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; } + + // + // Surface-independent probe for whether this machine's Vulkan can + // present a 10-bit (A2B10G10R10 / A2R10G10B10) image. 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 and queries format support; it + // does not require a window/surface and never throws — returns false + // if Vulkan is unavailable for any reason. + // + static bool supports10BitPresentation(); + + public slots: + void eventProcessingTimeout(); + + 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; } + + private: + bool initVulkan(); + void cleanupVulkan(); + bool createSwapchain(); + void cleanupSwapchain(); + + // 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; + QTVulkanVideoDevice* m_videoDevice; + + bool m_initialized; + bool m_firstPaintCompleted; + bool m_postFirstNonEmptyRender; + bool m_stopProcessingEvents; + bool m_userActive; + bool m_updatePending{false}; + + QSize m_csize; + QSize m_msize; + QWidget* m_eventWidget; + + unsigned int m_lastKey; + QEvent::Type m_lastKeyType; + 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; + + VkSemaphore m_vkImageAvailableSemaphore{VK_NULL_HANDLE}; + VkSemaphore m_vkRenderFinishedSemaphore{VK_NULL_HANDLE}; + VkFence m_vkFence{VK_NULL_HANDLE}; + + VkBuffer m_vkStagingBuffer{VK_NULL_HANDLE}; + VkDeviceMemory m_vkStagingBufferMemory{VK_NULL_HANDLE}; + size_t m_stagingBufferSize{0}; + + // Shared Image for GPU Interop + VkImage m_vkSharedImage{VK_NULL_HANDLE}; + VkDeviceMemory m_vkSharedImageMemory{VK_NULL_HANDLE}; + VkSemaphore m_vkGlReadySemaphore{VK_NULL_HANDLE}; + VkSemaphore m_vkVkReadySemaphore{VK_NULL_HANDLE}; + SharedImageInfo m_sharedImageInfo{-1, 0, 0, 0, -1, -1, 0}; + + void cleanupSharedImage(); + }; + +} // namespace Rv diff --git a/src/lib/app/RvCommon/RvDocument.cpp b/src/lib/app/RvCommon/RvDocument.cpp index 773864e6d..09d24bf87 100644 --- a/src/lib/app/RvCommon/RvDocument.cpp +++ b/src/lib/app/RvCommon/RvDocument.cpp @@ -24,6 +24,13 @@ #include #endif #include // WINDOWS: include AFTER other stuff +#if defined(PLATFORM_LINUX) +#include +#include +#ifdef __glew_h_ +#error "GLEW IS DEFINED BEFORE QTGUI!" +#endif +#endif #include #include #include @@ -145,7 +152,12 @@ namespace Rv , m_vsyncDisabled(false) , m_oldGLView(0) , m_glView(0) - , m_diagnosticsView(0) + , m_viewWidget(nullptr) +#if defined(PLATFORM_LINUX) + , m_vulkanView(nullptr) +#endif + , m_diagnosticsView(nullptr) + , m_diagnosticsDock(nullptr) , m_sourceEditor(0) , m_displayLink(0) , m_blockingOverlay(0) @@ -186,6 +198,74 @@ namespace Rv // // +#if defined(PLATFORM_LINUX) + // --- 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 GLX visual truncation; 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. + // + const bool want10bit = (opts.dispRedBits == 10 && opts.dispGreenBits == 10 && opts.dispBlueBits == 10 && opts.dispAlphaBits == 2); + + bool useVulkan = false; + if (want10bit) + { + useVulkan = VulkanView::supports10BitPresentation(); + if (!useVulkan) + { + cerr << "INFO: 10-bit display requested but Vulkan 10-bit " + "presentation is unavailable; falling back to OpenGL." + << endl; + } + } + + if (useVulkan) + { + // --- Vulkan path --- + m_vulkanView = new VulkanView(this, m_centralWidget, !m_startupResize); + + m_vulkanView->setFocusPolicy(Qt::StrongFocus); + m_vulkanView->setMouseTracking(true); + 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 = + 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); + } + else + { + RvSession* s = static_cast(docs.front()); + RvDocument* rvDoc = (RvDocument*)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). + 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, + true, // double buffer + opts.dispRedBits, opts.dispGreenBits, opts.dispBlueBits, opts.dispAlphaBits, !m_startupResize); + } + m_viewWidget = m_glView; + } +#else + // --- OpenGL path --- if (docs.empty()) { m_glView = @@ -196,31 +276,44 @@ namespace Rv { RvSession* s = static_cast(docs.front()); RvDocument* rvDoc = (RvDocument*)s->opaquePointer(); - m_glView = new GLView(this, rvDoc->view()->context(), this, opts.stereoMode && !strcmp(opts.stereoMode, "hardware"), + // 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, true, // double buffer opts.dispRedBits, opts.dispGreenBits, opts.dispBlueBits, opts.dispAlphaBits, !m_startupResize); } + m_viewWidget = m_glView; +#endif // PLATFORM_LINUX - // Create DiagnosticsView as a dockable widget (lazy initialization). - m_diagnosticsView = new DiagnosticsView(nullptr, m_glView->format()); + // 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. + const QSurfaceFormat diagnosticsFormat = m_glView ? m_glView->format() : QSurfaceFormat::defaultFormat(); + m_diagnosticsView = new DiagnosticsView(nullptr, diagnosticsFormat); // Dockable to QMainWindow, not centralwidget. - 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 + 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_stackedLayout = new QStackedLayout(m_centralWidget); m_stackedLayout->setStackingMode(QStackedLayout::StackAll); - m_stackedLayout->addWidget(m_glView); + m_stackedLayout->addWidget(m_viewWidget); setCentralWidget(m_viewContainerWidget); - m_glView->setFocus(Qt::OtherFocusReason); + m_viewWidget->setFocus(Qt::OtherFocusReason); // qApp->installEventFilter(m_glView); // #ifdef PLATFORM_DARWIN @@ -313,7 +406,7 @@ namespace Rv #endif // RvApp()->addVideoDevice(m_glView->videoDevice()); - m_session->setControlVideoDevice(m_glView->videoDevice()); + m_session->setControlVideoDevice(viewVideoDevice()); m_session->setRendererType("Composite"); m_session->setOpaquePointer(this); @@ -517,7 +610,8 @@ namespace Rv // #if defined(PLATFORM_DARWIN) && 0 - if (CGDesktopVideoDevice* cgdevice = dynamic_cast(m_glView->videoDevice()->physicalDevice())) + TwkGLF::GLVideoDevice* startDevice = viewVideoDevice(); + if (CGDesktopVideoDevice* cgdevice = startDevice ? dynamic_cast(startDevice->physicalDevice()) : nullptr) { if (m_displayLink) m_displayLink->start(m_session, cgdevice); @@ -552,14 +646,32 @@ namespace Rv } else if (m == IPCore::Session::updateMessage()) { - view()->videoDevice()->redraw(); + if (TwkGLF::GLVideoDevice* vd = viewVideoDevice()) + { + vd->redraw(); + } } else if (m == IPCore::Session::eventDeviceChangedMessage()) { - if (m_session->eventVideoDevice() && m_glView->videoDevice()) + // translator() is not on the shared GLVideoDevice base, so it is + // accessed via the concrete backend view here (still null-safe). + if (m_session->eventVideoDevice()) { - m_glView->videoDevice()->translator().setRelativeDomain(m_session->eventVideoDevice()->width(), - m_session->eventVideoDevice()->height()); + const int w = m_session->eventVideoDevice()->width(); + const int h = m_session->eventVideoDevice()->height(); +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + m_vulkanView->videoDevice()->translator().setRelativeDomain(w, h); + } + else +#endif + { + if (m_glView) + { + m_glView->videoDevice()->translator().setRelativeDomain(w, h); + } + } } } else if (m == TwkApp::Document::filenameChangedMessage()) @@ -610,7 +722,7 @@ namespace Rv setBuildMenu(); } #endif - m_glView->setFocus(Qt::OtherFocusReason); + m_viewWidget->setFocus(Qt::OtherFocusReason); } else if (m == IPCore::Session::audioUnavailbleMessage()) { @@ -742,9 +854,9 @@ namespace Rv void RvDocument::resetSizePolicy() { - m_glView->setMinimumContentSize(64, 64); - m_glView->setMinimumSize(QSize(64, 64)); - m_glView->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred)); + setActiveViewMinimumContentSize(64, 64); + m_viewWidget->setMinimumSize(QSize(64, 64)); + m_viewWidget->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred)); } void RvDocument::setDocumentDisabled(bool b, bool menuBarOnly) @@ -765,6 +877,17 @@ 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; + } + // // On the mac, we need to carefully replace the content GL widget so // that Qt doesn't resize the dock widgets and everything @@ -790,7 +913,7 @@ namespace Rv newGLView->setContentSize(oldGLView->sizeHint().width(), oldGLView->sizeHint().height()); - newGLView->setMinimumSize(oldGLView->minimumSizeHint().width(), oldGLView->minimumSizeHint().height()); + newGLView->setMinimumSize(QSize(oldGLView->minimumSizeHint().width(), oldGLView->minimumSizeHint().height())); bool resetGLPrefs = false; @@ -807,6 +930,7 @@ namespace Rv m_stackedLayout->addWidget(newGLView); m_stackedLayout->removeWidget(oldGLView); m_glView = newGLView; + m_viewWidget = m_glView; m_glView->show(); m_glView->setFocus(Qt::OtherFocusReason); @@ -843,10 +967,20 @@ namespace Rv QTimer::singleShot(100, this, SLOT(lazyDeleteGLView())); } - void RvDocument::showDiagnostics() { m_diagnosticsDock->show(); } + void RvDocument::showDiagnostics() + { + if (m_diagnosticsDock) + m_diagnosticsDock->show(); + } void RvDocument::setStereo(bool b) { +#if defined(PLATFORM_LINUX) + // 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(); bool dbl = false; @@ -864,6 +998,10 @@ namespace Rv { if (m_vsyncDisabled) return; +#if defined(PLATFORM_LINUX) + if (!m_glView) + return; +#endif const bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); bool dbl = false; @@ -879,6 +1017,10 @@ namespace Rv void RvDocument::setDoubleBuffer(bool b) { +#if defined(PLATFORM_LINUX) + if (!m_glView) + return; +#endif bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); const int red = m_glView->format().redBufferSize(); @@ -894,6 +1036,10 @@ namespace Rv void RvDocument::setDisplayOutput(DisplayOutputType type) { +#if defined(PLATFORM_LINUX) + if (!m_glView) + return; +#endif const bool vsync = m_glView->format().swapInterval() == 1; const bool stereo = m_glView->format().stereo(); bool dbl = false; @@ -936,8 +1082,66 @@ namespace Rv rebuildGLView(stereo, vsync, dbl, red, green, blue, alpha); } + void RvDocument::setActiveViewContentSize(int w, int h) + { +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + m_vulkanView->setContentSize(w, h); + return; + } +#endif + if (m_glView) + { + m_glView->setContentSize(w, h); + } + } + + void RvDocument::setActiveViewMinimumContentSize(int w, int h) + { +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + m_vulkanView->setMinimumContentSize(w, h); + return; + } +#endif + if (m_glView) + { + m_glView->setMinimumContentSize(w, h); + } + } + + bool RvDocument::activeViewFirstPaintCompleted() const + { +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + return m_vulkanView->firstPaintCompleted(); + } +#endif + return m_glView && m_glView->firstPaintCompleted(); + } + GLView* RvDocument::view() const { return m_glView; } + QWidget* RvDocument::viewWidget() const { return m_viewWidget; } + + TwkGLF::GLVideoDevice* RvDocument::viewVideoDevice() const + { +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + return m_vulkanView->videoDevice(); + } +#endif + return m_glView ? m_glView->videoDevice() : nullptr; + } + +#if defined(PLATFORM_LINUX) + VulkanView* RvDocument::vulkanView() const { return m_vulkanView; } +#endif + void RvDocument::center() { QScreen* screen = QApplication::screenAt(mapToGlobal(QPoint(0, 0))); @@ -1055,19 +1259,39 @@ namespace Rv h += int(mh); w += int(mw); - m_glView->setContentSize(w, h); - m_glView->setMinimumContentSize(w, h); - m_glView->updateGeometry(); + setActiveViewContentSize(w, h); + setActiveViewMinimumContentSize(w, h); +#if defined(PLATFORM_LINUX) + if (m_vulkanView) + { + const int dh = m_viewWidget->height() - h; + const int dw = m_viewWidget->width() - w; + if (dh || dw) + { + m_viewWidget->resize(w, h); + m_viewWidget->updateGeometry(); + resize(width() - dw, height() - dh); + setActiveViewContentSize(w, h); + setActiveViewMinimumContentSize(w, h); + m_viewWidget->resize(w, h); + m_viewWidget->updateGeometry(); + } + } + else +#endif + { + m_viewWidget->updateGeometry(); - const int dh = m_glView->height() - h; - const int dw = m_glView->width() - w; + const int dh = m_viewWidget->height() - h; + const int dw = m_viewWidget->width() - w; - if (dh || dw) - { - resize(width() - dw, height() - dh); - m_glView->setContentSize(w, h); - m_glView->setMinimumContentSize(w, h); - m_glView->updateGeometry(); + if (dh || dw) + { + resize(width() - dw, height() - dh); + setActiveViewContentSize(w, h); + setActiveViewMinimumContentSize(w, h); + m_viewWidget->updateGeometry(); + } } m_resetPolicyTimer->start(); @@ -1194,7 +1418,8 @@ namespace Rv } m_session->userRenderEvent("layout"); - Session::Margins margins = m_glView->videoDevice()->margins(); + TwkGLF::GLVideoDevice* marginsDevice = viewVideoDevice(); + Session::Margins margins = marginsDevice ? marginsDevice->margins() : Session::Margins(); float mw = int(margins.left + margins.right); float mh = int(margins.top + margins.bottom); @@ -1263,14 +1488,14 @@ namespace Rv DB("resizeToFit final target w " << w << " h " << h); - m_glView->setContentSize(int(w), int(h)); - m_glView->setMinimumContentSize(int(w), int(h)); - m_glView->updateGeometry(); + setActiveViewContentSize(int(w), int(h)); + setActiveViewMinimumContentSize(int(w), int(h)); + m_viewWidget->updateGeometry(); - DB("resizeToFit resulting size w " << m_glView->width() << " h " << m_glView->height()); + DB("resizeToFit resulting size w " << m_viewWidget->width() << " h " << m_viewWidget->height()); - const int dh = m_glView->height() - int(h); - const int dw = m_glView->width() - int(w); + const int dh = m_viewWidget->height() - int(h); + const int dw = m_viewWidget->width() - int(w); // // WHY? Dunno @@ -1281,11 +1506,11 @@ namespace Rv // if ((dh || dw)) { resize(width() - dw, height() - dh); - m_glView->setContentSize(int(w), int(h)); - m_glView->setMinimumContentSize(int(w), int(h)); - m_glView->updateGeometry(); + setActiveViewContentSize(int(w), int(h)); + setActiveViewMinimumContentSize(int(w), int(h)); + m_viewWidget->updateGeometry(); } - DB("resizeToFit final resulting size w " << m_glView->width() << " h " << m_glView->height()); + DB("resizeToFit final resulting size w " << m_viewWidget->width() << " h " << m_viewWidget->height()); m_resetPolicyTimer->start(); @@ -1367,7 +1592,7 @@ namespace Rv } } - m_glView->setFocus(Qt::OtherFocusReason); + m_viewWidget->setFocus(Qt::OtherFocusReason); activateWindow(); raise(); // @@ -1380,7 +1605,7 @@ namespace Rv QRect RvDocument::childrenRect() { QRect mr = mb()->geometry(); - QRect vr = m_glView->geometry(); + QRect vr = m_viewWidget->geometry(); DB("RvDocument::childrenRect mb" << " shown " << menuBarShown() << " vis " << mb()->isVisible() << " w" << mr.width() << " h " << mr.height() @@ -1889,7 +2114,10 @@ namespace Rv if (m_session) { m_session->userRenderEvent("view-size-changed", ""); - m_session->deviceSizeChanged(m_glView->videoDevice()); + if (TwkGLF::GLVideoDevice* vd = viewVideoDevice()) + { + m_session->deviceSizeChanged(vd); + } } } @@ -1941,7 +2169,7 @@ namespace Rv // For some reason KDE wants our main window to come up // "lowered" ie beneath the other windows. This is the // only way I've found to counter that. - if (waitingForFirstPaint && view() && view()->firstPaintCompleted()) + if (waitingForFirstPaint && activeViewFirstPaintCompleted()) { activateWindow(); raise(); @@ -2038,13 +2266,11 @@ namespace Rv void RvDocument::watchedFileChanged(const QString& path) { - TwkApp::GenericStringEvent event("file-changed", m_glView->videoDevice(), path.toUtf8().data()); - - // cout << m_watcher->files().size() << endl; - // cout << m_watcher->directories().size() << endl; - - // m_glView->frameBuffer()->sendEvent(event); - m_glView->videoDevice()->sendEvent(event); + TwkApp::VideoDevice* vdev = viewVideoDevice(); + if (!vdev) + return; + TwkApp::GenericStringEvent event("file-changed", vdev, path.toUtf8().data()); + vdev->sendEvent(event); } bool RvDocument::queryDriverVSync() const { return false; } diff --git a/src/lib/app/RvCommon/VulkanBuildProbe.cpp b/src/lib/app/RvCommon/VulkanBuildProbe.cpp new file mode 100644 index 000000000..00b6afb0d --- /dev/null +++ b/src/lib/app/RvCommon/VulkanBuildProbe.cpp @@ -0,0 +1,22 @@ +// +// Copyright (C) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#if defined(PLATFORM_LINUX) +#include +#endif + +namespace +{ + // Build-time probe only: keep one direct Vulkan symbol reference so opt-in + // Linux builds prove both headers and link-time loader availability. + void rvVulkanBuildProbeNoOp() + { + PFN_vkVoidFunction fn = vkGetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateInstance"); + (void)fn; + } +} // namespace diff --git a/src/lib/app/RvCommon/VulkanView.cpp b/src/lib/app/RvCommon/VulkanView.cpp new file mode 100644 index 000000000..7b2de8e4b --- /dev/null +++ b/src/lib/app/RvCommon/VulkanView.cpp @@ -0,0 +1,1387 @@ +// +// Copyright (c) 2026 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#ifdef PLATFORM_LINUX + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Rv +{ + using namespace std; + using namespace TwkApp; + using namespace IPCore; + + //-------------------------------------------------------------------------- + // VulkanView implementation + //-------------------------------------------------------------------------- + + VulkanView::VulkanView(RvDocument* doc, QWidget* parent, bool noResize) + : QWidget(parent) + , m_doc(doc) + , m_videoDevice(nullptr) + , m_initialized(false) + , m_firstPaintCompleted(false) + , m_postFirstNonEmptyRender(noResize) + , m_stopProcessingEvents(false) + , m_userActive(true) + , m_csize(1024, 576) + , m_msize(128, 128) + , m_eventWidget(nullptr) + , m_lastKey(0) + , m_lastKeyType(QEvent::None) + { + // 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* w = windowHandle()) + { + w->setSurfaceType(QSurface::VulkanSurface); + + // Set 10-bit format + QSurfaceFormat fmt; + fmt.setRedBufferSize(10); + fmt.setGreenBufferSize(10); + fmt.setBlueBufferSize(10); + fmt.setAlphaBufferSize(2); + w->setFormat(fmt); + } + + ostringstream str; + str << UI_APPLICATION_NAME " Main Window (Vulkan)" << "/" << m_doc; + m_videoDevice = new QTVulkanVideoDevice(nullptr, str.str(), this, nullptr); + + m_activityTimer.start(); + + m_eventProcessingTimer.setSingleShot(true); + connect(&m_eventProcessingTimer, SIGNAL(timeout()), this, SLOT(eventProcessingTimeout())); + } + + VulkanView::~VulkanView() + { + delete m_videoDevice; + cleanupSwapchain(); + cleanupVulkan(); + } + + //-------------------------------------------------------------------------- + + void VulkanView::setEventWidget(QWidget* widget) + { + m_eventWidget = widget; + if (m_videoDevice) + { + 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 + //-------------------------------------------------------------------------- + + void VulkanView::initialize() + { + if (m_initialized) + { + return; + } + + if (QWindow* w = windowHandle()) + { + w->setSurfaceType(QSurface::VulkanSurface); + QSurfaceFormat fmt; + fmt.setRedBufferSize(10); + fmt.setGreenBufferSize(10); + fmt.setBlueBufferSize(10); + fmt.setAlphaBufferSize(2); + w->setFormat(fmt); + } + + if (!initVulkan()) + { + std::cerr << "[VulkanView] initVulkan failed\n"; + return; + } + + m_initialized = true; + + if (m_doc) + { + m_doc->initializeSession(); + } + } + + bool VulkanView::supports10BitPresentation() + { + // Build a minimal instance (no surface) and ask each physical device + // whether it can use a 10-bit color format as a swapchain image, i.e. + // VK_FORMAT_A2B10G10R10_UNORM_PACK32 / VK_FORMAT_A2R10G10B10_UNORM_PACK32 + // with the color-attachment + transfer-dst features the swapchain uses. + // This mirrors the format chosen in createSwapchain(); the actual + // surface-level negotiation still happens there at present time. + QVulkanInstance qtVkInst; + if (!qtVkInst.create()) + { + std::cerr << "[VulkanView] supports10BitPresentation: QVulkanInstance " + "create failed\n"; + return false; + } + + VkInstance instance = qtVkInst.vkInstance(); + if (instance == VK_NULL_HANDLE) + { + return false; + } + + uint32_t deviceCount = 0; + vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); + if (deviceCount == 0) + { + return false; + } + std::vector devices(deviceCount); + vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); + + const VkFormat tenBitFormats[] = {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}; + const VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT; + + for (VkPhysicalDevice dev : devices) + { + for (VkFormat fmt : tenBitFormats) + { + VkFormatProperties props = {}; + vkGetPhysicalDeviceFormatProperties(dev, fmt, &props); + if ((props.optimalTilingFeatures & needed) == needed) + { + return true; + } + } + } + + return false; + } + + bool VulkanView::initVulkan() + { + // Create Instance + VkApplicationInfo appInfo = {}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "RV VulkanView"; + appInfo.apiVersion = VK_API_VERSION_1_1; + + // Need surface extensions + std::vector instanceExtensions = { + VK_KHR_SURFACE_EXTENSION_NAME, +#if defined(VK_USE_PLATFORM_XLIB_KHR) + VK_KHR_XLIB_SURFACE_EXTENSION_NAME, +#elif defined(VK_USE_PLATFORM_WAYLAND_KHR) + VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME, +#elif defined(VK_USE_PLATFORM_XCB_KHR) + VK_KHR_XCB_SURFACE_EXTENSION_NAME, +#endif + }; + + // Try to get Qt's extensions + static QVulkanInstance* qtVkInst = nullptr; + if (!qtVkInst) + { + qtVkInst = new QVulkanInstance(); + if (!qtVkInst->create()) + { + std::cerr << "[VulkanView] QVulkanInstance create failed\n"; + return false; + } + } + + m_vkInstance = qtVkInst->vkInstance(); + + // Create Surface + QWindow* w = windowHandle(); + if (!w) + { + return false; + } + + w->setVulkanInstance(qtVkInst); + + m_vkSurface = qtVkInst->surfaceForWindow(w); + if (!m_vkSurface) + { + std::cerr << "[VulkanView] Failed to create Vulkan surface\n"; + 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 = devices[0]; // just pick first for now + + // Find queue family + uint32_t queueFamilyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(m_vkPhysicalDevice, &queueFamilyCount, nullptr); + std::vector queueFamilies(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(m_vkPhysicalDevice, &queueFamilyCount, queueFamilies.data()); + + m_queueFamilyIndex = 0; + bool foundQueue = false; + for (uint32_t i = 0; i < queueFamilyCount; i++) + { + VkBool32 presentSupport = false; + vkGetPhysicalDeviceSurfaceSupportKHR(m_vkPhysicalDevice, i, m_vkSurface, &presentSupport); + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT && presentSupport) + { + m_queueFamilyIndex = i; + foundQueue = true; + break; + } + } + + if (!foundQueue) + { + return false; + } + + // 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, VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, + VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME}; + + VkDeviceCreateInfo createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + createInfo.pQueueCreateInfos = &queueCreateInfo; + createInfo.queueCreateInfoCount = 1; + createInfo.enabledExtensionCount = 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); + + // 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; + vkCreateCommandPool(m_vkDevice, &poolInfo, nullptr, &m_vkCommandPool); + + // Sync objects + VkSemaphoreCreateInfo semaphoreInfo = {}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkImageAvailableSemaphore); + vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkRenderFinishedSemaphore); + + VkFenceCreateInfo fenceInfo = {}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &m_vkFence); + + return true; + } + + void VulkanView::cleanupVulkan() + { + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + if (m_vkImageAvailableSemaphore) + vkDestroySemaphore(m_vkDevice, m_vkImageAvailableSemaphore, nullptr); + if (m_vkRenderFinishedSemaphore) + vkDestroySemaphore(m_vkDevice, m_vkRenderFinishedSemaphore, nullptr); + if (m_vkFence) + vkDestroyFence(m_vkDevice, m_vkFence, nullptr); + + if (m_vkCommandPool) + vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr); + + vkDestroyDevice(m_vkDevice, nullptr); + } + // Surface is managed by QVulkanInstance? We shouldn't destroy it here if QVulkanInstance owns it, but wait, we got it from + // surfaceForWindow. Actually QVulkanWindow destroys it. We can just leave it for QVulkanInstance to clean up, or we can + // vkDestroySurfaceKHR if needed. For safety we don't destroy instance/surface here, they are tied to Qt. + } + + bool VulkanView::createSwapchain() + { + if (!m_vkDevice || !m_vkSurface) + { + 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()); + + VkSurfaceFormatKHR surfaceFormat = formats[0]; + bool found10bit = false; + // Prefer A2B10G10R10 exclusively: GL_RGB10_A2 (used for both the GPU interop texture + // and the CPU fallback packing) maps to this layout. A2R10G10B10 has the opposite R/B + // component order, so accepting it here would swap red and blue in all rendered pixels. + for (const auto& fmt : formats) + { + if (fmt.format == VK_FORMAT_A2B10G10R10_UNORM_PACK32) + { + surfaceFormat = fmt; + found10bit = true; + break; + } + } + + // The Vulkan path was selected to deliver 10-bit. If the surface does + // not actually offer a 10-bit format, presentation truncates to 8-bit + // and Vulkan provides no quality benefit over OpenGL (only the GL<->Vulkan + // interop overhead). The device-level probe in supports10BitPresentation() + // cannot detect this surface/compositor limitation, so warn here once it + // is known. (Decision-time fallback to OpenGL is a possible follow-up.) + static bool warned8bit = false; + if (!found10bit && !warned8bit) + { + warned8bit = true; + // Also report if A2R10G10B10 is available but not usable. + bool has_a2r10g10b10 = false; + for (const auto& fmt : formats) + { + if (fmt.format == VK_FORMAT_A2R10G10B10_UNORM_PACK32) + { + has_a2r10g10b10 = true; + break; + } + } + if (has_a2r10g10b10) + { + std::cerr << "[VulkanView] WARNING: Vulkan surface offers A2R10G10B10 but not " + "A2B10G10R10; presenting 8-bit (channel-swapped 10-bit not yet " + "supported).\n"; + } + else + { + std::cerr << "[VulkanView] WARNING: Vulkan surface offers no 10-bit " + "format; presenting 8-bit. Vulkan gives no benefit over " + "OpenGL on this surface/compositor.\n"; + } + } + + m_vkSwapchainFormat = surfaceFormat.format; + + m_vkSwapchainExtent = capabilities.currentExtent; + if (m_vkSwapchainExtent.width == 0xFFFFFFFF) + { + m_vkSwapchainExtent = {(uint32_t)width(), (uint32_t)height()}; + } + if (m_vkSwapchainExtent.width == 0 || m_vkSwapchainExtent.height == 0) + { + return false; + } + + uint32_t imageCount = capabilities.minImageCount + 1; + if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) + { + imageCount = capabilities.maxImageCount; + } + + 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; + createInfo.oldSwapchain = VK_NULL_HANDLE; + + if (vkCreateSwapchainKHR(m_vkDevice, &createInfo, nullptr, &m_vkSwapchain) != VK_SUCCESS) + { + return false; + } + + 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 = (uint32_t)m_vkCommandBuffers.size(); + vkAllocateCommandBuffers(m_vkDevice, &allocInfo, m_vkCommandBuffers.data()); + + return true; + } + + void VulkanView::cleanupSwapchain() + { + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + if (m_vkStagingBuffer) + { + vkDestroyBuffer(m_vkDevice, m_vkStagingBuffer, nullptr); + m_vkStagingBuffer = VK_NULL_HANDLE; + } + if (m_vkStagingBufferMemory) + { + vkFreeMemory(m_vkDevice, m_vkStagingBufferMemory, nullptr); + m_vkStagingBufferMemory = VK_NULL_HANDLE; + } + m_stagingBufferSize = 0; + + if (!m_vkCommandBuffers.empty()) + { + vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, m_vkCommandBuffers.size(), m_vkCommandBuffers.data()); + m_vkCommandBuffers.clear(); + } + + if (m_vkSwapchain) + { + vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr); + m_vkSwapchain = VK_NULL_HANDLE; + } + } + } + + // Helper to find memory type + uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) + { + 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 UINT32_MAX; + } + + void VulkanView::cleanupSharedImage() + { + if (m_vkDevice) + { + vkDeviceWaitIdle(m_vkDevice); + + if (m_vkSharedImage) + { + vkDestroyImage(m_vkDevice, m_vkSharedImage, nullptr); + m_vkSharedImage = VK_NULL_HANDLE; + } + if (m_vkSharedImageMemory) + { + vkFreeMemory(m_vkDevice, m_vkSharedImageMemory, nullptr); + m_vkSharedImageMemory = VK_NULL_HANDLE; + } + if (m_vkGlReadySemaphore) + { + vkDestroySemaphore(m_vkDevice, m_vkGlReadySemaphore, nullptr); + m_vkGlReadySemaphore = VK_NULL_HANDLE; + } + if (m_vkVkReadySemaphore) + { + vkDestroySemaphore(m_vkDevice, m_vkVkReadySemaphore, nullptr); + m_vkVkReadySemaphore = VK_NULL_HANDLE; + } + } + + if (m_sharedImageInfo.memoryFd != -1) + { + ::close(m_sharedImageInfo.memoryFd); + m_sharedImageInfo.memoryFd = -1; + } + if (m_sharedImageInfo.glReadySemaphoreFd != -1) + { + ::close(m_sharedImageInfo.glReadySemaphoreFd); + m_sharedImageInfo.glReadySemaphoreFd = -1; + } + if (m_sharedImageInfo.vkReadySemaphoreFd != -1) + { + ::close(m_sharedImageInfo.vkReadySemaphoreFd); + m_sharedImageInfo.vkReadySemaphoreFd = -1; + } + m_sharedImageInfo.width = 0; + m_sharedImageInfo.height = 0; + m_sharedImageInfo.size = 0; + } + + const VulkanView::SharedImageInfo* VulkanView::getSharedImageInfo(int w, int h) + { + if (!m_vkDevice) + return nullptr; + + // If we already have a shared image of the right size, return it + if (m_vkSharedImage && m_sharedImageInfo.width == w && m_sharedImageInfo.height == h) + { + return &m_sharedImageInfo; + } + + cleanupSharedImage(); + + if (!m_vkSwapchain || m_vkSwapchainExtent.width != (uint32_t)w || m_vkSwapchainExtent.height != (uint32_t)h) + { + cleanupSwapchain(); + if (!createSwapchain()) + return nullptr; + } + + // 1. Create Shared Image + VkExternalMemoryImageCreateInfo extMemInfo = {}; + extMemInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + extMemInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.pNext = &extMemInfo; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = m_vkSwapchainFormat; // e.g., VK_FORMAT_A2B10G10R10_UNORM_PACK32 + imageInfo.extent = {(uint32_t)w, (uint32_t)h, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_LINEAR; // Use linear tiling to avoid GL/Vulkan optimal swizzle mismatch + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT; // Only used as transfer src in Vulkan + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + if (vkCreateImage(m_vkDevice, &imageInfo, nullptr, &m_vkSharedImage) != VK_SUCCESS) + { + std::cerr << "[VulkanView] Failed to create shared image\n"; + return nullptr; + } + + // 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_vkSharedImage, &subresource, &layout); + + if (layout.rowPitch % 4 != 0) + { + // Cannot represent this stride as an integer pixel-width texture; fall back to CPU bridge. + cleanupSharedImage(); + return nullptr; + } + m_sharedImageInfo.strideWidth = static_cast(layout.rowPitch / 4); + + VkMemoryRequirements memReqs; + vkGetImageMemoryRequirements(m_vkDevice, m_vkSharedImage, &memReqs); + + VkExportMemoryAllocateInfo exportAllocInfo = {}; + exportAllocInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + exportAllocInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.pNext = &exportAllocInfo; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = findMemoryType(m_vkPhysicalDevice, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (allocInfo.memoryTypeIndex == UINT32_MAX) + { + std::cerr << "[VulkanView] No device-local memory type for shared image\n"; + cleanupSharedImage(); + return nullptr; + } + + if (vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_vkSharedImageMemory) != VK_SUCCESS) + { + std::cerr << "[VulkanView] Failed to allocate shared image memory\n"; + cleanupSharedImage(); + return nullptr; + } + + vkBindImageMemory(m_vkDevice, m_vkSharedImage, m_vkSharedImageMemory, 0); + + // Export Memory FD + auto pfnGetMemoryFdKHR = (PFN_vkGetMemoryFdKHR)vkGetDeviceProcAddr(m_vkDevice, "vkGetMemoryFdKHR"); + if (!pfnGetMemoryFdKHR) + { + std::cerr << "[VulkanView] vkGetMemoryFdKHR not found\n"; + cleanupSharedImage(); + return nullptr; + } + + VkMemoryGetFdInfoKHR getFdInfo = {}; + getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + getFdInfo.memory = m_vkSharedImageMemory; + getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + int memFd = -1; + if (pfnGetMemoryFdKHR(m_vkDevice, &getFdInfo, &memFd) != VK_SUCCESS) + { + std::cerr << "[VulkanView] Failed to get memory FD\n"; + cleanupSharedImage(); + return nullptr; + } + + // 2. Create Shared Semaphores + VkExportSemaphoreCreateInfo exportSemInfo = {}; + exportSemInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; + exportSemInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkSemaphoreCreateInfo semInfo = {}; + semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semInfo.pNext = &exportSemInfo; + + if (vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_vkGlReadySemaphore) != VK_SUCCESS + || vkCreateSemaphore(m_vkDevice, &semInfo, nullptr, &m_vkVkReadySemaphore) != VK_SUCCESS) + { + std::cerr << "[VulkanView] Failed to create shared semaphores\n"; + cleanupSharedImage(); + return nullptr; + } + + // Export Semaphore FDs + auto pfnGetSemaphoreFdKHR = (PFN_vkGetSemaphoreFdKHR)vkGetDeviceProcAddr(m_vkDevice, "vkGetSemaphoreFdKHR"); + if (!pfnGetSemaphoreFdKHR) + { + std::cerr << "[VulkanView] vkGetSemaphoreFdKHR not found\n"; + cleanupSharedImage(); + 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_vkGlReadySemaphore; + pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &glReadyFd); + + getSemFdInfo.semaphore = m_vkVkReadySemaphore; + pfnGetSemaphoreFdKHR(m_vkDevice, &getSemFdInfo, &vkReadyFd); + + m_sharedImageInfo.memoryFd = memFd; + m_sharedImageInfo.size = memReqs.size; + m_sharedImageInfo.width = w; + m_sharedImageInfo.height = h; + m_sharedImageInfo.glReadySemaphoreFd = glReadyFd; + m_sharedImageInfo.vkReadySemaphoreFd = vkReadyFd; + + // 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); + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_vkSharedImage; + 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 = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + vkEndCommandBuffer(cb); + + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cb; + + vkResetFences(m_vkDevice, 1, &m_vkFence); + vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_vkFence); + vkWaitForFences(m_vkDevice, 1, &m_vkFence, VK_TRUE, UINT64_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_vkVkReadySemaphore; + vkQueueSubmit(m_vkQueue, 1, &signalInfo, VK_NULL_HANDLE); + + return &m_sharedImageInfo; + } + + //-------------------------------------------------------------------------- + // presentSharedImage + //-------------------------------------------------------------------------- + + void VulkanView::presentSharedImage() + { + if (!m_vkDevice || !m_vkSharedImage || !m_vkSwapchain) + return; + + // Acquire image + uint32_t imageIndex; + VkResult result = + vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, UINT64_MAX, m_vkImageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + // Swapchain out of date; recreate it next frame. Per the Vulkan spec, after an + // acquire error the semaphore is in undefined state and must not be reused. + vkDestroySemaphore(m_vkDevice, m_vkImageAvailableSemaphore, nullptr); + m_vkImageAvailableSemaphore = VK_NULL_HANDLE; + VkSemaphoreCreateInfo semaphoreInfo = {}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkImageAvailableSemaphore); + return; + } + + vkResetFences(m_vkDevice, 1, &m_vkFence); + + 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 swapchain image to transfer dst + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_vkSwapchainImages[imageIndex]; + 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 = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + // Copy shared image to swapchain image + 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 = {(uint32_t)m_sharedImageInfo.width, (uint32_t)m_sharedImageInfo.height, 1}; + + vkCmdCopyImage(cb, m_vkSharedImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_vkSwapchainImages[imageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + // Transition swapchain image to present + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = 0; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, + &barrier); + + 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 + VkSemaphore waitSemaphores[] = {m_vkGlReadySemaphore, m_vkImageAvailableSemaphore}; + VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + submitInfo.waitSemaphoreCount = 2; + submitInfo.pWaitSemaphores = waitSemaphores; + submitInfo.pWaitDstStageMask = waitStages; + + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cb; + + // Signal render finished AND vkReady (so GL can write next frame) + VkSemaphore signalSemaphores[] = {m_vkRenderFinishedSemaphore, m_vkVkReadySemaphore}; + submitInfo.signalSemaphoreCount = 2; + submitInfo.pSignalSemaphores = signalSemaphores; + + vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_vkFence); + + // Present + VkPresentInfoKHR presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &m_vkRenderFinishedSemaphore; + VkSwapchainKHR swapchains[] = {m_vkSwapchain}; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = swapchains; + presentInfo.pImageIndices = &imageIndex; + + vkQueuePresentKHR(m_vkQueue, &presentInfo); + + vkWaitForFences(m_vkDevice, 1, &m_vkFence, VK_TRUE, UINT64_MAX); + } + + //-------------------------------------------------------------------------- + // presentPixelData + //-------------------------------------------------------------------------- + + void VulkanView::presentPixelData(const void* pixels, int w, int h) + { + if (!m_vkDevice) + return; + + if (!m_vkSwapchain || m_vkSwapchainExtent.width != (uint32_t)w || m_vkSwapchainExtent.height != (uint32_t)h) + { + cleanupSwapchain(); + if (!createSwapchain()) + return; + } + + size_t size = w * h * 4; + + // Recreate staging buffer if needed + if (size > m_stagingBufferSize) + { + if (m_vkStagingBuffer) + vkDestroyBuffer(m_vkDevice, m_vkStagingBuffer, nullptr); + if (m_vkStagingBufferMemory) + vkFreeMemory(m_vkDevice, m_vkStagingBufferMemory, 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_vkStagingBuffer); + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(m_vkDevice, m_vkStagingBuffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(m_vkPhysicalDevice, memRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (allocInfo.memoryTypeIndex == UINT32_MAX) + { + std::cerr << "[VulkanView] No host-visible memory type for staging buffer\n"; + return; + } + + vkAllocateMemory(m_vkDevice, &allocInfo, nullptr, &m_vkStagingBufferMemory); + vkBindBufferMemory(m_vkDevice, m_vkStagingBuffer, m_vkStagingBufferMemory, 0); + + m_stagingBufferSize = size; + } + + // Copy to staging buffer + void* data; + vkMapMemory(m_vkDevice, m_vkStagingBufferMemory, 0, size, 0, &data); + memcpy(data, pixels, size); + vkUnmapMemory(m_vkDevice, m_vkStagingBufferMemory); + + // Acquire image + uint32_t imageIndex; + VkResult result = + vkAcquireNextImageKHR(m_vkDevice, m_vkSwapchain, UINT64_MAX, m_vkImageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + // Per the Vulkan spec, after an acquire error the semaphore is in undefined state. + vkDestroySemaphore(m_vkDevice, m_vkImageAvailableSemaphore, nullptr); + m_vkImageAvailableSemaphore = VK_NULL_HANDLE; + VkSemaphoreCreateInfo semaphoreInfo = {}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkImageAvailableSemaphore); + cleanupSwapchain(); + return; + } + + vkResetFences(m_vkDevice, 1, &m_vkFence); + + 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 + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_vkSwapchainImages[imageIndex]; + 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 = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + // 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 = {(uint32_t)w, (uint32_t)h, 1}; + + vkCmdCopyBufferToImage(cb, m_vkStagingBuffer, m_vkSwapchainImages[imageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + // Transition image to present + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = 0; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, + &barrier); + + vkEndCommandBuffer(cb); + + // Submit + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + VkSemaphore waitSemaphores[] = {m_vkImageAvailableSemaphore}; + VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = waitSemaphores; + submitInfo.pWaitDstStageMask = waitStages; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cb; + VkSemaphore signalSemaphores[] = {m_vkRenderFinishedSemaphore}; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = signalSemaphores; + + vkQueueSubmit(m_vkQueue, 1, &submitInfo, m_vkFence); + + // Present + VkPresentInfoKHR presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = signalSemaphores; + VkSwapchainKHR swapchains[] = {m_vkSwapchain}; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = swapchains; + presentInfo.pImageIndices = &imageIndex; + + vkQueuePresentKHR(m_vkQueue, &presentInfo); + + vkWaitForFences(m_vkDevice, 1, &m_vkFence, VK_TRUE, UINT64_MAX); + } + + //-------------------------------------------------------------------------- + + void VulkanView::requestUpdate() + { + // 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; + + 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); + 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. + requestUpdate(); + } + + void VulkanView::paintEvent(QPaintEvent* event) + { + 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); + 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; + } + +} // namespace Rv + +#endif // PLATFORM_LINUX