You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vulkan渲染到存储纹理后读取异常问题求助

Vulkan存储纹理跨管线读取异常问题排查

问题描述

我创建了一个存储纹理,将屏幕显示的相同片段数据存入其中,但在另一个管线的片段着色器中读取该纹理时无法正常工作。

使用了相同的Render Pass和Pipeline Layout,执行流程为:

  • 启动Render Pass
  • 执行第一个管线
  • 结束Render Pass
  • 为存储纹理执行vkCmdPipelineBarrier
  • 再次启动同一Render Pass
  • 执行第二个管线
  • 结束Render Pass

现象:实际显示效果与预期不符,画面存在闪烁。


相关代码

1. Render Pass间的屏障同步代码

VkImageMemoryBarrier imageMemoryBarrier = {};
imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
// We won't be changing the layout of the image
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
imageMemoryBarrier.image = outlineTexture->getTeximage();
imageMemoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    0,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

2. 纹理创建流程代码

VkFormatProperties formatProperties;
vkGetPhysicalDeviceFormatProperties(creDevice.physical_Device(), VK_FORMAT_R8G8B8A8_UNORM, &formatProperties);
// Check if requested image format supports image storage operations
assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT);

VkImageCreateInfo imageCreateInfo{};
imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageCreateInfo.extent = { (uint32_t)texWidth, (uint32_t)texHeight, 1 };
imageCreateInfo.mipLevels = 1;
imageCreateInfo.arrayLayers = 1;
imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
// Image will be sampled in the fragment shader and used as storage target in the compute shader
imageCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
imageCreateInfo.flags = 0;
// If compute and graphics queue family indices differ, we create an image that can be shared between them
// This can result in worse performance than exclusive sharing mode, but save some synchronization to keep the sample simple
/*
std::vector<uint32_t> queueFamilyIndices;
if (vulkanDevice->queueFamilyIndices.graphics != vulkanDevice->queueFamilyIndices.compute) {
    queueFamilyIndices = {
        vulkanDevice->queueFamilyIndices.graphics,
        vulkanDevice->queueFamilyIndices.compute
    };
    imageCreateInfo.sharingMode = VK_SHARING_MODE_CONCURRENT;
    imageCreateInfo.queueFamilyIndexCount = 2;
    imageCreateInfo.pQueueFamilyIndices = queueFamilyIndices.data();
}
*/
QueueFamilyIndices indices = creDevice.findPhysicalQueueFamilies();
uint32_t queueFamilyIndices[] = { indices.graphicsFamily, indices.presentFamily };

if (indices.graphicsFamily != indices.presentFamily) {
    imageCreateInfo.sharingMode = VK_SHARING_MODE_CONCURRENT;
    imageCreateInfo.queueFamilyIndexCount = 2;
    imageCreateInfo.pQueueFamilyIndices = queueFamilyIndices;
}
else {
    imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    imageCreateInfo.queueFamilyIndexCount = 0;      // Optional
    imageCreateInfo.pQueueFamilyIndices = nullptr;  // Optional
}

//imageCreateInfo.sharingMode = VK_SHARING_MODE_CONCURRENT;
//imageCreateInfo.queueFamilyIndexCount = 2;
//imageCreateInfo.pQueueFamilyIndices = queueFamilyIndices;
creDevice.createImageWithInfo(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);

if (auto commandBuffer = creDevice.beginSingleTimeCommands()) {

    VkImageSubresourceRange subresourceRange = {};
    subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = 1;
    subresourceRange.layerCount = 1;

    VkImageMemoryBarrier imageMemoryBarrier{};
    imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
    imageMemoryBarrier.image = textureImage;
    imageMemoryBarrier.subresourceRange = subresourceRange;
    imageMemoryBarrier.srcAccessMask = 0;

    vkCmdPipelineBarrier(
        commandBuffer,
        VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
        VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
        0,
        0, nullptr,
        0, nullptr,
        1, &imageMemoryBarrier);
    creDevice.endSingleTimeCommands(commandBuffer);
}

// Create sampler
VkSamplerCreateInfo sampler{};
sampler.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampler.magFilter = VK_FILTER_LINEAR;
sampler.minFilter = VK_FILTER_LINEAR;
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sampler.addressModeV = sampler.addressModeU;
sampler.addressModeW = sampler.addressModeU;
sampler.mipLodBias = 0.0f;
sampler.maxAnisotropy = 1.0f;
sampler.compareOp = VK_COMPARE_OP_NEVER;
sampler.minLod = 0.0f;
sampler.maxLod = creDevice.msaaSamples;
//sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
if (vkCreateSampler(creDevice.device(), &sampler, nullptr, &textureSampler) != VK_SUCCESS) {
    throw std::runtime_error("failed to create texture sampler!");
};

// Create image view
VkImageViewCreateInfo view{};
view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view.image = VK_NULL_HANDLE;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = VK_FORMAT_R8G8B8A8_UNORM;
view.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
view.image = textureImage;

if (vkCreateImageView(creDevice.device(), &view, nullptr, &textureImageView) != VK_SUCCESS) {
    throw std::runtime_error("failed to create texture image view!");
}

3. 写入存储纹理的着色器代码

#version 450

layout (location = 0) in vec3 fragColor;
layout (location = 1) in vec2 screenSize;

layout (set = 0, binding = 2, rgba8) uniform image2D otlImage;
layout (set = 1, binding = 1) uniform sampler2D texSampler;

layout (location = 0) out vec4 outColor;

void main() {

outColor = vec4(fragColor, 1.0) * texture(texSampler, fragTextureCoord);

imageStore(otlImage, ivec2((gl_FragCoord.x + 0.5)/screenSize.x, (gl_FragCoord.y + 0.5)/screenSize.y), outColor);

}

4. 读取存储纹理的着色器代码

"""Vertex shader"""
 
#version 450

layout (location = 0) out vec2 outUV;

void main() 
{
    outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
    gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f);
}

"""Fragment Shader"""

#version 450

layout (location = 0) in vec2 outUV;

layout (location = 0) out vec4 outColor;

layout (set = 0, binding = 2, rgba8) uniform image2D otlImage;

void main() 
{   
    outColor = imageLoad(otlImage, ivec2(outUV)); 
}

问题排查与修复

1. 屏障同步阶段错误

当前vkCmdPipelineBarrier的srcStageMask设为VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,但第一个管线是在片段着色器中执行imageStore写入纹理,写入操作属于片段着色器阶段,而非顶点着色器。正确的srcStageMask应该是VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,确保所有片段着色器的写入操作完成后再允许读取。

修改后的屏障代码:

vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,  // 修正源阶段为片段着色器
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    0,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

2. 纹理坐标计算错误

写入纹理时的坐标转换逻辑错误:

ivec2((gl_FragCoord.x + 0.5)/screenSize.x, (gl_FragCoord.y + 0.5)/screenSize.y)

gl_FragCoord的坐标范围是[0, width]和[0, height],除以screenSize.x/y(即宽高)后得到的是[0,1]的归一化坐标,但imageStore需要的是像素索引(整数坐标),正确的计算应该直接取gl_FragCoord的整数部分:

修改写入着色器的坐标计算:

imageStore(otlImage, ivec2(gl_FragCoord.xy), outColor);

3. 读取阶段的坐标范围不匹配

读取着色器中,outUV的范围是[0,2](顶点着色器生成的坐标是(0,0),(2,0),(0,2)),直接转换为ivec2(outUV)会得到远超纹理分辨率的坐标。正确的做法是将outUV归一化到[0,1]后,乘以纹理的实际宽高得到像素索引:

首先修改顶点着色器生成[0,1]范围的UV:

void main() 
{
    outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2) * 0.5f;  // 归一化到[0,1]
    gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f);
}

然后在片段着色器中,通过UBO传入纹理分辨率并计算像素坐标:

// 添加UBO绑定纹理分辨率
layout(set = 0, binding = 3) uniform TextureSize {
    vec2 size;
};

void main() 
{   
    outColor = imageLoad(otlImage, ivec2(outUV * size)); 
}

4. 闪烁问题的额外注意点

闪烁通常是因为帧间数据未正确同步,或者双缓冲纹理未正确切换。如果使用了交换链双缓冲,确保存储纹理的写入和读取是针对当前帧的正确纹理实例,或者在帧间添加足够的同步屏障,避免前一帧的数据干扰当前帧。


内容的提问来源于stack exchange,提问作者Akshayakeerthi Duraisamy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 18:44:55