【转载】RenderDoc 截帧 DXBC 编译 一文详解

1,644 阅读7分钟

原文链接

原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式以及部分有误的地方进行了修正,方便自己日后阅读

上篇

Hello . 大家好
今天给大家带来的是 一文详解 RenderDoc 截帧 DXBC 编译我是守城的麦田!

1 DXBC 简介

DXBC 指令是 D3D 着色器语言使用的指令HLSL 高级着色语言经过编译器编译之后,会生成相应的 DXBC 指令。DXBC 指令可以理解为 GPU 需要真正执行的指令。 OpenGL 或者说是其它 GPU 厂商,他们提供的指令其实跟 DXBC 大同小异,略微有差异的也只是某些特殊的指令不是硬件支持而已。虽然编译器大部分情况下可以帮助我们优化代码,但是由于编译器特别智能,在某些情况下并不能保证代码是最优的方式。了解 DXBC 指令可以帮助我们在编写 Shader 时,能够写出更加可靠、性能优异的代码。另外了解 DXBC 指令,在某些情况下可以帮助我们更好的逆向其它游戏的一些材质效果。

2 开始准备

DXBC 指令其实非常简单,学习起来也非常容易,它的套路可以说是非常的单一,大多数指令可以归纳为这样的形式:

下面是一个片段着色器的编译结果

图片

我们这次测试的最基本的 Shader 如下

Pass
{
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

    struct Attributes
    {
        float4 positionOS : POSITION;
    };

    struct Varyings
    {
        float4 positionHCS : SV_POSITION;
    };

    CBUFFER_START(UnityPerMaterial)
    half4 _BaseColor;
    CBUFFER_END

    Varyings vert(Attributes IN)
    {
        Varyings OUT;
        OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
        return OUT;
    }

    half4 frag() : SV_Target
    {
        return _BaseColor;
    }
    ENDHLSL
}

3 片元阶段分析   

挂载上 RenderDoc 后,找到对应的渲染 Pass

图片

当前 Shader 只包含顶点和片元着色器,我们先从 PS 阶段读起,PS 阶段代码如下所示:

图片

ps_4_0
      dcl_constantbuffer cb0[1], immediateIndexed
      dcl_output o0.xyzw
   0: mov o0.xyzw, cb0[0].xyzw
   1: ret

DXBC 外部传入的变量

以下涉及到的 DXBC 官方解释链接

ps_4_0

本部分包含像素着色器版本 4_0 实现的输入和输出寄存器的参考信息,即当前的 HLSL 寄存器版本为 4.0

dcl_constantbuffer cb0[1]

声明着色器 常量缓冲区 ,这句话表明声明寄存器 cb0 的常量缓冲区,其中包含 1 个元素。可以使用文本索引访问这些元素。

immediateIndexed

使用文本值为缓冲区编制索引。

输出

dcl_output 声明一个着色器输出寄存器。

o0.xyzwo0o1o2 这种类型的寄存器,为输出寄存器,整个 DXBC 内要去猜测这个寄存器的含义。

mov 指令

使用句式:mov dst, src

dst 是目标寄存器。src 是一个源寄存器。即在源寄存器和目标寄存器内移动数据。0: mov o0.xyzw, cb0[0].xyzw 这里将 常量缓冲区 内的数据移动到 目标缓冲区

ret 指令

对于主函数,此指令将停止着色器执行。

4 顶点阶段分析

vs_4_0
      dcl_constantbuffer cb0[77], immediateIndexed
      dcl_constantbuffer cb1[4], immediateIndexed
      dcl_input v0.xyz
      dcl_output_siv o0.xyzw, position
      dcl_temps 2
   0: mul r0.xyzw, v0.yyyy, cb1[1].xyzw
   1: mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw
   3: add r0.xyzw, r0.xyzw, cb1[3].xyzw
   4: mul r1.xyzw, r0.yyyy, cb0[74].xyzw
   5: mad r1.xyzw, cb0[73].xyzw, r0.xxxx, r1.xyzw
   6: mad r1.xyzw, cb0[75].xyzw, r0.zzzz, r1.xyzw
   7: mad o0.xyzw, cb0[76].xyzw, r0.wwww, r1.xyzw
   8: ret

dcl_input 指令

声明着色器输入寄存器

  • 语法:dcl_input vN[.mask]
  • N 是标识寄存器号的整数。
  • [.mask] 是一个可选组件掩码 (.xyzw) ,指定要使用的注册组件。

此处的 dcl_input v0.xyz 即对应 Shader 内的 float4 positionOS : POSITION;

dcl_output_siv 指令

  • 声明包含 系统值 参数的输出寄存器。
  • 语法:dcl_output_siv oN[.masks], systemValue

图片

所以这里的 dcl_output_siv o0.xyzwposition 即代表 Varyings 下的 positionHCS

接下来就是一系列的矩阵转换,从 模型空间 转换到 齐次裁剪空间,分别乘 MVP 矩阵,Unity 内写法为OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz) ;

拆解一下这句,如下

图片

上面注释的一句等价于下面三句。其中OptimizeProjectionMatrix(glstate_matrix_projection) 是由 UniversalRenderPipelineCore.cs 文件下的 ShaderPropertyId 类传入的, 用于根据平台特殊处理的矩阵。

public static class ShaderPropertyId
    {
    ...
        public static readonly int viewMatrix = Shader.PropertyToID("unity_MatrixV");
        public static readonly int projectionMatrix = Shader.PropertyToID("glstate_matrix_projection");
    ...
    }

dcl_temps 指令

声明临时寄存器。

  • 语法:dcl_temps N N 为临时寄存器个数
  • 每个寄存器都有一个 32 位四分量值的空间。临时和 可索引临时 寄存器的总数 必须小于或等于 4096
  • 此处的 dcl_temps 2 即申明了两个临时寄存器 r0-r1。下面需要用到。

mul 指令

  • 语法 mul dst、src0、src1dst 是目标寄存器。
    • src0 是源寄存器。
    • src1 是源寄存器。
    • dest.x = src0.x * src1.x;
  • mul r0.xyzw, v0.yyyy, cb1[1].xyzw 这条指令的意思为:r0.xyzw = v0.yyyy * cb1[1].xyzw

mad 指令

  • 乘以并添加源。
  • 语法:mad dst, src0, src1, src2dst 是目标寄存器。
    • src0 是源寄存器。
    • src1 是源寄存器。
    • src2 是源寄存器。
  • 举例:dst.x = src0.x * src1.x + src2.x;
  • mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw 这条指令的意思为:r0.xyzw = cb1[0].xyzw * v0.xxxx + r0.xyzw

add指令

  • 两个向量相加。
  • 语法:add dst, src0, src1dst 是目标寄存器。
    • src0 是源寄存器。
    • src1 是源寄存器。
  • 举例:dst.x = src0.x + src1.x;
  • add r0.xyzw, r0.xyzw, cb1[3].xyzw 这条指令的意思为:r0.xyzw = r0.xyzw + cb1[3].xyzw

分析

   0: mul r0.xyzw, v0.yyyy, cb1[1].xyzw
   1: mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw
   3: add r0.xyzw, r0.xyzw, cb1[3].xyzw

为了更加具象点,把上面这段 DXBC 翻译一下:

   0: r0.xyzw = v0.yyyy * cb1[1].xyzw;
   1: r0.xyzw = cb1[0].xyzw * v0.xxxx + r0.xyzw
   2: r0.xyzw = cb1[2].xyzw * v0.zzzz + r0.xyzw
   3: r0.xyzw = r0.xyzw + cb1[3].xyzw

这里在原 Shader 内只是做了一系列的矩阵转换,但是 DXBC 会默认将 HLSL 替换为 Vector4 类型,包括向量,由推导可知,这里的 cb1[4]M 矩阵,怎么来的呢?

  • dcl_constantbuffer cb1 可知,cb1[4] 是由外部传入的参数类型
  • 我在 Shader 内只计算 M 矩阵,汇编后如下图所示:
  • 图片
  • 由此,可推断出:cb1[4] 正是 unity_ObjectToWorld,而cb1[1]——cb1[3] 分别对应 M 矩阵的每一列。
  • unity_ObjectToWorld 是一个 4X4 的矩阵,positionOS 则是一个 4X1 的矩阵(Unity 内使用列向量,矩阵左乘)
  • 那么一切都了然了,回想起矩阵乘法,两个矩阵 A 和 B 相乘,需要满足 A 的列数等于 B 的行数。A 矩阵的行元素乘以每一列然后相加作为新矩阵的行元素

继续看下面:

    4: mul r1.xyzw, r0.yyyy, cb0[74].xyzw
    5: mad r1.xyzw, cb0[73].xyzw, r0.xxxx, r1.xyzw
    6: mad r1.xyzw, cb0[75].xyzw, r0.zzzz, r1.xyzw
    7: mad o0.xyzw, cb0[76].xyzw, r0.wwww, r1.xyzw
  • 依此类推,范围 cb0[73]——[76] 则代表 VP 矩阵 unity_MatrixVP
  • 范围 cb0[61]——[64] 则代表 V 矩阵 unity_MatrixV
  • 范围 cb0[57]——[60] 则代表 P 矩阵 UNITY_MATRIX_P
  • 最后输出 ret 结束

到此,本次截帧分析的基础 Shader 已经结束。下篇分析,如果存在贴图等类型的输入分析。

下篇

1 获取内容

相比上篇,只是将上篇的颜色输入增加了贴图类型输入。下图为 Unity 内预览效果。

图片

还是对其进行 RenderDoc,获得到的 DXBC 顶点 如下所示:

vs_4_0
      dcl_constantbuffer cb0[77], immediateIndexed
      dcl_constantbuffer cb1[4], immediateIndexed
      dcl_constantbuffer cb2[1], immediateIndexed
      dcl_input v0.xyz
      dcl_input v1.xy
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xy
      dcl_temps 2
   0: mul r0.xyzw, v0.yyyy, cb1[1].xyzw
   1: mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw
   3: add r0.xyzw, r0.xyzw, cb1[3].xyzw
   4: mul r1.xyzw, r0.yyyy, cb0[74].xyzw
   5: mad r1.xyzw, cb0[73].xyzw, r0.xxxx, r1.xyzw
   6: mad r1.xyzw, cb0[75].xyzw, r0.zzzz, r1.xyzw
   7: mad o0.xyzw, cb0[76].xyzw, r0.wwww, r1.xyzw
   8: mad o1.xy, v1.xyxx, cb2[0].xyxx, cb2[0].zwzz
   9: ret

DXBC 片元 如下所示:

ps_4_0
      dcl_constantbuffer cb0[20], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v1.xy
      dcl_output o0.xyzw
   0: sample_b o0.xyzw, v1.xyxx, t0.xyzw, s0, cb0[19].x
   1: ret

原 Unity Shader 如下所示:

图片

2 开始分析

VS 阶段

相比上篇,Data 阶段多输入了一个 UV 信息,即 float2 uv : TEXCOORD0; 对应 DXBC 的 dcl_constantbuffer cb2[1], immediateIndexed

UT.uv = TRANSFORM_TEX(IN.uv, _BaseMap); 对应 VS 阶段内的计算 mad o1.xy, v1.xyxx, cb2[0].xyxx, cb2[0].zwzz

由上篇分析的 mad 指令,这里直接可翻译为:o1.xy = v1.xyxx * cb2[0].xyxx + cb2[0].zwzz,即:Unity 内对 TRANSFORM_TEX 的预定义: #define TRANSFORM_TEX(tex, name) ((tex.xy) * name##_ST.xy + name##_ST.zw)

PS 阶段

dcl_sampler 指令

声明采样器寄存器。官方解释

  • 语法:dcl_sampler sN,模式   
  • 示例:dcl_sampler s3, default

图片

dcl_resource_texture2d 指令

声明一个 Texture2D 类型的贴图

dcl_input 指令

上篇介绍了 dcl_input 指令第一种用法声明着色器输入寄存器语法:dcl_input vN[.mask]

  • N 是标识寄存器号的整数。
  • [.mask] 是一个可选组件掩码 (.xyzw) ,指定要使用的注册组件。

这里新增一个 interpolationMode 用法,该用法为可选模式有以下几种输入:

  • constant: 不在寄存器的值之间插值。
  • linear: 在寄存器值之间线性插值。
  • linearCentroid: 与线性相同,但是质心在多重采样时被截断。
  • linearNoperspective: 与线性相同,但没有透视校正。
  • linearNoperspectiveCentroid: 与线性相同,但是质心在多重采样时被截断,无透视校正。

dcl_input_ps linear v1.xy: 对应的正是 VS 阶段传过来的 UV 数据。

dcl_output 指令

声明着色器输出寄存器。

sample_b 指令

采样贴图,官方解释如下:

图片

由上图可知语法为:sample_b[_aoffimmi (u,v,w) ] dest[.mask], srcAddress[.swizzle],srcResource[.swizzle], srcSampler, srcLODBias.select_component

sample_b o0.xyzw, v1.xyxx, t0.xyzw, s0, cb0[19].x 则分别对应,最后的 输出值UV采样贴图采样器常量缓冲区范围

至此,RenderDoc 截帧 DXBC 编译 一文详解分析结束,其他的渲染情况大家可以依据官方文档举一反三。