为什么顺序在着色器中很重要?
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;
}
如果我交换 b0
和 b1
寄存器,渲染将不再有效 (e1)。如果我不理会这些寄存器,并再次交换 World
和 WorldViewProjection
之间的顺序,渲染将不再有效 (e2)。但是,只需将 ViewBuffer
移动到 HLSL
文件中 ObjectBuffer
的上方而不进行其他修改,它就可以正常工作。
现在,我认为寄存器的放置相当重要,第一个寄存器 b0
需要该缓冲区中给出的三个属性,而且我知道 HLSL
常量缓冲区需要位于16 字节块。但是,这给我留下了一些问题。
问题
鉴于 HLSL
期望常量缓冲区位于 16 字节块中;
- 为什么 e2 中的顺序如此重要?
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 个字节。那么,如果大小保持不变,为什么顺序很重要?
- 为什么在这种情况下必须将
ObjectBuffer
分配给 b0
而不是任何其他 b
寄存器?
快速说明
我目前正在对问题做进一步的分析,以便给出更详细准确的答案。当我发现更多细节时,我会更新问题和答案以尽可能准确地反映。
基本答案
上述问题的确切问题(在 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);
快速说明
这个问题有 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;
}
如果我交换 b0
和 b1
寄存器,渲染将不再有效 (e1)。如果我不理会这些寄存器,并再次交换 World
和 WorldViewProjection
之间的顺序,渲染将不再有效 (e2)。但是,只需将 ViewBuffer
移动到 HLSL
文件中 ObjectBuffer
的上方而不进行其他修改,它就可以正常工作。
现在,我认为寄存器的放置相当重要,第一个寄存器 b0
需要该缓冲区中给出的三个属性,而且我知道 HLSL
常量缓冲区需要位于16 字节块。但是,这给我留下了一些问题。
问题
鉴于 HLSL
期望常量缓冲区位于 16 字节块中;
- 为什么 e2 中的顺序如此重要?
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 个字节。那么,如果大小保持不变,为什么顺序很重要?
- 为什么在这种情况下必须将
ObjectBuffer
分配给b0
而不是任何其他b
寄存器?
快速说明
我目前正在对问题做进一步的分析,以便给出更详细准确的答案。当我发现更多细节时,我会更新问题和答案以尽可能准确地反映。
基本答案
上述问题的确切问题(在 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);