citra 图形设置_游戏引擎随笔 0x15:现代图形 API 的 MSAA

0e87f99daca9b4763672c3b952a6601d.png

题图为我们目前正在 GGE 上开发中的 Strands-Base Hair Rendering 效果图,由于人类的发丝只有 40~50 微米粗细,渲染到屏幕上往往不足一个像素大小,aliasing 的情况很严重,使用 TAAFXAA 无法解决这种由于几何光栅化导致的几何 aliasing,因此想到了使用 MSAA。起初出于惯性思维,以为这个特性也就一天差不多就能完成,事实比我预想的复杂得多,几乎把整个国庆假期都搭了进去,但在这个过程中对 AA 和现代图形 API 有了更深入的了解,所以有必要把在 GGE 支持 MSAA 的这些要点和心得记录下来,用以备忘。

MSAA原理

MSAA (多重采样抗锯齿)是一种几何抗锯齿技术,是对点、线、面等基本几何图元进行抗锯齿的一种机制。在光栅化时,在每个像素中增加多个 Sample,为每个 Sample 计算像素覆盖率并进行深度模板测试,通过后进行 Sample Shading,这样 Sample 都有各自对应的 Color、Depth/Stencil 值,最后在 Resolving 时针对这些 Sample 进行评估得到像素的最终颜色,从而达到抗锯齿效果。由于是依赖光栅化机制,因此MSAA 不能解决 Surface 的 Aliasing。MSAA 原理如下图:

1dc913e261a2a8a9b828d18eddf1e096.png
每像素 1 个 Sample

48490ff70e3c3c35e28ba37ffe989e6b.png
每像素 4 个 Sample,像素最终颜色由 4 个 Sample 的 Color 评估决定,形成抗锯齿效果

当 Sample 执行 Shading 时,使用的顶点属性(Position、UV、Color 等)是经过插值的像素中心的值,而不是每个 Sample 所属位置的插值。由于使用的是同一份插值,因此这意味着每个 Sample 都可以一定程度的代表当前像素,当最终 Resolving 时平均像素中的 Sample Color 做为像素的最终输出。Resolving 流程示意图如下:

bf60d713821b69d93e1064d03a71579e.png
将 Multi Sample 的 Color 和 Depth Resolve 为 Single Sample 的 ColorDepth

从计算角度看,为了提高性能,几乎所有的硬件都只会对每个被 Coved 的 Sample 进行一次Shading,被同一个 Primitive 覆盖的 Sample 通过索引共享一个 Shading 的结果。从存储角度看,MSAA 的内存开销是非 MSAA(也可以理解为 Single Sample,只是一个像素只有一个在像素的中心的 Sample)的 N 倍,N 就是对应的 Sample 数量。因此,MSAA 的计算效率相对 SSAA 这种每个 Sample 都执行 Shading 的方式要高得多,但带来的问题是当画面中的几何图元覆盖率不断变化时(例如 Camera 或物体的运动),Sample 的 Shading 计算次数的不固定导致计算性能不稳定,这也是在工程中使用 MSAA 实现抗锯齿时容易被忽略的一点。下图展示了不同情况下的 Sample 的 Shading 的次数:

228aa97d5625b0d231b01614ddc1b5c3.png
4 个 Sample 中有 3 个被覆盖,则执行 2 次 Shading 计算,存储在对应的 Sample 的 Color。

78079523f62a4f82c40dac4a81d4c0d2.png
当所有 Sample 被同一 Primitive 覆盖,则只 Shading 一次。

带有 Multi-Sampe 的 Color 和 Depth Stencil 都可以 Resolving,只是 Resolve 的模式不同,一般来说 Color 主要以 Average 为主,而 DepthStencil 则可以使用 Min、Max、Sample0 三种模式,毕竟对 DS 进行 Average 没有图形意义。

需要说明的是,除了硬件 Resolving,也可以利用 Shader 来实现“软”Resolving,但需要硬件的特性支持,下文会详细说明。

由上述原理可推导出一些 MSAA 的隐含限制:

  1. MSAA 时 Depth Stencil 也必须是 MultiSample 的,并且与 Color 的 Sample 数量相同。
  2. 对于 MRT 的所有 Color Buffer,都必须具有相同的 Sample 数量。

传统图形 API 的 MSAA

传统 API 的 MSAA 使用起来很简单,用于 present 的 swapchain 的 MSAA 仅仅是设置一个 SampleCount 参数即可,而对于 RenderTargetFrameBuffer 的 MSAA 则稍微多点步骤,在创建 RenderTargetFrameBuffer 时指定 SampleCount,然后再调用 Resolve API 并指定一个接收 Resolve 结果的资源即可完成。

传统 API 也可以实现软 Resolving,以 OpenGLES 为例,必须 3.1 及以上支持 MultiSample Texture,在 Fragment Shader 或 Compute Shader 中可使用 texelFetch 函数指定 sample index 获取对应 Sample 的 color,然后进行自定义 Resolving。

但是在传统 API 中是无法实现 Depth Stencil 的 Resolving,但是可以通过上述“软”Resovling的方式来实现。

现代图形 API 的 MSAA

老规矩,还是一个一个的来,先从最复杂的 Vulkan 讲起。

Vulkan

Render Pass 是 Vulkan 执行渲染的核心概念,而无论是作为 swapchain 中用于 present 的 VkImage 还是传统所谓的“RenderTarget”的 VkImage,本质上都是 Color Attachment,所以这就变成了如何对 Color Attachment 实现 MSAA 的问题。

流程上总体来说有 3 步来实现:

  1. Vulkan 资源的 MultiSample 配置。
  2. Vulkan GraphcisPipeline 的 MultiSample 配置。
  3. 进行 Resolving,这个步骤最复杂,并且有三种 Resolving 机制,下文会详细讲解。

第一步:在创建 VkImage 的 VkImageCreateInfo 中的 samples 指定为设备支持的 Sample 数量,这是个枚举类型,定义如下:

typedef enum VkSampleCountFlagBits {
    VK_SAMPLE_COUNT_1_BIT = 0x00000001,
    VK_SAMPLE_COUNT_2_BIT = 0x00000002,
    VK_SAMPLE_COUNT_4_BIT = 0x00000004,
    VK_SAMPLE_COUNT_8_BIT = 0x00000008,
    VK_SAMPLE_COUNT_16_BIT = 0x00000010,
    VK_SAMPLE_COUNT_32_BIT = 0x00000020,
    VK_SAMPLE_COUNT_64_BIT = 0x00000040,
    VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkSampleCountFlagBits;

在 Vulkan 中,不同的 Format、Image 类型、Image Tilling模式、Image 用法以及 Image 的创建标志都会影响到这个值,需要通过 vkGetPhysicalDeviceImageFormatProperties 函数获取格式属性 VkImageFormatProperties 中的 sampleCounts 字段作为支持的 Sample 数量。对于特定的 Usage,sampleCount 还受限于 VkPhysicalDeviceLimits(通过获取设备属性 VkPhysicalDeviceProperties 结构的 limit 字段)中对应的数值,比如 Color Attachment 对应的是 framebufferColorSampleCounts 字段,Depth Attachment 对应的是 framebufferDepthSampleCounts 字段等等,以此类推余下就不再赘述。

对于 PC 平台,大部分支持 1、2、4、8,而移动平台大部分是 1、2、4,Mali G77 可以支持到 8。

第二步:创建 Graphics Pipeline 的 VkGraphicsPipelineCreateInfo 结构中的 VkPipelineMultisampleStateCreateInfo 结构中 rasterizationSamples 字段,设置为第一步中指定的 Sample 数量,与第一步相同,需要查询设备是否支持设置的 Sample Count。

第三步:执行 Resolving,在 Vulkan 中 Resovle MultiSample Attachment 有三种方法:

1、利用 RenderPass 的 Subpass 自动执行 Attachment 的 Resolving。在需要 Resolve 的 Subpass 的 VkSubpassDescription 中指定 Resolve Attachments,需要注意的是,网络上有些教程出于同步的考虑,在 RenderPass 中设置了 SubPass Dependency,目的是用于保证 Attachment 在 Resolving 过程中进行同步。但实际上这是不必要的,正如 Vulkan 官方文档所说,Subpass 会自动执行其内部的渲染与其末尾的任何 Resolve 操作之间的同步,而无需显示的通过 Subpass Dependency 或者 Pipeline Barrier 进行同步,摘录原文如下:

Moving to the next subpass automatically performs any multisample resolve operations in the subpass being ended. End-of-subpass multisample resolves are treated as color attachment writes for the purposes of synchronization. This applies to resolve operations for both color and depth/stencil attachments. That is, they are considered to execute in the VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BITpipeline stage and their writes are synchronized with VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT. Synchronization between rendering within a subpass and any resolve operations at the end of the subpass occurs automatically, without need for explicit dependencies or pipeline barriers.

在移动平台上,Resovling 更重要的是要充分借助 GPU 上的 Tile MemoryCache 的 硬件优势,可以实现几乎“零”开销 MSAA。一般情况下,我们只关注 Resolving 的抗锯齿结果,而不需要保留 Multi Sample 的内容,因此我们完全可以将 Multi Sample 的 Attachment 存储在 Tile Cache 上,然后在 Tile Cache 上原地执行 Resovling 操作,将 Resolve 结果写入到 GPU Memory 中作为 Resovle Attachment 输出,最后当 Resolving 结束时丢弃 Multi Sample 的内容,这样整个的 Multi Samle Attachment 的内容完全没有 GPU 带宽消耗,进而提升 MSAA 整体的性能。

要实现上述零带宽开销的 MSAA,需要如下设置:

  • Multi Sample 的 Attachment Load Action 需要设置为 Clear 或 DontCare
  • Multi Sample 的 Attachment Store Action 必须设置为 DontCare

再进一步,可以借助 Transient Usage bit 和 Lazily Allocate 内存机制,甚至可以做到 Multi Sample Attachment 的零内存开销,也就是 Memoryless,这样又节省了内存开销。具体的开启方式请参考我之前的 Vulkan 实战回顾文章中的 Memoryless 一段,这里就不赘述了:

丛越:游戏引擎随笔 0x09:现代图形 API 实战回顾-Vulkan 篇​zhuanlan.zhihu.com
53934ba6fa72272a36175806c6832ab0.png

由于 Vulkan 中 Resovle Attachment 也在 Subpass 中配置,因此也需要指定 Resolve Attachment 的 LoadStore Action,这里有个不易察觉的优化,由于 Resolve Attachment 是直接从 MultiSample Attachment 接收 Resolving 结果,因此无需 Clear,可以设置为 DontCare,这样性能更佳,而 Store Action 则必然要设置为 Store。

2、通过 vkCmdResolveImage 执行 Resovling,这个 API 不能在 RenderPass 内调用。需要注意的是,这个 API 属于 Copy Command,因此在执行之前需要将 Multi Sample 和 Resovle 资源同步到对应的 Transfer Read 和 Write 读写位上并且切换到对应的 Transfer SrcDst Optimal 布局上。使用 vkCmdResolveImage 的流程如下图所示:

187ac7f523e4734cd486fe68e280bf6f.png

从图中可以看出,在 Sence Pass 中将 MultiSample Color 先存储到 GPU 内存中,然后在 Resolve Pass 中再回读,对 GPU 来说这必然增加了 N(Sample 数量)倍的内存开销,并且还带来了更多的 GPU 带宽消耗,因此与 RenderPass 自动执行 Resolve 相比,性能会低很多,因此在移动平台,这种方式是不推荐使用的。

3、Shader Resovling,与第二种方法类似,区别只是需要手动编写 Shader,Fragment、Compute 都可以,在 Shader 中使用 texelFetch指令遍历每个 Sample 进行 Resolve。这种方式的好处是可以实现自定义的 Resolve 算法,但所带来的问题与第二种方式一样,内存和带宽消耗都成倍增加,另外还需要对渲染流程有侵入式的修改,因此也不推荐。如果一定要这样做,建议用 Compute Shader 实现,毕竟 Compute Pipeline 很短,而且可以和 Graphics Pipeline 并行,这样在 Resolving 的时候可以并行执行其他 Graphcis 计算,如果使用 Fragment Shader 还需要 RenderPass 的配合,流程也比较繁琐。另外需要注意的是使用 Compute 实现时的资源同步问题。

DepthStencil 的 Resovling

Vulkan 1.2 之前没有提供 DS 的 Resolving 核心功能,vkCmdResolveImage 只能 Resovle Color Attachment,因此如果需要 DS 的 Resolving 只能通过 VK_KHR_depth_stencil_resolve 扩展实现,Vulkan 在 1.2 中将此扩展提升为核心功能。

在 1.2 中使用 DS Resovling 需要用到 VkSubpassDescription2 新结构来描述 Subpass,而且在创建 RenderPass 时需要使用 vkCreateRenderPass2 函数,并且指定 VkRenderPassCreateInfo2 结构。

但可惜的是在 VkSubpassDescription2 结构中并没有如我们预想的那样和 Color 的Resovle Attachment 一样有个可以直接设置 DS Resolve Attachment 的成员,而是通过 VkSubpassDescriptionDepthStencilResolve 来指定 DS 的 Resovle Attachment,还需指定 DS Resovle 模式,Vulkan 支持 Sample0、Min、Max、Average 四种模式。然后将这个结构设置给 VkSubpassDescription2 的 pNext 成员,至此才算完成了整个 DS Resovling 的配置工作。

如果还是基于 1.2 以前的版本开发,需要开启 VK_KHR_depth_stencil_resolve 扩展,并且上述的结构和函数都要加上 KHR 后缀,开启 VK_KHR_depth_stencil_resolve 扩展需要以下 2 步:

  1. 创建 VkInstance 时启用 VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME 扩展
  2. 创建 VkDevice 时启用如下扩展:
    1. MULTIVIEW
    2. MAINTENANCE2
    3. CREATE_RENDERPASS_2
    4. DEPTH_STENCIL_RESOLVE

需要注意的是 vkCreateRenderPass2KHR 函数,不能直接使用,而需要手动的 vkGetDeviceProcAddr 获取函数地址才能使用,否则会出现链接错误。

最后有一点需要注意,由于使用了 VkSubpassDescription2KHR 结构,其所指定的所有 Attachment Reference 也需要使用新的 VkAttachmentReference2KHR 结构,当设置 Input Attachment 时,必须指定 aspectMask 成员,否则会引发校验层错误。


D3D12

D3D12 的 MSAA 设置比 Vulkan 多了一个 Quality 参数,代表 MSAA 的质量等级,这与特定的硬件特性相关,在 DX1011 时代不同的硬件不同 Quality 数值代表不同的 MSAA 实现机制,比如对于 AMD GPU 会调用 EQAA,Nvidia GPU 调用 CSAA。近年来随着 AA 技术的发展,软件 AA 技术比如 FXAATAA 等逐渐成为主流,硬件的 MSAA 应用逐渐缩小,因此这个 Quality 数值几乎不再使用,一般情况下只设置 0 即可。

在 D3D12 中 MSAA 的流程和 Vulkan 类似,也是 3 个步骤:

  1. D3D12 资源的 MultiSample 配置。
  2. D3D12 GraphcisPipeline 的 MultiSample 配置。
  3. 进行 Resolving,同 Vulkan 类似,也有三种 Resolving 机制,下文会详细讲解。

第一步:与 Vulkan 不同的是,除了在创建资源时指定 Sample Count 之外,还需要在创建 View 时指定 ViewDimension 类型为 2DMS2DMSARRAY,对于 SRVRTVDSV 都需要指定对应的 2DMS ViewDimension 类型。在设置 Sample Count 时需要查询设备是否支持,这需要调用 ID3D12Device::CheckFeatureSupport 接口查询,通过指定 D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS 类型来获取 D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS 数据结构,如果待设置的Format 以及 Sample Count 对应的 NumQualityLevels 不为 0,则支持这个数值的 MS,否则不支持。获取指定 DXGI Format 和 Sample Count 是否支持,获取代码如下,注意 SampleCount 都是 2 的整数倍:

for (auto SampleCount = 1; SampleCount <= D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT; SampleCount *= 2)
{
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS QualityLevels = {DXGIFormat, SampleCount};

    hr = m_pd3d12Device->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &QualityLevels, sizeof(QualityLevels));
    if (SUCCEEDED(hr) && QualityLevels.NumQualityLevels > 0)
        // 支持 当前 SampleCount
        ... 
}

第二步:创建 Graphics Pipeline 的 D3D12_GRAPHICS_PIPELINE_STATE_DESC 结构中的 SampleDesc 结构中 Count 字段,设置为第一步中指定的 Sample 数量,与第一步相同,需要查询设备是否支持设置的 Sample Count。

第三步:执行Resolving,与 Vulkan 一样,有三种方法:

1、D3D12 RS5 之前,可以通过 ID3D12GraphicsCommandList::ResolveSubresource 接口实现 Resolving,后来在 ID3D12GraphicsCommandList1 接口中,又提供了可解析指定区域的扩展函数:ResolveSubresourceRegion,除了可指定 Resolving 的区域,还可以指定 Resovle 模式。这两个函数都可以解析 Color 和 Depth Stencil。此外,此方案还必须手动对 MultiSample 和接收 Resolving 结果的 Resolve RTDS 调用 ResourceBarrier 执行同步,以保证 Resolving 源和目标资源过渡到正确的 Resource State 上。需要将 MS 资源过渡到 RESOLVE_SOURCE State,对 Resolved 资源过渡到 RESOLVE_DEST。

2、使用 D3D12 Render Pass 自动执行 RenderTargetDepthStencil 的 Resolving。

在 D3D12 RS5 时,D3D12 加入了 Render Pass 概念,使用 Render Pass 要求 Windows 10 版本1809(10.0; Build 17763)以上,使用 ID3D12GraphicsCommandList4 接口。与 Vulkan 的 RenderPass 不同的是,D3D12 并没有提供类似 VkRenderPass 对象用于操作,而是在 BeginRenderPass 时设置 RenderTarget 和 DepthStencil 描述结构,分别对应的是 D3D12_RENDER_PASS_RENDER_TARGET_DESC 和 D3D12_RENDER_PASS_DEPTH_STENCIL_DESC 结构,在描述的数据结构中定义所需的 RTDS 的访问的方式和相应参数,这种方式与 Metal 的 RenderPass Descriptor 概念几乎一致。

需要注意的是,使用 RenderPass 仍需手动执行所使用的 RT 和 DS 资源的同步,也就是 ResourceBarrier,以保证资源 State 的正确切换。

在这两个描述结构中,都需要提供以下信息:

  1. RTVDSV Descriptor
  2. 进入 Pass 时对使用的 RTVDSV 的参数设置
  3. 离开 Pass 时对使用的 RTVDSV 的参数设置

第 1 点与之前的 OMSetRenderTargets API 设置参数相同,不再赘述。重点说后面两点。

进入 Pass 时对使用的 RTVDSV 的参数配置是通过 D3D12_RENDER_PASS_BEGINNING_ACCESS 结构完成,定义如下:

typedef struct D3D12_RENDER_PASS_BEGINNING_ACCESS {
  D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE Type;
  union {
    D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS Clear;
  };
} D3D12_RENDER_PASS_BEGINNING_ACCESS;

其中 Type 参数是访问类型,枚举定义如下:

typedef enum D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE {
  D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_DISCARD,
  D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE,
  D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
  D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_NO_ACCESS
} ;

由定义可见,与 VulkanMetal 的 Load Action 很相似,基本都可以直接映射。

接下来是 Clear 参数,结构定义如下:

typedef struct D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS {
  D3D12_CLEAR_VALUE ClearValue;
} D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS;

可见内部仅仅定义了 Clear Value(ColorDepthStencil),因此对于 Begining Access 来说只是定义了如何操作 RT 以及设置 Clear Value。这是通过一个数据结构将原有多个 API 的调用统一成一个 API 调用。

Endging Access 与 Beginning Access 配置类似,不同之处在于提供了 Resolving 配置,结构如下:

typedef struct D3D12_RENDER_PASS_ENDING_ACCESS {
  D3D12_RENDER_PASS_ENDING_ACCESS_TYPE Type;
  union {
    D3D12_RENDER_PASS_ENDING_ACCESS_RESOLVE_PARAMETERS Resolve;
  };
} D3D12_RENDER_PASS_ENDING_ACCESS;

Ending Access Type 与 Beginning Access Type 类似,也可以直接映射到 VulkanMetal 的 Save Action。而 Resolve 参数是本篇文章关注的重点,用 RenderPass 来实现自动 Resovling 就在于此。Resolve 的结构定义如下:

typedef struct D3D12_RENDER_PASS_ENDING_ACCESS_RESOLVE_PARAMETERS {
  ID3D12Resource                                                       *pSrcResource;
  ID3D12Resource                                                       *pDstResource;
  UINT                                                                 SubresourceCount;
  const D3D12_RENDER_PASS_ENDING_ACCESS_RESOLVE_SUBRESOURCE_PARAMETERS *pSubresourceParameters;
  DXGI_FORMAT                                                          Format;
  D3D12_RESOLVE_MODE                                                   ResolveMode;
  BOOL                                                                 PreserveResolveSource;
} D3D12_RENDER_PASS_ENDING_ACCESS_RESOLVE_PARAMETERS;

与 ResolveSubresourceRegion 函数类似,可以指定 Resovling 区域和模式,区域在 pSubresourceParameters 参数中指定。最后一个参数 PreserveResolveSource 指示在 Resolving 完成后是否保留原始 MultiSample 的内容,如果为 FALSE 则会被丢弃。

在使用 D3D12 Render Pass 实现自动 Resolving 的过程中,由于全网没有任何可以参考的文档和 Sample Code(官方没有提供),甚是折腾了一番,最后总算调试通过,所以 GGE 也有可能是首个使用 D3D12 Render Pass 实现 MSAA 的图形引擎。主要有以下几个坑点需要注意:

  • 必须指定 pSubresourceParameters 参数,否则无法 Resovling。
  • Depth、Stencil 需要分别配置 Resolving 参数,如果 DS Format 中没有包含 Stencil,则无需指定参数,否则驱动会报错。
  • 对接收 Resolving 结果的 Resolve RTDS 必须手动设置 ResourceBarrier 进行同步,保证过渡到对应的 RESOLVE_DEST State 上。

3、Shader Resovling,这个方法与 Vulkan Shader Resolving 的方案基本一致,在 Shader 中使用 Samplerless 的 Load 指令,获取每个 Sample 对应的数据进行自定义 Resovling。但这种方案的缺点与 Vulkan 一样,就不再赘述。


Metal

Metal 使用 MTKView 提供了类似于 VulkanD3D12 的 Swapchain 的功能,包括 Drawable 的 textures、默认的 DepthStencil texture,也提供了用于 MSAA 的 texture。当指定 MTKView 的 sampleCount 属性大于 1 时,MTKView 会自动创建 multisampleColorTexture,并在 MTKView 提供的缺省 RenderPass 中自动执行 Resovling,因此对于 present drawable 的 MSAA 来说,简单设置 MTKView 即可实现 MSAA,在这一点上 Metal 保持了传统 API 的简单性。但是对于 attachment 的 MSAA 来说,Metal 提供了与 VulkanD3D12 类似的机制,不过是这三者中最简单的。因此,在 Metal 中对 RenderPass 的 attachment 启用 MSAA 也是 3 个步骤:

  1. Metal 资源的 MultiSample 配置。
  2. Metal Render Pipeline Descriptor 的 MultiSample 配置。
  3. 进行 Resolving。

第一步:首先在创建 MultiSample 的 MTLTexture 时需指定 2DMS texture 类型(最新的 iOS14 系统支持 2D MS 数组)。其次是指定 MTLTextureDescriptor 的 sampleCount,通过 MTLDevice 的 supportsTextureSampleCount 函数可获取支持的 sampleCount,下图是 Metal 官方文档中列出的设备支持的情况:

efcfab4cd309e18b0f0fae3e7328e5fd.png
从图中可知,所有 Metal 设备支持都支持 1x 和 4xMS,所有 iOS 设备支持 2xMS,iOS不支持 8xMS,这与 Vulkan 在移动平台基本一致

第二步:在创建 MTLRenderPipelineState 的 MTLRenderPipelineDescriptor 结构中的 sampleCount 字段,设置为第一步中指定的 Sample 数量,与第一步相同,需要查询设备是否支持设置的 Sample Count。

第三步:执行 Resolving,在 Metal 中 Resovling 有二种方法:通过 Render Pass Descriptor 自动执行解析和手动 Shader 解析。

1、Render Pass Descriptor 自动执行解析。Metal 中的配置非常简单,只需设置 MTLRenderPassAttachmentDescriptor 的 resolveTexture属性即可,同时这个 Descriptor 还提供了其他 resolve 属性设置,比如 level、slice、plane,可指定 texture 的一部分来执行 resolving。

接下来还需要设置 Render Pass attachment 的 MTLStoreAction,如果不需要保留原始 MultiSample 内容,则设置为 MTLStoreActionMultisampleResolve,如果需要保留,则设置为 MTLStoreActionStoreAndMultisampleResolve。在 iOS 平台上,如果不保留原始 MultiSample 内容,则可以设置 MultiSample 的 texture 存储模式为 Memoryless,这样可进一步减少内存开销,实现 MSAA 的近乎“零”开销,这也是苹果官方极力推荐的优化 MSAA 的手段。

此外还可以设置 Resolve Mode,在 Metal 中对 Depth 和 Stencil 的 ResovleMode 是分开定义的,Depth 对应 MTLMultisampleDepthResolveFilter,有 Sample0、Min、Max 三种可选;Stencil 对应 MTLMultisampleStencilResolveFilter可以选择与 Depth 相同的 Mode, 也可以选择 Stencil 的第一个采样点。

2、Shader Resovling,这个方法与 VulkanD3D12 Shader Resolving 的方案基本一致,在 Shader 中使用 Samplerless 的 read 函数,获取 Sample 索引对应的数据自定义 Resovling 算法。由于 iOS 平台也是基于 Tile Cache 架构,因此在 iOS 上使用通过不同的 Render Pass 进行 Shader Resolving 时,同样会存在内存和带宽开销的问题。不过可以合并 pass 并使用 Programmable Blending 技术实现在一个 pass 内实现 Resolving,这样就可以避免这个问题,只是实现上会更加复杂。

如果只对 MTKView 的 drawable 的 texture 使用 MSAA,有个坑点需要注意,当指定 MTKView 的 sampleCount 属性大于 1 时会自动创建 multisampleColorTexture 用来承载 MultiSample 数据,由于是自动创建,无法设置 storage mode 为 memoryless,因此在这种情况下开启 MSAA,会多 2 * SampleCount 倍的内存开销,如果不想有这样的开销,可以手动创建 Memoryless 的 MS Color、MS Depth 以及 Color Resolve texture,通过 RenderPass 自动解析后再用 MTKView 的 RenderPass present,这样会多创建一个 resolve texture,比并且会多一次 pass。