使用 vulkan 绘制图像时出现错误

I have a bug when drawing an image with vulkan

我正在尝试编写一个也支持图像绘制的 vulkan 引擎。但是当我尝试将图像渲染到全屏四边形时,它不起作用。我尝试调试代码一个多星期了,但我无法让它工作。我正在用 stb_image 加载图像。这是相关代码:

errno = 0;

m_imageData = stbi_load(filename.c_str(), &m_width, &m_height, &m_channels, STBI_rgb_alpha);
if (m_imageData == nullptr) {
    String error = stbi_failure_reason();
    console::printErr("Image loading failed!\nImage: "_d + filename + "\nFailure reason: " + error + "\n" + strerror(errno));
}

然后我将 m_imageData 传递给此方法:

m_device = vulkanManager.getDevice().getDevice();
m_imageSize = width * height * 4;

VkImageCreateInfo imageCreateInfo;
imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCreateInfo.pNext = nullptr;
imageCreateInfo.flags = 0;
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageCreateInfo.extent.width = width;
imageCreateInfo.extent.height = height;
imageCreateInfo.extent.depth = 1;
imageCreateInfo.mipLevels = 1;
imageCreateInfo.arrayLayers = 1;
imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageCreateInfo.queueFamilyIndexCount = 0;
imageCreateInfo.pQueueFamilyIndices = nullptr;
imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

VkResult result = vkCreateImage(m_device, &imageCreateInfo, nullptr, &m_image);
debug::Assert_Vulkan(result);

void* stagingBufferMemory;

VulkanBuffer stagingBuffer((uint)m_imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
stagingBuffer.mapMemory(&stagingBufferMemory, 0);

memcpy(stagingBufferMemory, pixels, m_imageSize);

stagingBuffer.unmapMemory();

VkMemoryRequirements imageMemRequirements;
vkGetImageMemoryRequirements(m_device, m_image, &imageMemRequirements);

VkMemoryAllocateInfo imageMemAllocInfo;
imageMemAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
imageMemAllocInfo.pNext = nullptr;
imageMemAllocInfo.allocationSize = imageMemRequirements.size;
imageMemAllocInfo.memoryTypeIndex = vulkanManager.findMemoryTypeIndex(imageMemRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

result = vkAllocateMemory(m_device, &imageMemAllocInfo, nullptr, &m_imageMemory);
debug::Assert_Vulkan(result);

vkBindImageMemory(m_device, m_image, m_imageMemory, 0);

VulkanCommandBuffer copyBufferToImage(vulkanManager.getDevice().getGraphicsQueueIndex());
copyBufferToImage.startCmdBuffer(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

VkBufferImageCopy region;
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = { 0, 0, 0 };
region.imageExtent = { width, height, 1 };

vkCmdCopyBufferToImage(copyBufferToImage.getCommandBuffer(), stagingBuffer.getBuffer(), m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);

copyBufferToImage.endCmdBuffer();

transitionImageLayout(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

copyBufferToImage.execute();

transitionImageLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
stagingBuffer.destroy();

transitionImageLayout 是辅助函数:

void VulkanImage::transitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout)
{
    VulkanCommandBuffer cmdBuffer(vulkanManager.getDevice().getGraphicsQueueIndex());
    cmdBuffer.startCmdBuffer(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

    VkPipelineStageFlags srcStage, dstStage;

    VkImageMemoryBarrier memoryBarrier;

    if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
        memoryBarrier.srcAccessMask = 0;
        memoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

        srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    }
    else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        memoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        memoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

        srcStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    }

    memoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    memoryBarrier.pNext = nullptr;
    memoryBarrier.oldLayout = oldLayout;
    memoryBarrier.newLayout = newLayout;
    memoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    memoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    memoryBarrier.image = m_image;
    memoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    memoryBarrier.subresourceRange.baseArrayLayer = 0;
    memoryBarrier.subresourceRange.baseMipLevel = 0;
    memoryBarrier.subresourceRange.layerCount = 1;
    memoryBarrier.subresourceRange.levelCount = 1;

    vkCmdPipelineBarrier(
        cmdBuffer.getCommandBuffer(),
        srcStage, dstStage,
        0,
        0, nullptr,
        0, nullptr,
        1, &memoryBarrier
    );

    cmdBuffer.endCmdBuffer();
    cmdBuffer.execute();
}

VulkanCommandBufferVulkanBuffer 只是 VkBufferVkCommandBuffer.

的包装

加载完所有图像后,我创建了一个 imageView 并使用该图像更新了片段着色器中的描述符。但是当我执行程序时,屏幕上只有白色。但是当我只在片段着色器中绘制一种颜色时,我的着色器正在工作。

这是完整的 Visual Studio 2017 项目的 link:link(使用 x64 调试配置构建)

回答你的问题。问题出在描述符集上,更具体地说——您在绘制之前没有将它绑定到命令缓冲区。为了使用描述符集,除了分配和更新它们之外,您还需要绑定它们。更新描述符集只是将特定资源(图像、采样器、缓冲区)与给定的描述符集相关联。这样您就可以拥有多个具有各种资源的描述符集。但是为了使用给定的描述符集,你需要在绘制之前绑定它。绑定操作是通过 vkCmdBindDescriptorSets() 函数调用完成的,您可以通过该函数调用指定应该使用哪些描述符集(因此 - 哪些资源应该用于绘图)。在你的代码中 vkCmdBindDescriptorSets() 函数从未被调用(它没有在任何地方使用,或者至少 Visual Studio 没有找到它)。

但我对您的代码也有一些意见:

  1. 创建 Vulkan 实例时指定了错误的版本。您指定 0.0.1 版本而不是 1.0.0。目前补丁版本无关紧要,因为所有 Vulkan 驱动程序和 SDK 版本都应该与旧版本的 Vulkan 兼容(补丁版本较低)。

  2. 创建交换链期间显示错误假设(和警告消息):

if (surface.getSurfaceCapabilities().minImageCount != 2) debug::Break("Your graphics device does not support double buffering!");

minImageCount surface capabilities 成员意味着您可以使用至少该数量的图像创建交换链(您不能要求更少)。但这并不意味着不支持双缓冲。如果驱动程序指定 3 怎么办? 3张图片足够双倍(甚至三倍)缓冲,但这里3张图片也会触发中断。

  1. 您创建了一个启用了混合的图形管线。当我将片段着色器设置为仅提供红色输出 (1, 0, 0, 0) 时,颜色不可见(因为它是透明的)。我不得不将其更改为 (1, 0, 0, 1)。当然,它是有效的,但有时它可能会导致调试问题。

  2. 您的绘图代码的组织方式很奇怪。您创建描述符集布局、管道布局(使用描述符集)和图形管道。然后,您立即记录一个绑定管道并绘制几何图形的命令缓冲区。这是无效的,因为管线已经使用了描述符集(在管线布局和片段着色器中指定),但到目前为止还没有更新。几帧后,您更新了描述符集。只有在这一点之后,您的绘图代码才开始正确。您应该重构它以避免将来出现潜在问题。

  3. 你的纹理坐标是错误的。但是一旦您解决了描述符集的问题,就很容易发现这个问题。