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
+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 };
}