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:
co-authored by
Claude Opus 5.5
parent
423ea57db7
commit
4826686a4d
@@ -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,
|
||||
®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;
|
||||
}
|
||||
Reference in New Issue
Block a user