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