原文链接
原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式以及部分有误的地方进行了修正,方便自己日后阅读
上篇
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.xyzwo0,o1,o2 这种类型的寄存器,为输出寄存器,整个 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.xyzw, position 即代表 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 NN为临时寄存器个数 - 每个寄存器都有一个 32 位四分量值的空间。临时和 可索引临时 寄存器的总数 必须小于或等于
4096。 - 此处的
dcl_temps 2即申明了两个临时寄存器r0-r1。下面需要用到。
mul 指令
- 语法
mul dst、src0、src1,dst是目标寄存器。 -
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, src2,dst是目标寄存器。 -
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, src1,dst是目标寄存器。 -
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 编译 一文详解分析结束,其他的渲染情况大家可以依据官方文档举一反三。