GPU 时代的 C-style 字符串

更正 (2017-10-13):如果你依赖本文提供的关于 Metal 的信息,请务必阅读《GPU 时代的 C-style 字符串 —— 再度绊倒》对本文的更正。

由来

曾经有个问题征求答案 ——「计算机系统早期发展的先驱影响最大的决策失误是什么?」很多人赞同以 '\0' 结束的 C-style 字符串。随着计算机解决问题领域的扩展,新领域也会面对各自「早期发展先驱」带来的问题。或许每个时代都有自己的「C-style 字符串」问题。GPU 是过去二十年里「先驱」辈出的领域。从六七年前我的 team 开始接触基于 OpenGL 的产品,到今天自己写 renderer 一年有余,自己和身边的同事反复的被同一个问题绊倒。这个问题 —— alpha premultiplication 应该有资格被称为 GPU 时代的 C-style 字符串。

Alpha 的概念很容易理解。首先,有红绿蓝三原色 (R, G, B channels) 组成颜色。然后加上透明度 (alpha-channel)。透明度本身不会直接显示出来,因为显示器不是透明的,通常的场景也不会在无限远处「透明」。Alpha 是通过对「底色」的修改体现出来的 [1]。如果一个 pixel 颜色为 (r, g, b, a),底色为 (r’, g’, b’) (注意底色没有 alpha),最终结果是:

(r, g, b) * a + (r', g', b') * (1 - a)   [2]

一切概念都非常清晰完美!有那么好的事?C-style 字符串要登场了。

因为「先驱们」发现大多数应用的计算中 (r, g, b) * a 这个式子总是固定出现,并不会出现单独的 (r, g, b) 因子。于是先驱们决定原则上图片(特别是 GPU video memory 中的图片,即 texture)中不再存储最初的 (r, g, b, a),而是存储 (r*a, b*a, g*a, a),以预存储的方式节省乘法运算。这就是 alpha-premultiplied 形式。

到此为止,premultiplication 还只是某些优化情形下的推荐方式,并未上升到 C-style 字符串的影响力。但先驱们又决定,即然在众多时候都需要这个优化,今后 GPU 的 texture sampler,以及显卡的最终 on-screen 显示都假定 pixel 必须是 alpha-premultiplied 形式。

从此混乱开始了。当一个 render pass 的结果有可能被 resample 或者 on-screen 显示时,就必须储存成 premultiplied 形式。否则这个结果经过 GPU 的硬件处理就会产生错误。这导致了很多 shader 中不得不夹杂 premulplied 和 un-premultiplied 两种形式的数据。一份数据是不是 premultiplied 形式没有任何编译期或者运行期的类型信息说明,由于图形编程本身的特点,通过 debugging 来研究难度也很大。开发者只能从静态代码上下文推测。而且 premulitplication 操作可以或者在 fixed-function 部分设置,或者在 shader 中编写代码执行,更增大了通过上下文识别数据形式的难度。

Metal 的实现

说到这里具体谈谈 Metal 的实现。虽然从六七年前开始就被 alpha 相关问题不停绊倒,直到最近才在 Metal 上具体总结了一下。

Metal 的 fixed-function 部分缺省行为即执行 premultiplication。也就是说,在关闭 blending 时下面的 shader 代码,

会写入:float4(vert.rgb * vert.a, vert.a) 。这个行为并不对称,其逆操作 —— texture sampling 并不会自动 divided by alpha [3]。 这导致很多 shader 代码的操作不对称,必须查看 pipeline 的 fixed-function 参数才能理解,是降低 shader 代码清晰度的因素之一。

如果打开 fixed-function blending,写入的数据与 render target 的原有颜色有关。如果用纯黑色 (0, 0, 0, 0) clear 整个 color attachment,并如下设置 color attachment descriptor:

其结果和关闭 blending 的行为一致。上面的 MTLBlendFactorSourceAlpha 决定了对 render 结果执行 premultiplication。如果将其替换为 MTLBlendFactorOne,render 结果就是 un-premultiplied [4]。

未来

这么多年来,只要是显示或存储带透明度的图片,几乎没人能杜绝「黑边」的 bug [5]。而那些中间步骤的还藏有多少看上去不明显的透明度问题,不会有人知道。Premultiplication 绝对当之无愧作为「GPU 时代的 C-style 字符串」。如今在代码里看到越来越多的 std::string,也希望有一天 premultiplication 能从图形图像处理中完全消失。只是不知道当硬件性能充允的时候,已经积累的代码和习惯是否能允许剔除这个遗迹。

脚注:

  1. 如果你听过很多关于 Photoshop 的笑话,那么应该知道「透明底色」是棋盘格的颜色。
  2. 最终结果并没有 alpha-channel,因为为了描述简单没有考虑类似 Photoshop 中 blending group 的概念。归根结底,blending group 只是中间结果而不是最终显示。
  3. 相比之下,Metal 对 sRGB gamma encoded 数据会在 shader 输入输出时进行对称的 linear/delinear 变换。
  4. 当 texture 底色为 (0, 0, 0, 0) 的时候,图中最后两行对 color attachment 的设置并不会有什么作用。
  5. 通常是黑色高透明 pixel 忘记 premultiplication,或是浅色中等透明 pixel 过多进行 premultiplication。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s


%d 博主赞过: