探索 Vulkan 音视频技术(8):计算着色器应用

这个系列文章我们来介绍一位海外工程师如何探索 Vulkan 音视频技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍 Vulkan 计算着色器应用。

——来自公众号“关键帧Keyframe”的分享

Vulkan 中的计算着色器为通用 GPU 计算(GPGPU)打开了无限可能的世界,使开发者能够利用现代显卡的大规模并行处理能力来完成传统渲染之外的任务。在这篇博客文章中,我们将探索 SaschaWillems/Vulkan 仓库中的计算着色器示例,展示如何实现从图像处理到粒子模拟的各种计算密集型应用。

计算着色器是在 GPU 上运行的特殊程序,但不是传统图形管线的一部分。与顶点或片段着色器不同,计算着色器通过着色器存储缓冲对象(SSBO)直接访问 GPU 内存,并且可以执行任意计算。这使它们成为以下任务的理想选择:

  • 图像处理和过滤
  • 物理模拟
  • 粒子系统
  • 光线追踪
  • 机器学习操作
  • 数据并行算法

计算着色器的关键优势在于它们能够同时处理数千或数百万个数据点,利用 GPU 的并行架构实现比基于 CPU 的解决方案显著的性能提升。

来源:computeshader.cpp#L1-L9

1、计算着色器基础设置

在深入了解具体示例之前,让我们了解任何 Vulkan 计算着色器应用所需的基本组件:

1.1、计算管线创建

计算管线比图形管线更简单,因为它们只需要一个计算着色器阶段:

VkComputePipelineCreateInfo computePipelineCreateInfo =
    vks::initializers::computePipelineCreateInfo(compute.pipelineLayout, 0);
computePipelineCreateInfo.stage = loadShader(fileName, VK_SHADER_STAGE_COMPUTE_BIT);
VkPipeline pipeline;
VK_CHECK_RESULT(vkCreateComputePipelines(device, pipelineCache, 1,
    &computePipelineCreateInfo, nullptr, &pipeline));

来源:computeshader.cpp#L466-L476

1.2、计算描述符集

计算着色器通常使用存储图像或缓冲对象:

std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
    // 绑定 0:输入图像(只读)
    vks::initializers::descriptorSetLayoutBinding(
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_COMPUTE_BIT, 0),
    // 绑定 1:输出图像(写入)
    vks::initializers::descriptorSetLayoutBinding(
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_COMPUTE_BIT, 1),
};

来源:computeshader.cpp#L444-L449

1.3、分派计算工作

计算着色器执行的最关键部分是分派命令:

vkCmdDispatch(compute.commandBuffer, storageImage.width / 16,
    storageImage.height / 16, 1);

这将工作划分为 16×16 线程组(工作组),这是 2D 图像处理的常见模式。

来源:computeshader.cpp#L282

2、使用计算着色器进行图像处理

computeshader 示例演示了如何使用计算着色器进行实时图像过滤。这是计算着色器的完美介绍,因为它视觉化、直观,并展示了 GPU 并行性的强大功能。

2.1、存储图像设置

对于图像处理,我们需要计算着色器可以读取和写入的图像:

// 图像将在片段着色器中采样,并在计算着色器中用作存储目标
imageCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;

关键要求是图像格式必须支持存储操作:

// 检查请求的图像格式是否支持图像存储操作
assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT);

来源:computeshader.cpp#L108-L109, computeshader.cpp#L124

2.2、多重滤镜实现

该示例实现了三种不同的图像滤镜,每个都是单独的计算着色器:

filterNames = { "emboss", "edgedetect", "sharpen" };
for (auto& shaderName : filterNames) {
    std::string fileName = getShadersPath() + "computeshader/" + shaderName + ".comp.spv";
    computePipelineCreateInfo.stage = loadShader(fileName, VK_SHADER_STAGE_COMPUTE_BIT);
    VkPipeline pipeline;
    VK_CHECK_RESULT(vkCreateComputePipelines(device, pipelineCache, 1,
        &computePipelineCreateInfo, nullptr, &pipeline));
    compute.pipelines.push_back(pipeline);
}

来源:computeshader.cpp#L469-L476

2.3、计算与图形之间的同步

计算着色器应用的一个关键方面是适当的同步。计算着色器必须在图形管线尝试读取结果之前完成处理:

// 图像内存屏障,确保计算着色器写入完成
VkImageMemoryBarrier imageMemoryBarrier = {};
imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(
    drawCmdBuffers[i],
    VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    VK_FLAGS_NONE,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

来源:computeshader.cpp#L216-L234

3、使用计算着色器的粒子系统

computeparticles 示例展示了一个更高级的用例:实时粒子系统模拟。这演示了计算着色器如何处理复杂的物理计算并高效地更新大型数据集。

3.1、粒子数据结构

粒子存储在着色器存储缓冲对象(SSBO)中,允许计算着色器读取和写入粒子数据:

struct Particle {
    glm::vec2 pos;        // 粒子位置
    glm::vec2 vel;        // 粒子速度
    glm::vec4 gradientPos; // 渐变贴图的纹理坐标
};

// 我们使用着色器存储缓冲对象来存储粒子
vks::Buffer storageBuffer;

来源:computeparticles.cpp#L33-L41

3.2、计算着色器统一变量

计算着色器需要 delta 时间和吸引器位置等参数:

struct UniformData {
    float deltaT;           // 帧 delta 时间
    float destX;           // 吸引器的 x 位置
    float destY;           // 吸引器的 y 位置
    int32_t particleCount = PARTICLE_COUNT;
} uniformData;

来源:computeparticles.cpp#L65-L70

3.3、队列族同步

当计算和图形使用不同的队列族时,需要额外的同步:

if (graphics.queueFamilyIndex != compute.queueFamilyIndex) {
    VkBufferMemoryBarrier buffer_barrier = {
        VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
        nullptr,
        0,
        VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
        compute.queueFamilyIndex,
        graphics.queueFamilyIndex,
        storageBuffer.buffer,
        0,
        storageBuffer.size
    };
    vkCmdPipelineBarrier(drawCmdBuffers[i],
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
        0, 0, nullptr, 1, &buffer_barrier, 0, nullptr);
}

来源:computeparticles.cpp#L132-L155

4、高级计算应用

该仓库还包括几个演示更高级技术的其他计算着色器示例:

4.1、计算剔除和 LOD

computecullandlod 示例展示了如何使用计算着色器进行:

  • 视锥体剔除:移除相机视图外的对象
  • 细节级别选择:根据距离选择适当的细节级别
  • 这显著减少了图形管线的工作负载

4.2、N-体模拟

computenbody 示例实现了引力 N-体模拟,其中每个粒子的运动都受到其他每个粒子的影响。这在计算上非常密集(O(n²) 复杂度),但非常适合 GPU 并行化。

4.3、无头计算

computeheadless 示例演示了无需任何图形输出的计算着色器使用,适用于:

  • 科学计算
  • 数据处理
  • 后台计算

4.4、使用计算进行光线追踪

computeraytracing 示例展示了如何使用计算着色器实现基本的光线追踪,表明可以在传统图形管线之外实现复杂的渲染算法。

5、性能考虑

使用计算着色器时,请记住以下性能提示:

5.1、工作组大小

根据 GPU 架构选择合适的工作组大小:

layout (local_size_x = 16, local_size_y = 16) in;

常见大小为 2D 操作使用 16×16,1D 操作使用 64、128 或 256。

来源:computeshader.cpp#L282

5.2、内存访问模式

组织数据以确保合并内存访问——warp 中的线程应访问连续的内存位置以获得最佳性能。

5.3、异步执行

在可用时使用单独的计算队列,以并发运行计算和图形操作:

// 从设备获取计算队列
vkGetDeviceQueue(device, vulkanDevice->queueFamilyIndices.compute, 0, &compute.queue);

来源:computeparticles.cpp#L439

6、结论

Vulkan 中的计算着色器为通用 GPU 计算提供了强大的工具。SaschaWillems/Vulkan 仓库中的示例展示了从简单图像过滤到复杂物理模拟的广泛应用范围。通过理解这些模式和技术,你可以利用自己的计算密集型应用程序的现代 GPU 的全部功能。

关键要点是:

  1. 适当的同步对于计算和图形操作之间的同步至关重要
  2. 存储图像和 SSBO是数据交换的主要手段
  3. 工作组大小显著影响性能
  4. 队列族管理对于最佳资源利用非常重要

无论你是在实现图像处理、粒子系统还是复杂模拟,计算着色器都为 Vulkan 中的并行计算任务提供了灵活且高性能的解决方案。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

探索 Vulkan 音视频技术(8):计算着色器应用

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论