[翻译] Vulkan-Sample-MSAA (Multisample anti-aliasing)

原文

Aliasing是以低于原始信号采样率的采样率进行采样导致的。在图形学中,这个过程可以描述为:基于一个会产生artifacts的分辨率去计算像素值,从而在模型边缘产生锯齿。多重采样抗锯齿(Multisample anti-aliasing,MSAA)是一种可以减少像素采样误差的技术。在下面的图中,左侧的图是没有进行抗锯齿渲染的,而右侧的使用了4倍的MSAA。

在计算像素的颜色时,GPU会根据给定的图元有没有覆盖像素的中心坐标(前提是这个图元通过了深度测试),来决定这个像素的颜色。如下图所示,如果不进行抗锯齿,fragment shader就是根据“有没有覆盖像素的中心坐标”来决定像素的颜色的。这样的单采样的方式会不会造成锯齿,就取决于像素密度了。

使用多重采样反锯齿,会在一个像素内进行多个位置的测试。在下图中,有四个采样点,因此称为4倍的MSAA。因为每个采样点都需要存储一个颜色值,所以MSAA实际上增加了每个像素的分辨率。需要注意的是,fragment shader仍然只计算一次(使用中心坐标),这时候像素的颜色结果取决于那些落在图元内的采样点的值(fragment shader会计算这个像素的前提仍然是这个图元通过了深度测试,这意味着深度缓冲区也需要更大,以容纳每个像素多个值)。换句话说,片段着色器的值将会是所有被覆盖到的采样点的值合的结果。像素的值会是所有采样点的平均值。以上便是MSAA的步骤。这样的思路里,

边缘处的图元的shader是不一样的,从而改善了锯齿效果。

在上图中,一个像素内采样点的位置,像一个被旋转过的网格,采样坐标在spec 中规定。不同的模式会水平边缘或者垂直边缘获得更好的结果。请注意,MSAA只对边缘有影响,对于图元内的像素没有影响,因为对于内部的像素,所有采样点存储相同的颜色值。

MSAA与超级采样反锯齿(SSAA)不同(MSAA效果更好),在SSAA中,对每个采样点都执行一遍片段着色器。这有助于减少图元内的锯齿状效果,但通常使用mip-maps已经缓解了这个问题。

要启用MSAA,首先查询 vkPhysicalDeviceLimits ,选择支持的MSAA级别,例如 VK_SAMPLE_COUNT_4_BIT,并在创建 multisampled attachments 和 设置图形管线中的pMultisampleState 的 rasterizationSamples 成员时使用它。正如前面所述,对于MSAA,我们不希望设置 sample shading,因为这意味着开启了更昂贵的SSAA。

Color resolve

tiler architectures 中做4倍MSAA是非常合适的,因为颜色解析可以在tile内存中做,因此是一个临时存储,典型的应用是下图的depth buffer。

(1. "Transient"(临时):在这里,"transient"是指将 multisampled attachments 分配为临时内存,意味着它们不需要持久存储和写回主存。这可以提高性能,因为避免了将多重采样数据写回主存的开销。

2. "Necessary load/store"(必要的加载/存储):这指的是对多重采样附件进行读取和写入操作是必要的,因为在渲染过程中需要对其进行处理或使用。这意味着在渲染通道的子通道中,需要正确配置加载和存储操作,以确保正确处理多重采样数据。

3. "Avoidable load/store"(可避免的加载/存储):这指的是在渲染过程中不再需要对多重采样附件进行读取和写入操作。如果已经使用了颜色解析等技术将多重采样数据转换为单一采样数据,并且不再需要多重采样数据,那么对其进行加载和存储操作将是可避免的开销。)

很重要的点是,如果在渲染场景后不再需要 multisampled attachments,那么需要避免将 multisampled attachments写出到内存。也就是说,multisampled attachments 必须使用 storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE 并且 usage |= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,并按照 Render Passes tutorial 中解释的方式使用 LAZILY_ALLOCATED 内存属性分配图像。

// multisampled attachments是临时的
// 这使得tilers可以完全避免将multisampled attachments写入到内存中,
// 显著的性能和带宽改进
load_store[i_color_ms].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;

如下所示,为了在write-back过程中(write-back是指在渲染管线结束时将图像数据从 GPU 内存返回到 CPU 内存的过程)解析颜色,请配置subpass,将 pResolveAttachments 指向我们希望多采样颜色解析到的那个单采样附件(single-sampled attachment),在本例中是名为 swapchain 的图像。

// Good practice
// 启用write-back 解析到单采样附件(single-sampled attachment)
subpass->set_color_resolve_attachments({i_swapchain});

启用了4X MSAA后,我们所渲染到的color attachment 是一个更大的color attachment ,这个color attachment 里面,每个像素会存储4个颜色值。如果把这个attachment 保留在 tile 内存中,性能的影响仍然很小(如上面屏幕截图中所示,带宽增加3%),而在边缘处会使抖动现象大大减少。原因正是前面所说的,因为硬件在将image写回主内存时对multisampled attachmen作了解析(对样本进行平均处理) 。

Vulkan提供了一种替代方法,使用 vkCmdResolveImage 来明确定义颜色附件的单独解析步骤:

// Bad practice
//将多采样附件解析到目标位置,非常昂贵
vkCmdResolveImage(cmd_buf.get_handle(),
                  multisampled_img.get_handle(),
                  VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                  swapchain_img.get_handle(),
                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                  to_u32(regions.size()), regions.data());

这种做法会产生较高的性能开销和内存占用,因为在这个做法里,需要在subpass 结束的时候存储这个multisampled attachments,并把这个比frambuffer大四倍的attachment 写回GPU以进行解析。

如图所示,这种方法消耗了更多的带宽,如果有其他实现方法,不建议在write-back过程中使用pResolveAttachments 解析颜色。为了说明这一点,示例中设置了切换选项:一种是 resolving on write-back(在write-back期间解析颜色),另一种是在separate resolve pass(单独的过程中解析颜色),并监视由此对带宽的影响。

截图所示的在搭载 Mali-G76的高端智能手机的结果,带宽的差异可以如下解释。当示例渲染2168 x 1080像素的图像,每个像素需要32位(RGBA8,4字节),帧率为60FPS:

1Xattachment :2168 * 1080 * 4 * 60 = 562字节/秒
// 这里比较奇怪,我自己计算的结果是↓
// 字节(Byte)和位(Bit)之间的换算关系是:1 字节(Byte) = 8 比特(Bit)
// 字节(Byte)和M 之间的换算关系是:1MB = 2E+20 Bytes = 1048576Bytes
// 2168 * 1080 * 4 * 60 = 561,945,600 字节/秒 = 536 M/s

如果我们需要为每个像素存储4个样本值,则将其乘以4:

4Xattachment :2168 * 1080 * 4 * 4 * 60 = 2247字节/秒

比较上面屏幕截图中计数器的数字,读取和写入带宽都大约增加了4倍attachment 的大小,因为在当前场景的renderpass结束时,需要将 multisampled attachments 写出,然后重新读取,再解析得到最终的颜色。这意味着 separate resolve pass导致带宽增加了5GB/秒。考虑到在移动设备中,外部DDR带宽的成本约为每GB/秒100毫瓦,这个500毫瓦的开销占据了设备大约2.5瓦功耗预算的20%,这是非常昂贵的。

这些计数器还可以使用Streamline之类的分析工具来记录,图示显示的 resolving on write-back的过程在前,随后进行separate resolve pass的过程。 

Depth resolve

在上面举的例子中,无论 MSAA 是如何设置的,深度缓冲区都是临时存储的。这是因为当颜色值被计算出来,写出到swapchain给显示器显示用后,深度值就没有用了,因此我们建议配置加载/存储操作以避免将其写出。

有时候我们可能需要保存 depth attachment。比如一些简单的post-processing pass需要同时对颜色和深度采样(作为纹理绑定),从而计算出基于屏幕空间(而不是世界空间或裁剪空间)的一些特殊效果。(比如SSAO(Screen Space Ambient Occlusion))

和我们的预判一致,在这种情况下,增加的带宽约等于写出两个全屏附件的attachment。只要我们记住在write-back 期间解析颜色和深度,使用 4X MSAA 几乎不会带来额外开销。

为了在 write-back期间解析depth值,需要使用 VK_KHR_depth_stencil_resolve (从 Vulkan 1.2开始支持) 。在配置subpass的时候, 我们必须使用 VkSubpassDescription2 a并将 pNext 指向一个 VkSubpassDescriptionDepthStencilResolve .这个结构定义了一个用于解析depth的 single-sampled attachment 。

// Good practice
// Multisampled attachment 是临时的
// 这使得tilers 完全避免将multisampled attachment 写出到内存。
// 带来了显著的性能和带宽改进
load_store[i_depth].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;

// 启用 解析到 single-sampled attachment的write-back
subpass->set_depth_stencil_resolve_attachment(i_depth_resolve);
subpass->set_depth_stencil_resolve_mode(depth_resolve_mode);

通过设置 depthResolveMode ,也可以选择如何解析深度,?depthResolveMode?可选的选项在这里: supported (示例代码中查询了设备支持的模式并且提供了一个下拉菜单):

typedef enum VkResolveModeFlagBits {
    VK_RESOLVE_MODE_NONE,
    VK_RESOLVE_MODE_SAMPLE_ZERO_BIT,
    VK_RESOLVE_MODE_AVERAGE_BIT,
    VK_RESOLVE_MODE_MIN_BIT,
    VK_RESOLVE_MODE_MAX_BIT
} VkResolveModeFlagBits;

与颜色相比,Vulkan 不提供其他方式来解析深度附件( vkCmdResolveImage 不支持深度)。因此,如果 VK_KHR_depth_stencil_resolve  没有未支持或没有正确地配置,那么这个pipeline将需要额外的读取 multisampled depth attachment 来执行后处理效果。

 

 

 在这个最糟糕的情况下,即同时将multisampled depth 和multisampled color 写入主存储器,由于separate resolve需要重新读取颜色,读取带宽增加了2366 MiB/s(接近上面计算出的4X附件的带宽)。写入带宽增加了3951 MiB/s,这大致对应于4X附件(2247 MiB/s)和1X附件(562 MiB/s)之间的差异(在这种情况下,深度也是32bpp),即1685 MiB/s,再加上写出额外的4X颜色附件所需的带宽,即2247 MiB/s。总的来说,读写带宽的增加为6.3GB/s,相对于 write-back 解析的最佳实践增加了302%,并且消耗了630 mW的功耗(预算的25%),如果节省下来,可延长电池寿命,实现可持续性性能和更好的用户体验。

最佳实践小结

对于大多数 multisampling的使用场景,可以将所有额外的采样数据保存在GPU内部的 tile 内存中,并在write-back tile 时将其解析为单个像素颜色。这意味着这些额外增加的采样点,不会增加额外的带宽,不会访问外部存储器,这使得它非常高效。多重采样抗锯齿(MSAA)可以完全与Vulkan render passes集成,允许明确指定在subpass结束时做多重采样解析。

应该:

  • 尽量使用(4x MSAA);它不会过于昂贵,同时又很好的改善了图像质量。
  • 对于多重采样图像,使用 loadOp = LOAD_OP_CLEAR 或者 loadOp = LOAD_OP_DONT_CARE
  • 对于多重采样图像,使用 storeOp = STORE_OP_DONT_CARE 。
  • 使用 LAZILY_ALLOCATED 内存来配合分配的 multisampled images;它们不需要持久化到主内存,不需要保存在物理存储设备上。
  • 在 subpass 中使用  pResolveAttachments 自动将多重采样颜色缓冲区解析为单采样颜色缓冲区。
  • 在subpass 中使用 VK_KHR_depth_stencil_resolve 来自动将多重采样深度缓冲区解析为单采样深度缓冲区。通常,只有在深度缓冲区将进一步使用的情况下才有用,在大多数情况下,它是临时的,不需要解析。

应避免:

  • 避免使用vkCmdResolveImage()函数;这会对带宽和性能产生显著负面影响。
  • 避免对 multisampled image attachments 使用  loadOp = LOAD_OP_LOAD
  • 避免对 multisampled image attachments 使用 storeOp = STORE_OP_STORE 。
  • 避免在未经过性能检查的情况下使用超过4倍的 MSAA。

影响:

  • 未能进行内联解析可能导致内存带宽显著增加和性能降低;手动编写和解析一个4x MSAA的1080p表面,以60 FPS运行,需要3.9GB/s的内存带宽,而使用内联解析时只需要500MB/s。