为什么顺序在着色器中很重要?

Why does order matter in shaders?

快速说明

这个问题有 C++ 标签,因为 C++ 中使用 DirectX 的开发人员多于 C# 中的开发人员。我认为这个问题与任何一种语言都没有直接关系,而是与使用的类型(据我了解完全相同)或 DirectX 本身以及它如何编译着色器有关。如果在 C++ 工作的人知道更好、更具描述性的答案,那么我更喜欢那个而不是我自己的答案。我懂这两种语言,但主要使用 C#


概述

HLSL 着色器中,在设置我的常量缓冲区时,我 运行 遇到了一个相当特殊的问题。有问题的原始常量缓冲区设置如下:

cbuffer ObjectBuffer : register(b0) {
    float4x4 WorldViewProjection;
    float4x4 World;
    float4x4 WorldInverseTranspose;
}

cbuffer ViewBuffer : register(b1) {
    DirectionalLight Light;
    float3 CameraPosition;
    float3 CameraUp;
    float2 RenderTargetSize;
}

如果我交换 b0b1 寄存器,渲染将不再有效 (e1)。如果我不理会这些寄存器,并再次交换 WorldWorldViewProjection 之间的顺序,渲染将不再有效 (e2)。但是,只需将 ViewBuffer 移动到 HLSL 文件中 ObjectBuffer 的上方而不进行其他修改,它就可以正常工作。

现在,我认为寄存器的放置相当重要,第一个寄存器 b0 需要该缓冲区中给出的三个属性,而且我知道 HLSL 常量缓冲区需要位于16 字节块。但是,这给我留下了一些问题。


问题

鉴于 HLSL 期望常量缓冲区位于 16 字节块中;

float4x4 类型与 Matrix 类型本质上是数组数组的类型不一样吗?

[ 0, 0, 0, 0 ] = 16 bytes
[ 0, 0, 0, 0 ] = 16 bytes
[ 0, 0, 0, 0 ] = 16 bytes
[ 0, 0, 0, 0 ] = 16 bytes
[    TOTAL   ] = 64 bytes

因为 float 本身是 4 个字节,这意味着 float4 是 16 个字节,因此 float4x4 是 64 个字节。那么,如果大小保持不变,为什么顺序很重要?

快速说明

我目前正在对问题做进一步的分析,以便给出更详细准确的答案。当我发现更多细节时,我会更新问题和答案以尽可能准确地反映。


基本答案

上述问题的确切问题(在 posting 时未知)是 HLSL 缓冲区与其 C# 表示不匹配;因此变量的重新排序导致着色器失败。但是,我仍然不确定为什么类型相同。我在寻找答案的过程中了解到了一些其他事情,并决定在这里post它们。


为什么顺序很重要

经过进一步的研究和测试,我仍然不能 100% 确定类型都相同的背后原因。总的来说,我认为这可能是由于 cbuffer 中的预期类型和 struct 中类型的顺序。在这种情况下,如果您的 cbuffer 首先需要 bool 然后是 float,那么重新排列会导致问题。

cbuffer MaterialBuffer : register(b0) {
    bool HasTexture;
    float SpecularPower;
    float4 Ambient;
    ...
}
// Won't work.
public struct MaterialBuffer {
    public float SpecularPower;
    public Vector2 padding2;
    public bool HasTexture;
    private bool padding0;
    private short padding1;
    public Color4 Ambient;
    ...
}
// Works.
public struct MaterialBuffer {
    public bool HasTexture;
    private bool padding0;
    private short padding1;
    public float SpecularPower;
    public Vector2 padding2;
    public Color4 Ambient;
    ...
}

我投入了一些研究工作来测试类型字节大小的差异,这似乎并没有真正改变什么,但我将 post 我在这里发现的常见基本类型:

1 Byte  : bool, sbyte, byte
2 Bytes : short, ushort
4 Bytes : int, uint, float
8 Bytes : long, ulong, double
16 Bytes: decimal

您必须意识到用于构造更复杂类型的基本类型。例如,假设您有一个 Vector2 和一个 X 属性 和一个 Y 属性。如果这些由 float 类型表示,那么您将需要在下一个 属性 之前填充 8 字节,除非您有其他一些东西来帮助达到 16 字节。但是,如果它们由 double 类型或 decimal 类型表示,则大小不同,您需要注意这一点。


注册作业

我能够解决注册问题;当您设置缓冲区时,这也对应于 C# 一侧。当您设置缓冲区时,您将索引分配给这些缓冲区,并且 HLSL 应该使用相同的索引。

// Buffer declarations in HLSL.
cbuffer ViewBuffer : register(b0)
cbuffer CameraBuffer : register(b1);
cbuffer MaterialBuffer : register(b2);

// Buffer assignments in C#.
context.VertexShader.SetConstantBuffer(0, viewBuffer);
context.VertexShader.SetConstantBuffer(1, cameraBuffer);
context.VertexShader.SetConstantBuffer(2, materialBuffer);

以上代码将按预期工作,因为缓冲区已分配给正确的寄存器。但是,例如,如果我们将相机的缓冲区更改为 8,则必须将 cbuffer 分配给寄存器 b8 才能正常工作。由于这个确切原因,下面的代码不起作用。

cbuffer CameraBuffer : register(b1)
context.VertexShader.SetConstantBuffer(8, cameraBuffer);