正文
当我开始图像编程时,像 HLSL 和 GLSL 这样的着色语言在游戏开发中还不流行,着色器是用汇编直接开发出来的。 当 HLSL 被引入时,我记得我们为了好玩,试图通过手工生成更短、更紧凑的汇编代码来击败编译器,这并不难。 从那时起,着色器编译技术就有了巨大的进步,现在,在大多数情况下,手工生成更好的汇编代码是相当困难的(而且着色器已经变得如此庞大和复杂,无论如何,它不再是划算的了)。
即使现在没有人直接用汇编编写着色器,对于图形程序员来说,能够阅读和理解编译器产生的着色器汇编(ISA)代码仍然是有用的。 首先,它帮助你理解编译器是如何解释高级着色器指令的。 有些指令,如 tan()
或 整数除法
不能直接映射到硬件,可以扩展成许多汇编指令。 其次,它可以帮助理解 GPU 如何工作,如何请求数据,执行分支,写入输出等。 第三,当实际的着色器代码不可用时,它可以帮助调试着色器。 虽然我们通常不再手动调优着色器的汇编代码,但理解它可以帮助我们做出更好的高级着色器编写决策,这可以导致更高性能的汇编代码。 最后,对我个人来说,有时不带抽象层的阅读代码,能对代码的作用有一个清晰的理解,并尽可能直捣黄龙地接近它的本质。
在这篇博文中,我们将讨论一点着色器汇编,并提供一些如何阅读它的指引。 本讨论主要集中在 DirectX 和 HLSL ,类似的想法将适用于其他 API /着色语言。 同样在例子中,我使用的是 AMD 的着色器汇编(ISA),因为它有很好的文档,并且有很容易使用的工具,比如优秀的 Shader Playground,即使你并没有一个可访问的 AMD 的 GPU 。
在我们开始之前,有必要提一下着色器编译分为两个阶段: 首先,一个像 fxc 或 dxc 这样的工具将 HLSL 代码编译成 GPU 无关的格式,称为中间语言(IL)。 然后,GPU 驱动程序将 IL 转换为最终的着色器汇编(ISA),可以在特定的 GPU 上执行。 我们将关注 ISA ,而不是 IL,因为它更能代表实际执行的代码。 在下面的例子中,一个 HLSL 着色器 将两个数字相乘产生左边的 IL 代码和右边的 ISA 代码。 IL 代码仍然是相对高层次的,隐藏了很多实现细节。
中间语言(Intermediate Language) | GCN ISA | |
---|---|---|
il_ps_2_55 dcl_global_flags refactoringAllowed dcl_cb cb0[1] dcl_input_generic_interp(linear) v0 dcl_output_generic o0 mul_ieee r4096, v0, cb0[0] mov o0, r4096 ret_dyn end | s_mov_b32 m0, s8 s_buffer_load_dwordx4 s[0:3], s[4:7], 0x00 v_interp_p1_f32 v2, v0, attr0.x v_interp_p2_f32 v2, v1, attr0.x v_interp_p1_f32 v3, v0, attr0.y v_interp_p2_f32 v3, v1, attr0.y v_interp_p1_f32 v4, v0, attr0.z v_interp_p2_f32 v4, v1, attr0.z v_interp_p1_f32 v0, v0, attr0.w v_interp_p2_f32 v0, v1, attr0.w s_waitcnt lgkmcnt(0) v_mul_f32 v1, s0, v2 v_mul_f32 v2, s1, v3 v_mul_f32 v3, s2, v4 v_mul_f32 v0, s3, v0 v_cvt_pkrtz_f16_f32 v1, v1, v2 v_cvt_pkrtz_f16_f32 v0, v3, v0 exp mrt0, v1, v1, v0, v0 done compr vm s_endpgm end |
让我们考虑这个虚构的 HLSL 着色器。 尽管它没有做任何有用的事情,但它使用了许多在更实际的场景中会使用到的语言特性,如属性插值、常量缓冲区、纹理读取、数学运算和分支:
struct PSInput
{
float2 uv : TEXCOORD;
};
cbuffer cbData
{
float4 data;
}
Texture2D<float4> tex;
SamplerState samplerLinear;
float4 PSMain(PSInput input) : SV_TARGET
{
float4 result = tex.Sample(samplerLinear, input.uv);
float factor = data.x * data.y;
if( factor > 0 )
return data.z * result;
else
return data.w * result;
}
这是它使用针对 AMD 的 GCN GPU 架构的 Radeon GPU 分析器生成的着色器汇编代码:
s_mov_b32 m0, s20
s_mov_b64 s[22:23], exec
s_wqm_b64 exec, exec
v_interp_p1_f32 v2, v0, attr0.x
v_interp_p2_f32 v2, v1, attr0.x
v_interp_p1_f32 v3, v0, attr0.y
v_interp_p2_f32 v3, v1, attr0.y
s_and_b64 exec, exec, s[22:23]
image_sample v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf
s_buffer_load_dwordx4 s[0:3], s[16:19], 0x00
s_waitcnt lgkmcnt(0)
v_mov_b32 v4, s1
v_mul_f32 v4, s0, v4
v_cmp_lt_f32 vcc, 0, v4
s_cbranch_vccz label_0017
s_waitcnt vmcnt(0)
v_mul_f32 v0, s2, v0
v_mul_f32 v1, s2, v1
v_mul_f32 v2, s2, v2
v_mul_f32 v3, s2, v3
s_branch label_001C
label_0017:
s_waitcnt vmcnt(0)
v_mul_f32 v0, s3, v0
v_mul_f32 v1, s3, v1
v_mul_f32 v2, s3, v2
v_mul_f32 v3, s3, v3
label_001C:
s_mov_b64 exec, s[22:23]
v_cvt_pkrtz_f16_f32 v0, v0, v1
v_cvt_pkrtz_f16_f32 v1, v2, v3
exp mrt0, v0, v0, v1, v1 done compr vm
s_endpgm
end
乍一看,它看起来像一堆神秘的指令和数字,但让我们先尝试在两个着色器之间对相应区域进行颜色编码,以大致了解 HLSL 是如何转换为汇编的。
未完待续