Compute Shader

目录

Introduction

Advantages

The Vulkan pipeline

An example

Data manipulation

Shader storage buffer objects (SSBO)

Storage images

Compute queue families

The compute shader stage

Loading compute shaders

Preparing the shader storage buffers

Descriptors

Compute pipelines

Compute space

Compute shaders

Running compute commands

Dispatch

Submitting work

Synchronizing graphics and compute

Drawing the particle system

Conclusion


Introduction

在本附加章节中,我们将了解计算着色器。到目前为止,之前的所有章节都涉及Vulkan管道的传统图形部分。但与OpenGL等旧API不同,Vulkan中的计算着色器支持是强制性的。这意味着,无论是高端桌面GPU还是低功耗嵌入式设备,您都可以在每个可用的Vulkan实现上使用计算着色器。

这打开了图形处理器单元(GPGPU)上通用计算的世界,无论您的应用程序在何处运行。GPGPU意味着您可以在GPU上进行一般计算,这在传统上是CPU的一个领域。但是随着GPU变得越来越强大和灵活,许多需要CPU通用功能的工作负载现在可以在GPU上实时完成。

可以使用GPU计算能力的几个例子是图像处理、可见性测试、后期处理、高级照明计算、动画、物理(例如,用于粒子系统)等等。甚至可以将计算用于不需要任何图形输出的非视觉计算工作,例如数字处理或AI相关的事情。这被称为“无头计算”。

Advantages

在GPU上进行计算成本高昂的计算有几个优点。最明显的是从CPU上卸载工作。另一种是不需要在CPU的主内存和GPU的内存之间移动数据。所有数据都可以留在GPU上,而无需等待主内存的缓慢传输。

除此之外,GPU是高度并行化的,其中一些GPU具有数万个小型计算单元。这通常使它们比具有几个大型计算单元的CPU更适合高度并行的工作流。

The Vulkan pipeline

重要的是要知道计算与管道的图形部分是完全分离的。这在以下官方规范中的Vulkan管道框图中可见:

在这个图中,我们可以看到左边的管道的传统图形部分,右边的几个阶段不是这个图形管道的一部分,包括计算着色器(阶段)。随着计算着色器阶段与图形管道分离,我们将能够在我们认为合适的任何地方使用它。这与例如始终应用于顶点着色器的变换输出的片段着色器非常不同。

图的中心还显示,例如,描述符集也被计算使用,因此我们所了解的关于描述符布局、描述符集和描述符的所有内容也适用于这里。

An example

我们将在本章中实现的一个易于理解的示例是基于GPU的粒子系统。这种系统在许多游戏中使用,通常由数千个需要以交互帧速率更新的粒子组成。渲染这样的系统需要两个主要组件:作为顶点缓冲区传递的顶点,以及基于某些公式更新它们的方法。

基于“经典”CPU的粒子系统将粒子数据存储在系统的主存储器中,然后使用CPU来更新它们。更新后,顶点需要再次传输到GPU的内存中,以便在下一帧中显示更新的粒子。最直接的方法是用每个帧的新数据重新创建顶点缓冲区。这显然是非常昂贵的。根据您的实现,还有其他选项,如映射GPU内存,以便它可以由CPU写入(在桌面系统上称为“可调整大小的BAR”,或在集成GPU上称为统一内存)或仅使用主机本地缓冲区(由于PCI-E带宽,这将是最慢的方法)。但无论选择哪种缓冲区更新方法,都需要CPU“往返”来更新粒子。

对于基于GPU的粒子系统,不再需要这种往返。顶点仅在开始时上载到GPU,所有更新都使用计算着色器在GPU的内存中完成。这一速度更快的主要原因之一是GPU与其本地内存之间的带宽更高。在基于CPU的场景中,您将受到主内存和PCI express带宽的限制,这通常只是GPU内存带宽的一小部分。

在具有专用计算队列的GPU上执行此操作时,可以与图形管道的渲染部分并行更新粒子。这叫做“异步计算”,是本教程中未涉及的高级主题。

这是本章代码的截图。此处显示的粒子由GPU上的计算着色器直接更新,无需任何CPU交互:

Data manipulation

在本教程中,我们已经了解了不同的缓冲区类型,如用于传递基本体的顶点和索引缓冲区,以及用于将数据传递到着色器的统一缓冲区。我们还使用图像进行纹理映射。但到目前为止,我们总是使用CPU写入数据,只在GPU上进行读取。

计算着色器引入的一个重要概念是能够任意读取和写入缓冲区。为此,Vulkan提供了两种专用存储类型。

Shader storage buffer objects (SSBO)

着色器存储缓冲区(SSBO)允许着色器读取缓冲区和写入缓冲区。使用这些类似于使用统一缓冲区对象。最大的区别在于,您可以将其他缓冲区类型别名为SSBO,并且它们可以任意大。

回到基于GPU的粒子系统,您现在可能想知道如何处理由计算着色器更新(写入)和由顶点着色器读取(绘制)的顶点,因为这两种用法似乎都需要不同的缓冲区类型。

但事实并非如此。在Vulkan中,您可以指定缓冲区和图像的多种用途。因此,对于要用作顶点缓冲区(在图形过程中)和存储缓冲区(计算过程中)的粒子顶点缓冲区,只需使用以下两个使用标志创建缓冲区:

VkBufferCreateInfo bufferInfo{};
...
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
...

if (vkCreateBuffer(device, &bufferInfo, nullptr, &shaderStorageBuffers[i]) != VK_SUCCESS) {
    throw std::runtime_error("failed to create vertex buffer!");
}

使用bufferInfo.usage设置的两个标志VK_BUFFER_USAGE_VERTEX_BUFFER_BIT和VK_BUFFER_USAGE_STORAGE_BUFFER_BIT告诉实现,我们希望将此缓冲区用于两种不同的场景:作为顶点着色器中的顶点缓冲区和存储缓冲区。注意,我们在这里还添加了VK_BUFFER_USAGE_TRANSFER_DST_BIT标志,以便我们可以将数据从主机传输到GPU。这是至关重要的,因为我们希望着色器存储缓冲区仅保留在GPU内存中(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),我们需要将数据从主机传输到此缓冲区。

下面是使用createBuffer函数的相同代码:

createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, shaderStorageBuffers[i], shaderStorageBuffersMemory[i]);

用于访问此类缓冲区的GLSL着色器声明如下所示:

struct Particle {
  vec2 position;
  vec2 velocity;
  vec4 color;
};

layout(std140, binding = 1) readonly buffer ParticleSSBOIn {
   Particle particlesIn[ ];
};

layout(std140, binding = 2) buffer ParticleSSBOOut {
   Particle particlesOut[ ];
};

在这个例子中,我们有一个类型化的SSBO,每个粒子都有一个位置和速度值(参见粒子结构)。然后SSBO包含未绑定数量的粒子,如[]所示。不必指定SSBO中元素的数量是优于例如统一缓冲区的优点之一。std140是一个内存布局限定符,用于确定着色器存储缓冲区的成员元素如何在内存中对齐。这为我们提供了在主机和GPU之间映射缓冲区所需的某些保证。

在计算着色器中写入这样的存储缓冲区对象是直接的,类似于在C++端写入缓冲区的方式:

particlesOut[index].position = particlesIn[index].position + particlesIn[index].velocity.xy * ubo.deltaTime;

Storage images

请注意,本章中我们将不进行图像处理。本段旨在让读者了解计算着色器也可用于图像处理。

存储映像允许您读取和写入映像。典型的用例是将图像效果应用于纹理、进行后期处理(反过来又非常类似)或生成mip贴图。

这与图像类似:

VkImageCreateInfo imageInfo {};
...
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
...

if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
    throw std::runtime_error("failed to create image!");
}

使用imageInfo.USAGE设置的两个标志VK_IMAGE_USAGE_SAMPLED_BIT和VK_IMACE_USAGE_STORAGE_BIT告诉实现,我们希望将此图像用于两种不同的场景:作为片段着色器中采样的图像和计算机着色器中的存储图像;

存储图像的GLSL着色器声明看起来类似于片段着色器中使用的采样图像:

layout (binding = 0, rgba8) uniform readonly image2D inputImage;
layout (binding = 1, rgba8) uniform writeonly image2D outputImage;

这里的一些不同之处是图像格式的附加属性,如rgba8,只读和只读限定符,告诉实现我们将只从输入图像读取并写入输出图像。最后但并非最不重要的是,我们需要使用image2D类型来声明存储映像。

然后使用imageLoad和imageStore完成计算着色器中存储图像的读取和写入:

vec3 pixel = imageLoad(inputImage, ivec2(gl_GlobalInvocationID.xy)).rgb;
imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), pixel);
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值