From 4826686a4d23c58cb7f3aab68f2ea76b3c347051 Mon Sep 17 00:00:00 2001 From: Matthias Janetschek Date: Wed, 7 Oct 2026 13:41:13 +0200 Subject: [PATCH] Render through public Qt API: OpenGL and Vulkan backends, --graphics-api Replace the QRhi rendering in OverlayController (private Qt API) with the render backends of the vrdashboard template (src/render/), which use public Qt API only (QQuickRenderTarget::fromOpenGLTexture / fromVulkanImage). The binary then runs on another Qt 6 patch release, e.g. the Steam Frame's system Qt. The Vulkan backend also does what SteamVR requires and the QRhi path lacked: the required instance/device extensions and the HMD's GPU (via openvr_init::renderInterop()), VK_FORMAT_R8G8B8A8_UNORM, and the transition to TRANSFER_SRC_OPTIMAL after each frame. - New option --graphics-api opengl|vulkan (default opengl, no fallback). - Shutdown() calls VR_Shutdown before the render target and Qt's device go (crash on the Steam Frame with Vulkan otherwise); it runs only once. - Mouse y is flipped on every platform: SteamVR reports it in GL convention. - Render delay 5 -> 16 ms (about 60 fps). - GuiPrivate is no longer needed. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 29 +- docs/steamframe/PROGRESS.md | 45 +++ src/main.cpp | 35 ++- src/openvr/openvr_init.cpp | 71 +++++ src/openvr/openvr_init.h | 7 + src/overlaycontroller.cpp | 176 ++++------- src/overlaycontroller.h | 28 +- src/render/OpenGLRenderBackend.cpp | 180 +++++++++++ src/render/OpenGLRenderBackend.h | 57 ++++ src/render/RenderBackend.cpp | 38 +++ src/render/RenderBackend.h | 116 ++++++++ src/render/VulkanRenderBackend.cpp | 461 +++++++++++++++++++++++++++++ src/render/VulkanRenderBackend.h | 109 +++++++ src/utils/setup.cpp | 21 +- src/utils/setup.h | 10 + 15 files changed, 1243 insertions(+), 140 deletions(-) create mode 100644 src/render/OpenGLRenderBackend.cpp create mode 100644 src/render/OpenGLRenderBackend.h create mode 100644 src/render/RenderBackend.cpp create mode 100644 src/render/RenderBackend.h create mode 100644 src/render/VulkanRenderBackend.cpp create mode 100644 src/render/VulkanRenderBackend.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e200346..20f03d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,11 +13,9 @@ set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) #find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg) #find_package(Qt${QT_VERSION_MAJOR} OPTIONAL_COMPONENTS DBus) # Core5Compat removed: nothing uses it, and the Steam Frame's system Qt -# doesn't ship it. -# GuiPrivate: QRhi (overlaycontroller.cpp) needs it. Since Qt 6.10 private -# modules must be requested explicitly. Temporary: it goes away when rendering -# switches to public API (docs/steamframe/DESIGN.md, section 3). -find_package(Qt6 REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg Qml GuiPrivate) +# doesn't ship it. No private Qt modules: the binary must run on another Qt 6 +# patch release, e.g. the Frame's system Qt (docs/steamframe/DESIGN.md). +find_package(Qt6 REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg Qml) find_package(Vulkan REQUIRED) qt_standard_project_setup(REQUIRES 6.8) @@ -69,6 +67,8 @@ set(SRCS src/utils/paths.cpp src/utils/setup.cpp src/utils/update_rate.cpp + src/render/RenderBackend.cpp + src/render/OpenGLRenderBackend.cpp ) set(HEADERS @@ -112,6 +112,8 @@ set(HEADERS src/utils/setup.h src/utils/update_rate.h src/utils/platform_capabilities.h + src/render/RenderBackend.h + src/render/OpenGLRenderBackend.h src/utils/Matrix.h src/quaternion/quaternion.h ) @@ -291,6 +293,21 @@ target_compile_definitions(AdvancedSettings PRIVATE APPLICATION_VERSION=\"${PROJECT_VERSION}\" ) +# Vulkan render backend (src/render/VulkanRenderBackend.*), selected at run +# time with --graphics-api vulkan. Needs a Qt built with Vulkan support; the +# Vulkan headers come from find_package(Vulkan) above. Nothing links against +# libvulkan: Qt loads it at run time. +if (QT_FEATURE_vulkan) + target_sources(AdvancedSettings PRIVATE + src/render/VulkanRenderBackend.cpp + src/render/VulkanRenderBackend.h + ) + target_compile_definitions(AdvancedSettings PRIVATE OVRAS_HAS_VULKAN=1) +else () + target_compile_definitions(AdvancedSettings PRIVATE OVRAS_HAS_VULKAN=0) +endif () +message(STATUS "Vulkan render backend: ${QT_FEATURE_vulkan}") + # Build for the Steam Frame (SteamOS, aarch64). Switches off what doesn't work # there and changes some defaults; the details are named capability constants # in src/utils/platform_capabilities.h, the only place reading this definition. @@ -324,8 +341,6 @@ target_link_directories(AdvancedSettings PRIVATE ${OPENVR_LIB_DIR}) target_link_libraries(AdvancedSettings PRIVATE Qt::Gui - # For QRhi; temporary, see find_package(Qt6 ...) above. - Qt::GuiPrivate Qt::Multimedia Qt::Qml Qt::Quick diff --git a/docs/steamframe/PROGRESS.md b/docs/steamframe/PROGRESS.md index bbd1ded..b2eb76b 100644 --- a/docs/steamframe/PROGRESS.md +++ b/docs/steamframe/PROGRESS.md @@ -57,3 +57,48 @@ `false`) is on. That's a chaperone-profile feature, so it belongs to R5 (iteration 2). - clang-format reflows comment blocks next to new comments. Separate new comments from existing commented-out code with a blank line, otherwise the diff grows. + +### Step 3: rendering through public API, `--graphics-api`, mouse y, shutdown order (2026-10-07) + +**Changes:** +- **`src/render/`** (new): `RenderBackend`, `OpenGLRenderBackend`, `VulkanRenderBackend` from + the template (`4a5b090`). + - Changes to the template code: `VRDASH_*` → `OVRAS_*` names, origin note in the file + header, references to `OverlayController`. + - Formatted completely in OVRAS style. + - One comment in `VulkanRenderBackend.h` was wrapped by hand, because the automatic + wrapping had broken its column layout. +- **`OverlayController`:** + - QRhi block replaced by `configureWindow` → `renderControl.initialize` → + `attachWindow` → `createRenderTarget`. + - `renderOverlay()` wraps the frame in `beginFrame`/`finishFrame`. It submits + `vrTextureBounds` + `vrTexture` while the context is current. + - `SetOverlayFromQRhiTexture`, the QRhi members and `QVulkanInstance` are removed. + - Render delay 5 → 16 ms (about 60 fps). +- **Shutdown:** `Shutdown()` runs only once. It calls `VR_Shutdown()`, then frees the + backend's render target (`releaseRenderTarget`, `detachWindow`). +- **Mouse y:** always flipped; the Linux un-flip and its TODO are removed. +- **`openvr_init::renderInterop()`:** Vulkan extensions and HMD GPU from SteamVR. +- **`main.cpp`:** parses `--graphics-api`, then `createRenderBackend` → `initialize` → + `QQuickWindow::setGraphicsApi` → `OverlayController`. +- **`setup.*`:** `--graphics-api opengl|vulkan` (default `opengl`). Unknown or not built in + → error, exit code `GENERAL_FAILURE`. +- **CMake:** + - `GuiPrivate` is gone. + - The Vulkan backend is built when `QT_FEATURE_vulkan` is set (`OVRAS_HAS_VULKAN`). + +**Results (x86_64, Qt 6.11.1):** +- The build succeeds. The Vulkan backend is built in. +- `objdump -T`: only `QVulkanDeviceFunctions::*` / `QVulkanFunctions::*` remain as + `Qt_6_PRIVATE_API`. Acceptance criterion R1 (symbols) is met. +- Warnings 1463 → 1630. The new code itself has none; the additional ones come from + `openvr.h` in the three new translation units. +- **Not tested yet:** + - running both backends, Vulkan validation, the snapshot test (step 5, with the + containers) + - mouse direction (Frame) + +**Risk:** +- `MyTextField` opens the SteamVR keyboard on `activeFocus`. The template had to send the + offscreen window an explicit `FocusIn`, otherwise `activeFocusItem` stayed null. The `qt6` + branch doesn't send it. If text fields don't respond on the Frame, that is the cause (R4). diff --git a/src/main.cpp b/src/main.cpp index 985de00..9ea3c12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "utils/setup.h" #include "settings/settings.h" #include "openvr/ovr_settings_wrapper.h" +#include "render/RenderBackend.h" #include #include @@ -46,6 +47,37 @@ int main( int argc, char* argv[] ) openvr_init::initializeOpenVR( openvr_init::OpenVrInitializationType::Overlay ); + + // Render backend (render/RenderBackend.h). Created after OpenVR, because + // the Vulkan backend needs SteamVR's device requirements + // (openvr_init::renderInterop()), and before OverlayController, because + // QQuickWindow::setGraphicsApi() must run before any QQuickWindow exists + // (OverlayController owns one). + GraphicsApi graphicsApi = GraphicsApi::OpenGL; + if ( !parseGraphicsApi( commandLineArgs.graphicsApi, &graphicsApi ) ) + { + qCritical() << "Unknown graphics API:" << commandLineArgs.graphicsApi + << "(expected opengl or vulkan)"; + return ReturnErrorCode::GENERAL_FAILURE; + } + std::unique_ptr renderBackend + = createRenderBackend( graphicsApi ); + if ( !renderBackend ) + { + qCritical() << "Graphics API" << commandLineArgs.graphicsApi + << "is not compiled into this build"; + return ReturnErrorCode::GENERAL_FAILURE; + } + QString renderBackendError; + if ( !renderBackend->initialize( openvr_init::renderInterop(), + &renderBackendError ) ) + { + qCritical() << renderBackend->name() + << "initialization failed:" << renderBackendError; + return ReturnErrorCode::GENERAL_FAILURE; + } + QQuickWindow::setGraphicsApi( renderBackend->graphicsApi() ); + try { QQmlEngine qmlEngine; @@ -61,7 +93,8 @@ int main( int argc, char* argv[] ) std::unique_ptr controller( new advsettings::OverlayController( commandLineArgs.desktopMode, commandLineArgs.forceNoSound, - qmlEngine ) ); + qmlEngine, + std::move( renderBackend ) ) ); //const auto url // = QUrl( ":/qt/qml/AdvancedSettings/src/res/qml/common/mainwidget.qml" ); QQmlComponent component( &qmlEngine, ":/qt/qml/AdvancedSettings/src/res/qml/common/mainwidget.qml" ); diff --git a/src/openvr/openvr_init.cpp b/src/openvr/openvr_init.cpp index 6f141c9..65f5384 100644 --- a/src/openvr/openvr_init.cpp +++ b/src/openvr/openvr_init.cpp @@ -6,6 +6,7 @@ #include #include #include "../openvr/ivrinput_manifest.h" +#include namespace openvr_init { @@ -136,4 +137,74 @@ void initializeOpenVR( const OpenVrInitializationType initType, int count ) } } +// Calls an OpenVR "Get...ExtensionsRequired" function and splits its +// space-separated result. `query( buffer, size )` returns the size needed, +// including the terminating zero. +template +static QByteArrayList queryExtensionList( Query query ) +{ + const uint32_t size = query( nullptr, 0 ); + if ( size == 0 ) + { + return {}; + } + QByteArray buffer( static_cast( size ), '\0' ); + query( buffer.data(), size ); + QByteArrayList result; + for ( const QByteArray& ext : + buffer.left( buffer.indexOf( '\0' ) ).split( ' ' ) ) + { + if ( !ext.isEmpty() ) + { + result.append( ext ); + } + } + return result; +} + +// Same as the template's VrRuntime::renderInterop(). The functions are called +// by the Vulkan backend while it creates its instance and device. +RenderInterop renderInterop() +{ + RenderInterop interop; + interop.vulkanInstanceExtensions = [] + { + vr::IVRCompositor* compositor = vr::VRCompositor(); + if ( !compositor ) + { + qWarning() << "IVRCompositor unavailable: cannot query required " + "Vulkan instance extensions"; + return QByteArrayList(); + } + return queryExtensionList( + [&]( char* buffer, uint32_t size ) + { + return compositor->GetVulkanInstanceExtensionsRequired( buffer, + size ); + } ); + }; + interop.vulkanDeviceExtensions = []( VkPhysicalDevice_T* device ) + { + vr::IVRCompositor* compositor = vr::VRCompositor(); + if ( !compositor ) + { + return QByteArrayList(); + } + return queryExtensionList( + [&]( char* buffer, uint32_t size ) + { + return compositor->GetVulkanDeviceExtensionsRequired( + device, buffer, size ); + } ); + }; + interop.vulkanOutputDevice = []( VkInstance_T* instance ) + { + uint64_t device = 0; + vr::VRSystem()->GetOutputDevice( + &device, vr::TextureType_Vulkan, instance ); + return reinterpret_cast( device ); + }; + return interop; +} + } // namespace openvr_init diff --git a/src/openvr/openvr_init.h b/src/openvr/openvr_init.h index 0a4faaf..6dc7465 100644 --- a/src/openvr/openvr_init.h +++ b/src/openvr/openvr_init.h @@ -1,5 +1,7 @@ #pragma once +#include "../render/RenderBackend.h" + namespace openvr_init { enum class OpenVrInitializationType @@ -10,4 +12,9 @@ enum class OpenVrInitializationType void initializeOpenVR( const OpenVrInitializationType initType, int count = 0 ); +// What SteamVR requires from the render backend's graphics device: the +// Vulkan instance/device extensions and the GPU driving the HMD (Vulkan only; +// OpenGL ignores it). Requires an initialized OpenVR connection. +RenderInterop renderInterop(); + } // namespace openvr_init diff --git a/src/overlaycontroller.cpp b/src/overlaycontroller.cpp index 0aef604..6201430 100644 --- a/src/overlaycontroller.cpp +++ b/src/overlaycontroller.cpp @@ -28,12 +28,7 @@ #include #include #include -#include #include -// Direct3D exists only on Windows. -#ifdef _WIN32 -# include -#endif // application namespace namespace advsettings @@ -52,10 +47,13 @@ int verifyCustomTickRate( const int tickRate ) return tickRate; } -OverlayController::OverlayController( bool desktopMode, - bool noSound, - QQmlEngine& qmlEngine ) - : QObject(), m_desktopMode( desktopMode ), m_noSound( noSound ), +OverlayController::OverlayController( + bool desktopMode, + bool noSound, + QQmlEngine& qmlEngine, + std::unique_ptr renderBackend ) + : QObject(), m_renderBackend( std::move( renderBackend ) ), + m_desktopMode( desktopMode ), m_noSound( noSound ), m_verifiedCustomTickRateMs( verifyCustomTickRate( settings::getSetting( settings::IntSetting::APPLICATION_customTickRateMs ) ) ), m_actions(), m_alarm() @@ -331,8 +329,16 @@ void OverlayController::exitApp() // Does not fallthrough } +// Stops ticking and rendering, disconnects from SteamVR and frees the render +// target. Called by exitApp() and by the destructor; runs only once. void OverlayController::Shutdown() { + if ( m_shutdownDone ) + { + return; + } + m_shutdownDone = true; + disconnect( &m_pumpEventsTimer, SIGNAL( timeout() ), this, @@ -356,9 +362,21 @@ void OverlayController::Shutdown() m_pRenderTimer->stop(); m_pRenderTimer.reset(); } - m_render_pass_descriptor.reset(); - m_render_target.reset(); - m_pFBTexture.reset(); + + // Order: OpenVR shuts down while the graphics device still exists, then + // the render target goes. OpenVR's client holds objects on the device of + // the submitted textures; destroying the device first crashed on the + // Steam Frame with Vulkan (openvr.h: call VR_Shutdown before destroying + // these resources). OVRAS ends with exit(), so this is the only place + // where VR_Shutdown runs in overlay mode. + vr::VR_Shutdown(); + if ( m_renderBackend && m_renderBackend->makeCurrent() ) + { + // Frees what the backend created on Qt's device/context, before Qt + // destroys it together with m_renderControl. + m_renderBackend->releaseRenderTarget(); + m_renderBackend->detachWindow(); + } } void OverlayController::SetWidget( QQuickItem* quickItem, @@ -407,52 +425,31 @@ void OverlayController::SetWidget( QQuickItem* quickItem, m_pRenderTimer.reset( new QTimer() ); m_pRenderTimer->moveToThread( qApp->thread() ); m_pRenderTimer->setSingleShot( true ); - m_pRenderTimer->setInterval( 5 ); + // 16 ms: a frame starts at most 16 ms after a change, about 60 fps. + m_pRenderTimer->setInterval( 16 ); connect( m_pRenderTimer.get(), SIGNAL( timeout() ), this, SLOT( renderOverlay() ) ); -#if QT_CONFIG( vulkan ) - if ( m_window.graphicsApi() == QSGRendererInterface::Vulkan ) - { - m_vulkanInstance.reset( new QVulkanInstance ); - m_vulkanInstance->setExtensions( - QQuickGraphicsConfiguration::preferredInstanceExtensions() ); - if ( !m_vulkanInstance->create() ) - throw std::runtime_error( "Cannot create vulkan instance" ); - - m_window.setVulkanInstance( m_vulkanInstance.get() ); - } -#endif - + // Render target through the render backend (public Qt API only). + // Call order: see render/RenderBackend.h. + m_renderBackend->configureWindow( &m_window ); + if ( !m_renderBackend->makeCurrent() ) + throw std::runtime_error( "render backend: makeCurrent failed" ); if ( !m_renderControl.initialize() ) throw std::runtime_error( "could not initialize m_renderControl" ); - QRhi* rhi = this->rhi(); + QString renderError; + if ( !m_renderBackend->attachWindow( &m_window, &renderError ) ) + throw std::runtime_error( renderError.toStdString() ); + qInfo() << "Rendering with" << m_renderBackend->name() << "on" + << m_renderBackend->deviceName(); - qInfo() << "Started with" << rhi->backendName(); - - m_pFBTexture.reset( rhi->newTexture( - QRhiTexture::RGBA8, - quickItem->size().toSize(), - 1, - QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource ) ); - if ( !m_pFBTexture->create() ) - throw std::runtime_error( "RhiTexture not created" ); - - m_render_target.reset( - rhi->newTextureRenderTarget( { m_pFBTexture.get() } ) ); - m_render_pass_descriptor.reset( - m_render_target->newCompatibleRenderPassDescriptor() ); - m_render_target->setRenderPassDescriptor( - m_render_pass_descriptor.get() ); - if ( !m_render_target->create() ) - throw std::runtime_error( "failed to create render target" ); - - auto qqrt - = QQuickRenderTarget::fromRhiRenderTarget( m_render_target.get() ); - - m_window.setRenderTarget( qqrt ); + const QQuickRenderTarget renderTarget + = m_renderBackend->createRenderTarget( quickItem->size().toSize() ); + if ( renderTarget.isNull() ) + throw std::runtime_error( "could not create render target" ); + m_window.setRenderTarget( renderTarget ); quickItem->setParentItem( m_window.contentItem() ); m_window.setGeometry( 0, 0, @@ -551,62 +548,6 @@ void OverlayController::OnRenderRequest() } } -void OverlayController::SetOverlayFromQRhiTexture( QRhiTexture& tex ) -{ - vr::Texture_t vrTex = {}; - QRhi* rhi = this->rhi(); - - switch ( rhi->backend() ) - { - case QRhi::OpenGLES2: - { - vrTex.handle = reinterpret_cast( tex.nativeTexture().object ); - vrTex.eType = vr::TextureType_OpenGL; - vrTex.eColorSpace = vr::ColorSpace_Auto; - } - break; -// Direct3D exists only on Windows. -#ifdef _WIN32 - case QRhi::D3D11: - { - vrTex.handle = reinterpret_cast( tex.nativeTexture().object ); - vrTex.eType = vr::TextureType_DirectX; - vrTex.eColorSpace = vr::ColorSpace_Auto; - - } - break; -#endif -#if QT_CONFIG( vulkan ) - case QRhi::Vulkan: - { - const QRhiVulkanNativeHandles* vulkan_handles - = reinterpret_cast( - rhi->nativeHandles() ); - vr::VRVulkanTextureData_t vulkan = { - .m_nImage = tex.nativeTexture().object, - .m_pDevice = vulkan_handles->dev, - .m_pPhysicalDevice = vulkan_handles->physDev, - .m_pInstance = vulkan_handles->inst->vkInstance(), - .m_pQueue = vulkan_handles->gfxQueue, - .m_nQueueFamilyIndex = vulkan_handles->gfxQueueIdx, - .m_nWidth = static_cast( tex.pixelSize().width() ), - .m_nHeight = static_cast( tex.pixelSize().height() ), - .m_nFormat = static_cast(tex.format()), - .m_nSampleCount = static_cast( tex.sampleCount() ), - }; - vrTex.handle = &vulkan; - vrTex.eType = vr::TextureType_Vulkan; - vrTex.eColorSpace = vr::ColorSpace_Auto; - } - break; -#endif - default: - qFatal() << "unimplemented backend:" << rhi->backend(); - throw std::runtime_error( "unimplemented backend" ); - } - vr::VROverlay()->SetOverlayTexture( m_ulOverlayHandle, &vrTex ); -} - void OverlayController::renderOverlay() { if ( !m_desktopMode ) @@ -617,13 +558,24 @@ void OverlayController::renderOverlay() && !vr::VROverlay()->IsOverlayVisible( m_ulOverlayThumbnailHandle ) ) ) return; + // OpenGL: the context must be current while rendering and while + // handing the texture to SteamVR. + if ( !m_renderBackend->makeCurrent() ) + return; + // The backends put the texture into the state Qt expects + // (beginFrame) and the one SteamVR expects (finishFrame). + m_renderBackend->beginFrame(); m_renderControl.polishItems(); m_renderControl.beginFrame(); m_renderControl.sync(); m_renderControl.render(); m_renderControl.endFrame(); + m_renderBackend->finishFrame(); - SetOverlayFromQRhiTexture( *m_pFBTexture ); + vr::VRTextureBounds_t bounds = m_renderBackend->vrTextureBounds(); + vr::VROverlay()->SetOverlayTextureBounds( m_ulOverlayHandle, &bounds ); + vr::Texture_t texture = m_renderBackend->vrTexture(); + vr::VROverlay()->SetOverlayTexture( m_ulOverlayHandle, &texture ); } } @@ -643,12 +595,10 @@ bool OverlayController::pollNextEvent( vr::VROverlayHandle_t ulOverlayHandle, QPointF OverlayController::getMousePositionForEvent( vr::VREvent_Mouse_t mouse ) { - //TODO temp mouse is negative?? - float y = m_window.height() - mouse.y; -#ifdef __linux__ - float h = static_cast( m_window.height() ); - y = h - y; -#endif + // SteamVR reports mouse coordinates in GL convention (y counted from the + // bottom) on every platform and for every graphics API (verified on the + // Steam Frame with OpenGL and Vulkan); Qt counts y from the top. + float y = static_cast( m_window.height() ) - mouse.y; return { mouse.x, y }; } diff --git a/src/overlaycontroller.h b/src/overlaycontroller.h index f9568e9..a53ecd5 100644 --- a/src/overlaycontroller.h +++ b/src/overlaycontroller.h @@ -25,7 +25,7 @@ #include #include #include -#include +#include "render/RenderBackend.h" #include "openvr/openvr_init.h" @@ -108,16 +108,17 @@ private: vr::VROverlayHandle_t m_ulOverlayThumbnailHandle = vr::k_ulOverlayHandleInvalid; -#if QT_CONFIG( vulkan ) - std::unique_ptr m_vulkanInstance = nullptr; -#endif + // Renders the QML scene into a texture for SteamVR (OpenGL or Vulkan), + // using public Qt API only. Declared before m_renderControl and m_window: + // Qt's device/context is created from the backend's context/instance, so + // the backend must be destroyed after them. + std::unique_ptr m_renderBackend; QQuickRenderControl m_renderControl; QQuickWindow m_window{ &m_renderControl }; std::optional m_cached_vr_texture_type; - std::unique_ptr m_pFBTexture; - std::unique_ptr m_render_target; - std::unique_ptr m_render_pass_descriptor; + // Set by Shutdown(), which must run only once (see there). + bool m_shutdownDone = false; /* std::unique_ptr m_pFbo; @@ -194,11 +195,6 @@ private: void processRotationBindings(); void processExclusiveInputBinding(); - inline QRhi* rhi() - { - return m_renderControl.rhi(); - } - void applyTickInterval(); bool m_exclusiveState = false; @@ -206,8 +202,12 @@ private: bool m_keyPressTwoState = false; public: - void SetOverlayFromQRhiTexture( QRhiTexture& tex ); - OverlayController( bool desktopMode, bool noSound, QQmlEngine& qmlEngine ); + // renderBackend: initialized, and its graphics API already set with + // QQuickWindow::setGraphicsApi() (must happen before m_window exists). + OverlayController( bool desktopMode, + bool noSound, + QQmlEngine& qmlEngine, + std::unique_ptr renderBackend ); virtual ~OverlayController(); void Shutdown(); diff --git a/src/render/OpenGLRenderBackend.cpp b/src/render/OpenGLRenderBackend.cpp new file mode 100644 index 0000000..f8f9781 --- /dev/null +++ b/src/render/OpenGLRenderBackend.cpp @@ -0,0 +1,180 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#include "OpenGLRenderBackend.h" + +#include +#include +#include +#include +#include + +OpenGLRenderBackend::~OpenGLRenderBackend() +{ + releaseRenderTarget(); +} + +bool OpenGLRenderBackend::initialize( const RenderInterop&, QString* error ) +{ + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setDepthBufferSize( 24 ); + format.setStencilBufferSize( 8 ); + + m_context = std::make_unique(); + m_context->setFormat( format ); + if ( !m_context->create() ) + { + if ( error ) + *error = QStringLiteral( "Failed to create OpenGL context" ); + return false; + } + + m_surface = std::make_unique(); + m_surface->setFormat( m_context->format() ); + m_surface->create(); + if ( !m_surface->isValid() ) + { + if ( error ) + *error = QStringLiteral( "Failed to create offscreen surface" ); + return false; + } + return true; +} + +void OpenGLRenderBackend::configureWindow( QQuickWindow* window ) +{ + window->setGraphicsDevice( + QQuickGraphicsDevice::fromOpenGLContext( m_context.get() ) ); +} + +bool OpenGLRenderBackend::attachWindow( QQuickWindow*, QString* error ) +{ + if ( !makeCurrent() ) + { + *error = QStringLiteral( "makeCurrent failed" ); + return false; + } + QOpenGLFunctions* f = m_context->functions(); + m_deviceName = QStringLiteral( "%1 (%2)" ) + .arg( QString::fromLatin1( reinterpret_cast( + f->glGetString( GL_RENDERER ) ) ), + QString::fromLatin1( reinterpret_cast( + f->glGetString( GL_VERSION ) ) ) ); + return true; +} + +bool OpenGLRenderBackend::makeCurrent() +{ + return m_context && m_context->makeCurrent( m_surface.get() ); +} + +void OpenGLRenderBackend::doneCurrent() +{ + if ( m_context ) + m_context->doneCurrent(); +} + +QQuickRenderTarget OpenGLRenderBackend::createRenderTarget( QSize size ) +{ + releaseRenderTarget(); + if ( !makeCurrent() ) + return {}; + + QOpenGLFunctions* f = m_context->functions(); + GLuint texture = 0; + f->glGenTextures( 1, &texture ); + f->glBindTexture( GL_TEXTURE_2D, texture ); + f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + f->glTexImage2D( GL_TEXTURE_2D, + 0, + GL_RGBA8, + size.width(), + size.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr ); + f->glBindTexture( GL_TEXTURE_2D, 0 ); + + m_texture = texture; + m_size = size; + return QQuickRenderTarget::fromOpenGLTexture( m_texture, size ); +} + +void OpenGLRenderBackend::releaseRenderTarget() +{ + if ( m_texture && makeCurrent() ) + { + GLuint texture = m_texture; + m_context->functions()->glDeleteTextures( 1, &texture ); + } + m_texture = 0; + m_size = {}; +} + +void OpenGLRenderBackend::finishFrame() +{ + // SteamVR reads the texture from another process; make sure all + // rendering commands have been submitted before the handle is passed + // on. + m_context->functions()->glFlush(); +} + +void OpenGLRenderBackend::waitForGpu() +{ + if ( makeCurrent() ) + m_context->functions()->glFinish(); +} + +vr::Texture_t OpenGLRenderBackend::vrTexture() +{ + // For TextureType_OpenGL the handle *is* the GL texture name, cast to a + // pointer. + return vr::Texture_t{ reinterpret_cast( + static_cast( m_texture ) ), + vr::TextureType_OpenGL, + vr::ColorSpace_Auto }; +} + +vr::VRTextureBounds_t OpenGLRenderBackend::vrTextureBounds() const +{ + // Qt Quick renders into GL textures bottom-up (GL convention) and SteamVR + // treats TextureType_OpenGL accordingly: no flip needed (verified on the + // Steam Frame). + return vr::VRTextureBounds_t{ 0.f, 0.f, 1.f, 1.f }; +} + +QImage OpenGLRenderBackend::readback() +{ + if ( !m_texture || !makeCurrent() ) + return {}; + + QOpenGLFunctions* f = m_context->functions(); + GLuint fbo = 0; + f->glGenFramebuffers( 1, &fbo ); + f->glBindFramebuffer( GL_FRAMEBUFFER, fbo ); + f->glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0 ); + + QImage image( m_size, QImage::Format_RGBA8888_Premultiplied ); + f->glReadPixels( 0, + 0, + m_size.width(), + m_size.height(), + GL_RGBA, + GL_UNSIGNED_BYTE, + image.bits() ); + + f->glBindFramebuffer( GL_FRAMEBUFFER, + m_context->defaultFramebufferObject() ); + f->glDeleteFramebuffers( 1, &fbo ); + // GL rows are bottom-up. +#if QT_VERSION >= QT_VERSION_CHECK( 6, 9, 0 ) + return image.flipped( Qt::Vertical ); +#else + return image.mirrored( false, true ); +#endif +} diff --git a/src/render/OpenGLRenderBackend.h b/src/render/OpenGLRenderBackend.h new file mode 100644 index 0000000..ea421f7 --- /dev/null +++ b/src/render/OpenGLRenderBackend.h @@ -0,0 +1,57 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#pragma once + +#include +#include + +#include "RenderBackend.h" + +class OpenGLRenderBackend final : public RenderBackend +{ +public: + OpenGLRenderBackend() = default; + ~OpenGLRenderBackend() override; + + QString name() const override + { + return QStringLiteral( "OpenGL" ); + } + QSGRendererInterface::GraphicsApi graphicsApi() const override + { + return QSGRendererInterface::OpenGL; + } + + bool initialize( const RenderInterop& interop, QString* error ) override; + void configureWindow( QQuickWindow* window ) override; + bool attachWindow( QQuickWindow* window, QString* error ) override; + void detachWindow() override + { + releaseRenderTarget(); + } + QString deviceName() const override + { + return m_deviceName; + } + + bool makeCurrent() override; + void doneCurrent() override; + + QQuickRenderTarget createRenderTarget( QSize size ) override; + void releaseRenderTarget() override; + void beginFrame() override {} + void finishFrame() override; + void waitForGpu() override; + + vr::Texture_t vrTexture() override; + vr::VRTextureBounds_t vrTextureBounds() const override; + QImage readback() override; + +private: + std::unique_ptr m_context; + std::unique_ptr m_surface; + unsigned int m_texture = 0; // GLuint + QSize m_size; + QString m_deviceName; +}; diff --git a/src/render/RenderBackend.cpp b/src/render/RenderBackend.cpp new file mode 100644 index 0000000..cabfe3e --- /dev/null +++ b/src/render/RenderBackend.cpp @@ -0,0 +1,38 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#include "RenderBackend.h" + +#include "OpenGLRenderBackend.h" +#if OVRAS_HAS_VULKAN +# include "VulkanRenderBackend.h" +#endif + +std::unique_ptr createRenderBackend( GraphicsApi api ) +{ + switch ( api ) + { + case GraphicsApi::OpenGL: + return std::make_unique(); + case GraphicsApi::Vulkan: +#if OVRAS_HAS_VULKAN + return std::make_unique(); +#else + return nullptr; // built without Vulkan support +#endif + } + return nullptr; +} + +bool parseGraphicsApi( const QString& name, GraphicsApi* api ) +{ + const QString lower = name.toLower(); + if ( lower == QLatin1String( "opengl" ) || lower == QLatin1String( "gl" ) ) + *api = GraphicsApi::OpenGL; + else if ( lower == QLatin1String( "vulkan" ) + || lower == QLatin1String( "vk" ) ) + *api = GraphicsApi::Vulkan; + else + return false; + return true; +} diff --git a/src/render/RenderBackend.h b/src/render/RenderBackend.h new file mode 100644 index 0000000..350462e --- /dev/null +++ b/src/render/RenderBackend.h @@ -0,0 +1,116 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class QQuickWindow; + +// Requirements the VR runtime imposes on the graphics device. All optional: +// without them (snapshot mode, tests) backends pick defaults. +struct RenderInterop +{ + std::function vulkanInstanceExtensions; + std::function vulkanDeviceExtensions; + // The physical device driving the HMD, for a given VkInstance. + std::function vulkanOutputDevice; +}; + +// Abstraction over the graphics API used to render the Qt Quick scene into a +// texture that SteamVR can consume. Everything API-specific (context/device +// setup, texture allocation, layout transitions, handing the texture to OpenVR, +// CPU readback) lives behind this interface; OverlayController and the +// VR/input code stay API-agnostic. +// +// Call order (driven by OverlayController): +// initialize → configureWindow → [QQuickRenderControl::initialize] → +// attachWindow → createRenderTarget → per frame: beginFrame … [Qt renders] … +// finishFrame → vrTexture → releaseRenderTarget → detachWindow → [Qt destroys +// its device/context] +class RenderBackend +{ +public: + virtual ~RenderBackend() = default; + + virtual QString name() const = 0; + + // Must be applied via QQuickWindow::setGraphicsApi() before any + // QQuickWindow exists. + virtual QSGRendererInterface::GraphicsApi graphicsApi() const = 0; + + // Creates API-level objects (context / instance). Requires a + // QGuiApplication. + virtual bool initialize( const RenderInterop& interop, QString* error ) = 0; + + // Tells Qt Quick which device/context/instance to use. Before + // QQuickRenderControl::initialize(). + virtual void configureWindow( QQuickWindow* window ) = 0; + + // Picks up objects Qt created during QQuickRenderControl::initialize() + // (e.g. VkDevice). + virtual bool attachWindow( QQuickWindow* window, QString* error ) = 0; + // Releases everything created on Qt's device; called before Qt destroys + // it. + virtual void detachWindow() = 0; + + // Human-readable GPU/driver description (valid after attachWindow). + virtual QString deviceName() const = 0; + + // Bind the API context to the current thread (no-op for APIs without + // one). + virtual bool makeCurrent() = 0; + virtual void doneCurrent() = 0; + + // (Re)allocates the shared color texture; returns the target for + // QQuickWindow::setRenderTarget. + virtual QQuickRenderTarget createRenderTarget( QSize size ) = 0; + // Frees the texture. Called before Qt's device/context goes away. + virtual void releaseRenderTarget() = 0; + + // Before Qt renders a frame / after QQuickRenderControl::endFrame(). + // Backends put the texture into the state Qt resp. SteamVR expects + // (layouts, flushes). + virtual void beginFrame() = 0; + virtual void finishFrame() = 0; + + // Blocks until the GPU has finished all submitted work (benchmarks: + // makes the asynchronous OpenGL path comparable to the synchronous + // Vulkan one). + virtual void waitForGpu() = 0; + + // Texture description for IVROverlay::SetOverlayTexture. Only valid + // while the render target exists; must be submitted with the context + // current (see makeCurrent()). + virtual vr::Texture_t vrTexture() = 0; + + // UV bounds for IVROverlay::SetOverlayTextureBounds (compensates + // API-specific origin). + virtual vr::VRTextureBounds_t vrTextureBounds() const = 0; + + // Reads the last finished frame back to the CPU (snapshots, tests). + // Slow. + virtual QImage readback() = 0; +}; + +enum class GraphicsApi +{ + OpenGL, + Vulkan +}; + +// Returns nullptr if the API was not compiled in (Vulkan is optional, see +// OVRAS_HAS_VULKAN). +std::unique_ptr createRenderBackend( GraphicsApi api ); + +// "opengl" / "vulkan" (case-insensitive). Returns false for unknown names. +bool parseGraphicsApi( const QString& name, GraphicsApi* api ); diff --git a/src/render/VulkanRenderBackend.cpp b/src/render/VulkanRenderBackend.cpp new file mode 100644 index 0000000..170c71b --- /dev/null +++ b/src/render/VulkanRenderBackend.cpp @@ -0,0 +1,461 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#include "VulkanRenderBackend.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr VkFormat kFormat = VK_FORMAT_R8G8B8A8_UNORM; +} + +VulkanRenderBackend::~VulkanRenderBackend() +{ + // The VkDevice belongs to Qt Quick; everything we created on it must + // already be gone. + Q_ASSERT( m_commandPool == VK_NULL_HANDLE && m_image == VK_NULL_HANDLE ); +} + +void VulkanRenderBackend::detachWindow() +{ + releaseRenderTarget(); // waits for the device to be idle + if ( m_df ) + { + for ( Submission* submission : { &m_restore, &m_handoff } ) + { + if ( submission->fence != VK_NULL_HANDLE ) + m_df->vkDestroyFence( m_device, submission->fence, nullptr ); + *submission = {}; + } + // Destroying the pool also frees its command buffers. + if ( m_commandPool != VK_NULL_HANDLE ) + m_df->vkDestroyCommandPool( m_device, m_commandPool, nullptr ); + } + m_commandPool = VK_NULL_HANDLE; + m_df = nullptr; + m_device = VK_NULL_HANDLE; + m_queue = VK_NULL_HANDLE; +} + +bool VulkanRenderBackend::initialize( const RenderInterop& interop, + QString* error ) +{ + QByteArrayList instanceExtensions + = QQuickGraphicsConfiguration::preferredInstanceExtensions(); + if ( interop.vulkanInstanceExtensions ) + { + for ( const QByteArray& ext : interop.vulkanInstanceExtensions() ) + { + if ( !instanceExtensions.contains( ext ) ) + instanceExtensions.append( ext ); + } + } + + m_instance.setExtensions( instanceExtensions ); + // QVulkanInstance defaults to Vulkan 1.0; request what the loader + // supports (capped at 1.3) so Qt can rely on core 1.1+ features instead + // of half-enabled extensions. + const QVersionNumber supported = m_instance.supportedApiVersion(); + m_instance.setApiVersion( qMin( supported, QVersionNumber( 1, 3 ) ) ); + if ( qEnvironmentVariableIntValue( "OVRAS_VULKAN_VALIDATION" ) ) + m_instance.setLayers( { "VK_LAYER_KHRONOS_validation" } ); + if ( !m_instance.create() ) + { + *error + = QStringLiteral( "Failed to create Vulkan instance (VkResult %1)" ) + .arg( m_instance.errorCode() ); + return false; + } + + // Use the GPU SteamVR renders the HMD with; otherwise Qt picks one. + if ( interop.vulkanOutputDevice ) + m_physicalDevice + = interop.vulkanOutputDevice( m_instance.vkInstance() ); + if ( m_physicalDevice != VK_NULL_HANDLE && interop.vulkanDeviceExtensions ) + m_deviceExtensions = interop.vulkanDeviceExtensions( m_physicalDevice ); + return true; +} + +void VulkanRenderBackend::configureWindow( QQuickWindow* window ) +{ + window->setVulkanInstance( &m_instance ); + if ( m_physicalDevice != VK_NULL_HANDLE ) + window->setGraphicsDevice( + QQuickGraphicsDevice::fromPhysicalDevice( m_physicalDevice ) ); + QQuickGraphicsConfiguration config = window->graphicsConfiguration(); + config.setDeviceExtensions( m_deviceExtensions ); + window->setGraphicsConfiguration( config ); +} + +bool VulkanRenderBackend::attachWindow( QQuickWindow* window, QString* error ) +{ + QSGRendererInterface* rif = window->rendererInterface(); + auto resource = [&]( QSGRendererInterface::Resource r ) + { return rif->getResource( window, r ); }; + + auto* device = static_cast( + resource( QSGRendererInterface::DeviceResource ) ); + auto* physicalDevice = static_cast( + resource( QSGRendererInterface::PhysicalDeviceResource ) ); + auto* queue = static_cast( + resource( QSGRendererInterface::CommandQueueResource ) ); + auto* family = static_cast( + resource( QSGRendererInterface::GraphicsQueueFamilyIndexResource ) ); + if ( !device || !physicalDevice || !queue || !family ) + { + *error = QStringLiteral( + "Qt Quick did not expose its Vulkan device objects" ); + return false; + } + m_device = *device; + m_physicalDevice = *physicalDevice; + m_queue = *queue; + m_queueFamilyIndex = *family; + m_df = m_instance.deviceFunctions( m_device ); + + VkPhysicalDeviceProperties props{}; + m_instance.functions()->vkGetPhysicalDeviceProperties( m_physicalDevice, + &props ); + m_deviceName = QStringLiteral( "%1 (Vulkan %2.%3.%4)" ) + .arg( QString::fromUtf8( props.deviceName ) ) + .arg( VK_API_VERSION_MAJOR( props.apiVersion ) ) + .arg( VK_API_VERSION_MINOR( props.apiVersion ) ) + .arg( VK_API_VERSION_PATCH( props.apiVersion ) ); + + VkCommandPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + poolInfo.queueFamilyIndex = m_queueFamilyIndex; + if ( m_df->vkCreateCommandPool( + m_device, &poolInfo, nullptr, &m_commandPool ) + != VK_SUCCESS ) + { + *error = QStringLiteral( "vkCreateCommandPool failed" ); + return false; + } + for ( Submission* submission : { &m_restore, &m_handoff } ) + { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = m_commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + if ( m_df->vkAllocateCommandBuffers( + m_device, &allocInfo, &submission->commandBuffer ) + != VK_SUCCESS + || m_df->vkCreateFence( + m_device, &fenceInfo, nullptr, &submission->fence ) + != VK_SUCCESS ) + { + *error + = QStringLiteral( "Allocating command buffers/fences failed" ); + return false; + } + } + return true; +} + +bool VulkanRenderBackend::findMemoryType( uint32_t typeBits, + VkMemoryPropertyFlags properties, + uint32_t* index ) const +{ + VkPhysicalDeviceMemoryProperties memProps{}; + m_instance.functions()->vkGetPhysicalDeviceMemoryProperties( + m_physicalDevice, &memProps ); + for ( uint32_t i = 0; i < memProps.memoryTypeCount; ++i ) + { + if ( ( typeBits & ( 1u << i ) ) + && ( memProps.memoryTypes[i].propertyFlags & properties ) + == properties ) + { + *index = i; + return true; + } + } + return false; +} + +QQuickRenderTarget VulkanRenderBackend::createRenderTarget( QSize size ) +{ + releaseRenderTarget(); + if ( !m_df ) + return {}; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = kFormat; + imageInfo.extent + = { uint32_t( size.width() ), uint32_t( size.height() ), 1 }; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + // COLOR_ATTACHMENT for Qt; TRANSFER_SRC | SAMPLED are required by + // OpenVR. + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + | VK_IMAGE_USAGE_TRANSFER_SRC_BIT + | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if ( m_df->vkCreateImage( m_device, &imageInfo, nullptr, &m_image ) + != VK_SUCCESS ) + return {}; + + VkMemoryRequirements req{}; + m_df->vkGetImageMemoryRequirements( m_device, m_image, &req ); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = req.size; + if ( !findMemoryType( req.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &allocInfo.memoryTypeIndex ) + || m_df->vkAllocateMemory( + m_device, &allocInfo, nullptr, &m_imageMemory ) + != VK_SUCCESS + || m_df->vkBindImageMemory( m_device, m_image, m_imageMemory, 0 ) + != VK_SUCCESS ) + { + releaseRenderTarget(); + return {}; + } + + m_size = size; + m_layout = VK_IMAGE_LAYOUT_UNDEFINED; + // Qt transitions UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL in the first + // frame. + return QQuickRenderTarget::fromVulkanImage( + m_image, VK_IMAGE_LAYOUT_UNDEFINED, kFormat, size ); +} + +void VulkanRenderBackend::releaseRenderTarget() +{ + if ( !m_df ) + return; + m_df->vkDeviceWaitIdle( m_device ); + if ( m_image != VK_NULL_HANDLE ) + m_df->vkDestroyImage( m_device, m_image, nullptr ); + if ( m_imageMemory != VK_NULL_HANDLE ) + m_df->vkFreeMemory( m_device, m_imageMemory, nullptr ); + m_image = VK_NULL_HANDLE; + m_imageMemory = VK_NULL_HANDLE; + m_layout = VK_IMAGE_LAYOUT_UNDEFINED; + m_size = {}; +} + +bool VulkanRenderBackend::waitFor( Submission& submission ) +{ + if ( !submission.pending ) + return true; + submission.pending = false; + const bool ok = m_df->vkWaitForFences( + m_device, 1, &submission.fence, VK_TRUE, UINT64_MAX ) + == VK_SUCCESS; + m_df->vkResetFences( m_device, 1, &submission.fence ); + return ok; +} + +bool VulkanRenderBackend::submit( + Submission& submission, + const std::function& record, + bool wait ) +{ + if ( !waitFor( submission ) ) // the command buffer may still be in use + return false; + m_df->vkResetCommandBuffer( submission.commandBuffer, 0 ); + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + m_df->vkBeginCommandBuffer( submission.commandBuffer, &beginInfo ); + record( submission.commandBuffer ); + m_df->vkEndCommandBuffer( submission.commandBuffer ); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &submission.commandBuffer; + if ( m_df->vkQueueSubmit( m_queue, 1, &submitInfo, submission.fence ) + != VK_SUCCESS ) + return false; + submission.pending = true; + return wait ? waitFor( submission ) : true; +} + +void VulkanRenderBackend::transitionImage( Submission& submission, + VkImageLayout from, + VkImageLayout to, + bool wait ) +{ + // Only two transitions exist: rendered -> hand-off and back. + const bool toTransfer = to == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkAccessFlags colorWrite = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + const VkAccessFlags transferRead = VK_ACCESS_TRANSFER_READ_BIT; + const VkPipelineStageFlags colorStage + = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + const VkPipelineStageFlags transferStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = toTransfer ? colorWrite : transferRead; + barrier.dstAccessMask = toTransfer ? transferRead : colorWrite; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_image; + barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + const VkPipelineStageFlags srcStage + = toTransfer ? colorStage : transferStage; + const VkPipelineStageFlags dstStage + = toTransfer ? transferStage : colorStage; + + submit( + submission, + [&]( VkCommandBuffer cb ) + { + m_df->vkCmdPipelineBarrier( cb, + srcStage, + dstStage, + 0, + 0, + nullptr, + 0, + nullptr, + 1, + &barrier ); + }, + wait ); + m_layout = to; +} + +void VulkanRenderBackend::beginFrame() +{ + // Hand the image back to Qt in the layout its tracking assumes (after + // the first frame). No CPU wait needed: Qt's frame is submitted to the + // same queue afterwards. + if ( m_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ) + transitionImage( m_restore, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + false ); +} + +void VulkanRenderBackend::finishFrame() +{ + // Qt leaves the render target in COLOR_ATTACHMENT_OPTIMAL; OpenVR wants + // TRANSFER_SRC_OPTIMAL. Wait, so the texture is complete and in the + // right layout when SteamVR gets it. + transitionImage( m_handoff, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + true ); +} + +void VulkanRenderBackend::waitForGpu() +{ + if ( m_df ) + m_df->vkQueueWaitIdle( m_queue ); +} + +vr::Texture_t VulkanRenderBackend::vrTexture() +{ + m_vrTextureData.m_nImage = reinterpret_cast( m_image ); + m_vrTextureData.m_pDevice = m_device; + m_vrTextureData.m_pPhysicalDevice = m_physicalDevice; + m_vrTextureData.m_pInstance = m_instance.vkInstance(); + m_vrTextureData.m_pQueue = m_queue; + m_vrTextureData.m_nQueueFamilyIndex = m_queueFamilyIndex; + m_vrTextureData.m_nWidth = uint32_t( m_size.width() ); + m_vrTextureData.m_nHeight = uint32_t( m_size.height() ); + m_vrTextureData.m_nFormat = uint32_t( kFormat ); + m_vrTextureData.m_nSampleCount = 1; + // For TextureType_Vulkan the handle points to a VRVulkanTextureData_t. + return vr::Texture_t{ &m_vrTextureData, + vr::TextureType_Vulkan, + vr::ColorSpace_Auto }; +} + +vr::VRTextureBounds_t VulkanRenderBackend::vrTextureBounds() const +{ + // Vulkan images are top-down, like D3D — SteamVR's native convention + // (verified on the Steam Frame). + return vr::VRTextureBounds_t{ 0.f, 0.f, 1.f, 1.f }; +} + +QImage VulkanRenderBackend::readback() +{ + if ( m_image == VK_NULL_HANDLE + || m_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ) + return {}; // no finished frame yet + + const VkDeviceSize byteSize + = VkDeviceSize( m_size.width() ) * m_size.height() * 4; + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = byteSize; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + if ( m_df->vkCreateBuffer( m_device, &bufferInfo, nullptr, &buffer ) + != VK_SUCCESS ) + return {}; + + QImage image; + VkMemoryRequirements req{}; + m_df->vkGetBufferMemoryRequirements( m_device, buffer, &req ); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = req.size; + if ( findMemoryType( req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &allocInfo.memoryTypeIndex ) + && m_df->vkAllocateMemory( m_device, &allocInfo, nullptr, &memory ) + == VK_SUCCESS + && m_df->vkBindBufferMemory( m_device, buffer, memory, 0 ) + == VK_SUCCESS ) + { + const bool copied = submit( + m_handoff, + [&]( VkCommandBuffer cb ) + { + VkBufferImageCopy region{}; + region.imageSubresource + = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; + region.imageExtent = { uint32_t( m_size.width() ), + uint32_t( m_size.height() ), + 1 }; + m_df->vkCmdCopyImageToBuffer( + cb, + m_image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + buffer, + 1, + ®ion ); + }, + true ); + void* data = nullptr; + if ( copied + && m_df->vkMapMemory( m_device, memory, 0, byteSize, 0, &data ) + == VK_SUCCESS ) + { + image = QImage( m_size, QImage::Format_RGBA8888_Premultiplied ); + std::memcpy( image.bits(), data, size_t( byteSize ) ); + m_df->vkUnmapMemory( m_device, memory ); + } + } + m_df->vkDestroyBuffer( m_device, buffer, nullptr ); + if ( memory != VK_NULL_HANDLE ) + m_df->vkFreeMemory( m_device, memory, nullptr ); + return image; +} diff --git a/src/render/VulkanRenderBackend.h b/src/render/VulkanRenderBackend.h new file mode 100644 index 0000000..3aa26b4 --- /dev/null +++ b/src/render/VulkanRenderBackend.h @@ -0,0 +1,109 @@ +// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090), +// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3. + +#pragma once + +#include + +#include "RenderBackend.h" + +// Vulkan implementation. Qt Quick creates the VkDevice (on the physical device +// SteamVR reports for the HMD, with the device extensions SteamVR requires); +// this backend owns the color image Qt renders into and handles the layout +// dance SteamVR needs: +// +// Qt renders -> image in COLOR_ATTACHMENT_OPTIMAL +// finishFrame() -> transition to TRANSFER_SRC_OPTIMAL (required by +// OpenVR); the CPU waits for it, the texture is +// handed over right after +// SetOverlayTexture -> SteamVR copies from the image using our queue +// beginFrame() (next) -> transition back to COLOR_ATTACHMENT_OPTIMAL, the +// layout Qt's resource tracking expects; no CPU +// wait: queue order puts it before Qt's frame +// +// Everything runs on the GUI thread; QQuickRenderControl::endFrame() waits for +// the GPU (offscreen frames are synchronous). +class VulkanRenderBackend final : public RenderBackend +{ +public: + VulkanRenderBackend() = default; + ~VulkanRenderBackend() override; + + QString name() const override + { + return QStringLiteral( "Vulkan" ); + } + QSGRendererInterface::GraphicsApi graphicsApi() const override + { + return QSGRendererInterface::Vulkan; + } + + bool initialize( const RenderInterop& interop, QString* error ) override; + void configureWindow( QQuickWindow* window ) override; + bool attachWindow( QQuickWindow* window, QString* error ) override; + void detachWindow() override; + QString deviceName() const override + { + return m_deviceName; + } + + bool makeCurrent() override + { + return true; + } + void doneCurrent() override {} + + QQuickRenderTarget createRenderTarget( QSize size ) override; + void releaseRenderTarget() override; + void beginFrame() override; + void finishFrame() override; + void waitForGpu() override; + + vr::Texture_t vrTexture() override; + vr::VRTextureBounds_t vrTextureBounds() const override; + QImage readback() override; + +private: + bool findMemoryType( uint32_t typeBits, + VkMemoryPropertyFlags properties, + uint32_t* index ) const; + // A command buffer with its fence, for small submissions of our own. + struct Submission + { + VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + bool pending = false; + }; + // Records `record` into the submission's command buffer (after its + // previous use has finished) and submits it; with `wait`, blocks until + // the GPU has executed it. + bool submit( Submission& submission, + const std::function& record, + bool wait ); + bool waitFor( Submission& submission ); + void transitionImage( Submission& submission, + VkImageLayout from, + VkImageLayout to, + bool wait ); + + QVulkanInstance m_instance; + QByteArrayList m_deviceExtensions; + VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + VkQueue m_queue = VK_NULL_HANDLE; + uint32_t m_queueFamilyIndex = 0; + QVulkanDeviceFunctions* m_df = nullptr; + + VkCommandPool m_commandPool = VK_NULL_HANDLE; + Submission m_restore; // beginFrame(): async + Submission m_handoff; // finishFrame() and readback(): synchronous + + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_imageMemory = VK_NULL_HANDLE; + // Actual layout outside of Qt's frame + VkImageLayout m_layout = VK_IMAGE_LAYOUT_UNDEFINED; + QSize m_size; + + vr::VRVulkanTextureData_t m_vrTextureData{}; + QString m_deviceName; +}; diff --git a/src/utils/setup.cpp b/src/utils/setup.cpp index 5b6b733..d001db4 100644 --- a/src/utils/setup.cpp +++ b/src/utils/setup.cpp @@ -58,6 +58,12 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application ) k_resetSettingsDescription ); parser.addOption( resetSettings ); + QCommandLineOption graphicsApi( k_graphicsApi, + k_graphicsApiDescription, + k_graphicsApiValueName, + k_graphicsApiDefault ); + parser.addOption( graphicsApi ); + parser.process( application ); const bool desktopModeEnabled = parser.isSet( desktopMode ); @@ -84,11 +90,16 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application ) if ( resetSettingsEnabled ) qInfo() << "Reset SteamVR Settings."; - const CommandLineOptions commandLineArgs{ - desktopModeEnabled, forceNoSoundEnabled, - forceNoManifestEnabled, forceInstallManifestEnabled, - forceRemoveManifestEnabled, resetSettingsEnabled - }; + const QString graphicsApiValue = parser.value( graphicsApi ); + qInfo() << "Graphics API:" << graphicsApiValue; + + const CommandLineOptions commandLineArgs{ desktopModeEnabled, + forceNoSoundEnabled, + forceNoManifestEnabled, + forceInstallManifestEnabled, + forceRemoveManifestEnabled, + resetSettingsEnabled, + graphicsApiValue }; qInfo() << "Command line arguments processed."; diff --git a/src/utils/setup.h b/src/utils/setup.h index 2730a22..37b9311 100644 --- a/src/utils/setup.h +++ b/src/utils/setup.h @@ -28,6 +28,8 @@ struct CommandLineOptions const bool forceInstallManifest = false; const bool forceRemoveManifest = false; const bool resetSettings = false; + // Value of --graphics-api; checked in main() with parseGraphicsApi(). + const QString graphicsApi = "opengl"; }; // Manages the programs control flow and main settings. @@ -89,6 +91,14 @@ constexpr auto k_forceInstallManifestDescription constexpr auto k_forceRemoveManifest = "force-remove-manifest"; constexpr auto k_forceRemoveManifestDescription = "Forces removing the applications manifest. Application will exit early."; +// Selects the render backend (render/RenderBackend.h). No automatic fallback: +// an explicit choice is never changed silently. +constexpr auto k_graphicsApi = "graphics-api"; +constexpr auto k_graphicsApiDescription + = "Graphics API for rendering the overlay: opengl (default) or vulkan."; +constexpr auto k_graphicsApiValueName = "api"; +constexpr auto k_graphicsApiDefault = "opengl"; + constexpr auto k_resetSettings = "reset-steamvr-settings"; constexpr auto k_resetSettingsDescription = "Resets all SteamVR values that can be modified in OVRAS to defaults.";