ShaderJoy —— 如何阅读着色器的汇编代码(一)

575 阅读4分钟

正文

当我开始图像编程时,像 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 ends_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 是如何转换为汇编的。

未完待续