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>
462 lines
17 KiB
C++
462 lines
17 KiB
C++
// 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,
|
|
®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;
|
|
}
|