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 <noreply@anthropic.com>
This commit is contained in:
Matthias Janetschek
2026-10-07 13:41:13 +02:00
co-authored by Claude Opus 5.5
parent 423ea57db7
commit 4826686a4d
15 changed files with 1243 additions and 140 deletions
+22 -7
View File
@@ -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
+45
View File
@@ -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).
+34 -1
View File
@@ -1,6 +1,7 @@
#include "utils/setup.h"
#include "settings/settings.h"
#include "openvr/ovr_settings_wrapper.h"
#include "render/RenderBackend.h"
#include <QtLogging>
#include <QtDebug>
@@ -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> 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<advsettings::OverlayController> 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" );
+71
View File
@@ -6,6 +6,7 @@
#include <QtLogging>
#include <QtDebug>
#include "../openvr/ivrinput_manifest.h"
#include <QByteArray>
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 <typename Query>
static QByteArrayList queryExtensionList( Query query )
{
const uint32_t size = query( nullptr, 0 );
if ( size == 0 )
{
return {};
}
QByteArray buffer( static_cast<int>( 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<VkPhysicalDevice_T*>( device );
};
return interop;
}
} // namespace openvr_init
+7
View File
@@ -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
+63 -113
View File
@@ -28,12 +28,7 @@
#include <qapplication.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <rhi/qrhi.h>
#include <stdexcept>
// Direct3D exists only on Windows.
#ifdef _WIN32
# include <d3d11.h>
#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> 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<void*>( 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<ID3D11Texture2D *>( 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<const QRhiVulkanNativeHandles*>(
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<uint32_t>( tex.pixelSize().width() ),
.m_nHeight = static_cast<uint32_t>( tex.pixelSize().height() ),
.m_nFormat = static_cast<uint32_t>(tex.format()),
.m_nSampleCount = static_cast<uint32_t>( 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<float>( 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<float>( m_window.height() ) - mouse.y;
return { mouse.x, y };
}
+14 -14
View File
@@ -25,7 +25,7 @@
#include <QQuickRenderTarget>
#include <memory>
#include <optional>
#include <rhi/qrhi.h>
#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<QVulkanInstance> 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<RenderBackend> m_renderBackend;
QQuickRenderControl m_renderControl;
QQuickWindow m_window{ &m_renderControl };
std::optional<vr::ETextureType> m_cached_vr_texture_type;
std::unique_ptr<QRhiTexture> m_pFBTexture;
std::unique_ptr<QRhiTextureRenderTarget> m_render_target;
std::unique_ptr<QRhiRenderPassDescriptor> m_render_pass_descriptor;
// Set by Shutdown(), which must run only once (see there).
bool m_shutdownDone = false;
/*
std::unique_ptr<QOpenGLFramebufferObject> 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> renderBackend );
virtual ~OverlayController();
void Shutdown();
+180
View File
@@ -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 <QOpenGLFunctions>
#include <QQuickGraphicsDevice>
#include <QQuickWindow>
#include <QSurfaceFormat>
#include <cstdint>
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<QOpenGLContext>();
m_context->setFormat( format );
if ( !m_context->create() )
{
if ( error )
*error = QStringLiteral( "Failed to create OpenGL context" );
return false;
}
m_surface = std::make_unique<QOffscreenSurface>();
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<const char*>(
f->glGetString( GL_RENDERER ) ) ),
QString::fromLatin1( reinterpret_cast<const char*>(
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<void*>(
static_cast<uintptr_t>( 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
}
+57
View File
@@ -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 <QOffscreenSurface>
#include <QOpenGLContext>
#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<QOpenGLContext> m_context;
std::unique_ptr<QOffscreenSurface> m_surface;
unsigned int m_texture = 0; // GLuint
QSize m_size;
QString m_deviceName;
};
+38
View File
@@ -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<RenderBackend> createRenderBackend( GraphicsApi api )
{
switch ( api )
{
case GraphicsApi::OpenGL:
return std::make_unique<OpenGLRenderBackend>();
case GraphicsApi::Vulkan:
#if OVRAS_HAS_VULKAN
return std::make_unique<VulkanRenderBackend>();
#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;
}
+116
View File
@@ -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 <openvr.h>
#include <QByteArrayList>
#include <QImage>
#include <QQuickRenderTarget>
#include <QSGRendererInterface>
#include <QSize>
#include <QString>
#include <functional>
#include <memory>
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<QByteArrayList()> vulkanInstanceExtensions;
std::function<QByteArrayList( VkPhysicalDevice_T* )> vulkanDeviceExtensions;
// The physical device driving the HMD, for a given VkInstance.
std::function<VkPhysicalDevice_T*( VkInstance_T* )> 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<RenderBackend> createRenderBackend( GraphicsApi api );
// "opengl" / "vulkan" (case-insensitive). Returns false for unknown names.
bool parseGraphicsApi( const QString& name, GraphicsApi* api );
+461
View File
@@ -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 <QQuickGraphicsConfiguration>
#include <QQuickGraphicsDevice>
#include <QQuickWindow>
#include <QVersionNumber>
#include <QVulkanDeviceFunctions>
#include <QVulkanFunctions>
#include <cstring>
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<VkDevice*>(
resource( QSGRendererInterface::DeviceResource ) );
auto* physicalDevice = static_cast<VkPhysicalDevice*>(
resource( QSGRendererInterface::PhysicalDeviceResource ) );
auto* queue = static_cast<VkQueue*>(
resource( QSGRendererInterface::CommandQueueResource ) );
auto* family = static_cast<uint32_t*>(
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<void( VkCommandBuffer )>& 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<uint64_t>( 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,
&region );
},
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;
}
+109
View File
@@ -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 <QVulkanInstance>
#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<void( VkCommandBuffer )>& 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;
};
+16 -5
View File
@@ -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.";
+10
View File
@@ -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.";