为啥在 Metal 中不允许从片段着色器中写入缓冲区?

Posted

技术标签:

【中文标题】为啥在 Metal 中不允许从片段着色器中写入缓冲区?【英文标题】:Why is writing to a buffer from within a fragment shader disallowed in Metal?为什么在 Metal 中不允许从片段着色器中写入缓冲区? 【发布时间】:2016-03-31 07:44:25 【问题描述】:

如Metal Shading Language Guide中所述:

片段函数不允许写入缓冲区或纹理。

我知道是这种情况,但我很好奇为什么。能够从片段着色器中写入缓冲区非常有用;我知道,在硬件端提前不知道特定线程的内存写入的结束位置可能会更复杂,而原始缓冲区写入并不总是知道这一点,但这是 Metal 计算中公开的一项功能着色器,那么为什么不在片段着色器中呢?

附录

我应该澄清为什么我认为片段函数的缓冲区写入很有用。在光栅化管道的最常见用例中,三角形被光栅化和着色(根据片段着色器)并写入预定义的内存位置,在每次片段着色器调用之前 已知,并由来自的预定义映射确定标准化的设备坐标和帧缓冲区。这适合大多数用例,因为大多数时候您只想将三角形​​直接渲染到缓冲区或屏幕上。

在其他情况下,您可能希望在片段着色器中进行延迟写入,其结束位置基于片段属性而不是片段的确切位置;有效地,光栅化有副作用。例如,大多数基于 GPU 的体素化工作是通过从某个理想角度使用正交投影渲染场景,然后写入 3D 纹理,将片段的 XY 坐标及其相关深度值映射到 3D 纹理中的某个位置。这被描述为here。

其他用途包括某些形式的与顺序无关的透明度(绘制顺序不重要的透明度,允许重叠的透明对象)。一种解决方案是使用多层帧缓冲区,然后在单独的通道中根据片段的深度值对片段进行排序和混合。由于没有硬件支持这样做(在大多数 GPU 上,我相信英特尔有硬件加速),你必须维护原子计数器和每个像素的手动纹理/缓冲区写入,以协调对分层帧缓冲区的写入。

另一个例子可能是通过光栅化为 GI 提取虚拟点光源(即,在光栅化时为相关片段写出点光源)。在所有这些用例中,都需要从片段着色器写入缓冲区,因为 ROP 只为每个像素存储一个结果片段。如果没有此功能,获得等效结果的唯一方法是通过某种方式的深度剥离,这对于深度复杂度高的场景来说非常缓慢。

现在我意识到,我给出的示例并不是所有关于缓冲区写入的具体内容,而是更普遍地关于从片段着色器进行动态内存写入的想法,理想情况下还包括对原子性的支持。缓冲区写入似乎是一个简单的问题,包含它们将大大改善这种情况。

由于我在这里没有得到任何答案,我最终选择了posting the question on Apple's developer forums。我在那里得到了更多的反馈,但仍然没有真正的答案。除非我遗漏了什么,否则似乎几乎所有正式支持 Metal 的 OS X 设备都具有对此功能的硬件支持。据我了解,此功能于 2009 年左右首次出现在 GPU 中。这是当前 DirectX 和 OpenGL 中的一个共同功能(甚至不考虑 DX12 或 Vulkan),因此 Metal 将是唯一缺少它的“尖端”API .

我意识到 PowerVR 硬件可能不支持此功能,但 Apple 在按功能集区分金属着色语言方面没有问题。例如,ios 上的 Metal 允许在片段着色器中“免费”获取帧缓冲区,这在缓存重的 PowerVR 架构中直接得到硬件支持。此功能直接体现在 Metal Shading Language 中,因为它允许您使用 iOS 着色器的 [[color(m)]] 属性限定符声明片段函数输入。可以说,允许使用 device 存储空间限定符声明缓冲区,或使用 access::write 声明纹理作为片段着色器的输入,这对语言的语义变化不会比苹果为 iOS 优化所做的更大。因此,就我而言,PowerVR 缺乏支持并不能解释我在 OS X 上寻找的功能的缺失。

【问题讨论】:

【参考方案1】:

现在支持从片段着色器写入缓冲区,如在 What’s New in iOS 10, tvOS 10, and macOS 10.12

函数缓冲区读写适用于:iOS_GPUFamily3_v2, OSX_GPUFamily1_v2

片段函数现在可以写入缓冲区。可写缓冲区必须是 在设备地址空间中声明,并且不能是 const。采用 动态索引写入缓冲区。

此外,Metal Shading Language Specification 2.0 中没有指定限制的行(来自原始问题)

【讨论】:

【参考方案2】:

我认为您既不能在 OpenGL 或 DirectX 上的片段函数上写入任意像素或纹素。一件事是渲染 API,另一件事是片段或顶点函数。

片段函数旨在产生一个像素/纹素输出,每次运行一个,甚至每个都有多个通道。通常,如果您想写入缓冲区或纹理,则需要在表面(缓冲区或纹理)上使用片段函数渲染某些内容(四边形、三角形或其他内容。结果,每个像素/纹素将使用您的片段函数渲染. 例如光线投射或光线追踪片段函数通常使用这种方法。

不允许您编写任意像素/纹素有一个很好的理由:并行化。片段函数通常在大多数 GPU 上以非常高的并行化模式一次针对许多不同的像素/纹素执行,每个 GPU 都有自己的并行化方式(SMP,矢量...),但都执行非常高的并行化.因此,您可以只通过返回一个像素或纹素通道输出作为片段函数的返回来编写,以避免常见的并行化问题,如竞赛。这适用于我知道的每个图形库。

【讨论】:

有道理。因为片段着色器的许多实例并行运行,所以它们可以写入的任何外部资源都必须被序列化以避免冲突。输出片段是不同的,因为根据定义,片段着色器的每次运行都会写入渲染目标上不同的独占像素。

以上是关于为啥在 Metal 中不允许从片段着色器中写入缓冲区?的主要内容,如果未能解决你的问题,请参考以下文章

从片段着色器中的地形高程数据计算法线

根据顶点世界位置Y坐标修改Metal片段着色

OpenGL 片段着色器未写入 fbo 颜色缓冲区

在 Metal 中,将顶点和片段缓冲区设置为相同的 MTLBuffer 是不是仅将其复制到 GPU 一次?

帧缓冲纹理出现白色(片段着色器不影响它)

在着色器中使用 glTexture