HLSL语法小记(持续更新~)

最近在使用HLSL实现光照效果, 有一些设置与语法糖需要注意一下~

参考材料
1. DirectX11–HLSL语法入门
2. for Statement
3. if Statement
4. C++性能榨汁机之循环展开

1. 如何为VS或者PS传递Constant Buffers?

在stackexchange上看到一个比较靠谱的回答: How does Direct3D know if a constant buffer is for the vertex or pixel shader?

某答主的回答就不翻译了, 否则反而会觉得很违和:

The key step that you’re missing is the implicit slot assignment that occurs when you compile a shader. When you compile an HLSL shader that contains a bindable object (be it a Texture2D, RWStructuredBuffer, or cbuffer), each object must be assigned a slot number. This corresponds to the UINT StartSlot parameter to e.g. VSSetConstantBuffers. You can optionally declare a slot explicitly using e.g. : register(c0) as a declaration suffix, but if you omit the slot, the compiler will assign the lowest free slot available.

As for your original question, how DidrectX “knows” what cbuffer to bind, well it doesn’t. What’s actually happening is that since in your vertex shader, you only refer to one cbuffer, that gets assigned slot 0 for the vertex shader when you compile it, and the other is ignored. Likewise, the same occurs for the pixel shader, except the other cbuffer is assigned to slot 0. This means that when you assign buffers to the 0 StartSlot for both the vertex and pixel shader stages, it just so happens that those are the right buffers for those shaders. Basically, it just happens to work because slot 0 is always the right slot. Once you get to more advanced shaders that reference multiple cbuffers, you’ll want to get into the habit of assigning slots explicitly.

总结一下, 便是使用: register(c0)语法为Constant Buffer分配插槽, 然后使用VSSetConstantBuffers或者PSSetConstantBuffers告诉VS或者PS需要传递的Constant Buffer是哪一块. 它和GLSL中的uniform变量其实是有着异曲同工之妙的: 因为在GLSL中, 也是需要分别在VS或者PS中分别声明uniform变量, 来告诉当前shader需要使用的变量. 只不过感觉D3D是把这个声明过程放到了CPU上做. 这个区别见仁见智叭~

2. 语法糖(估计会持续更新)

条件语句
HLSL也支持if, else, continue, break, switch关键字, 此外discard关键字用于像素着色阶段抛弃该像素.

条件的判断使用一个布尔值进行, 通常由各种逻辑运算符或者比较运算符操作得到. 注意向量之间的比较或者逻辑操作是得到一个存有布尔值的向量, 不能够直接用于条件判断, 也不能用于switch语句.

判断与动态分支
基于值的条件分支只有在程序执行的时候被编译好的着色器汇编成两种方式: 判断(predication) 和动态分支(dynamic branching).

如果使用的是判断的形式, 编译器会提前计算两个不同分支下表达式的值, 然后使用比较指令来基于比较结果来”选择”正确的值.

而动态分支使用的是跳转指令来避免一些非必要的计算和内存访问.

着色器程序在同时执行的时候应当选择相同的分支, 以防止硬件在分支的两边执行. 通常情况下, 硬件会同时将一系列连续的顶点数据传入到顶点着色器并行计算, 或者是一系列连续的像素单元传入到像素着色器同时运算等.

动态分支会由于执行分支指令所带来的开销而导致一定的性能损失, 因此要权衡动态分支的开销和可以跳过的指令数目.

通常情况下编译器会自行选择使用判断还是动态分支, 但我们可以通过重写某些属性来修改编译器的行为. 我们可以在条件语句前可以选择添加下面两个属性之一:
[branch] 根据条件值的结果, 只计算其中一边的内容, 会产生跳转指令. 默认不加属性的条件语句为branch型.
[flatten] 两边的分支内容都会计算, 然后根据条件值选择其中一边. 可以避免跳转指令的产生.
用法如下:

[flatten]
if(...){
    ...
}

循环语句
HLSL也支持for,while和do while循环.和条件语句一样,它可能也会在基于运行时的条件值判断而产生动态分支,从而影响程序性能.如果循环次数较小,我们可以使用属性[unroll]来展开循环,代价是产生更多的汇编指令.

循环展开对程序性能有着很重要的影响,可以减少分支预测错误次数,增加取消数据相关进一步利用并行执行提高速度的机会.但是,并不建议大家进行手动的循环展开,在代码中进行循环展开会导致程序的可读性下降,代码膨胀.其实在我们开启了编译器优化的时候,编译器会自动对我们的循环代码进行循环展开,让我们可以在保持了代码可读性的同时,又能享受到循环展开对我们程序性能的提高.

PS:在VS里面可以手动选择关闭编译器优化(虽然我觉得一般没人这么作死叭……),如下图所示:

用法如下:

[unroll]
while(...){
    ...
}

若没有添加属性, 默认使用的则为[loop].

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注