DirectX11--深入理解HLSL常量緩沖區打包規則


HLSL常量緩沖區打包規則

DirectX11 With Windows SDK完整目錄

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

盡管打包規則並不復雜,但是稍不留意就可能會導致因為打包規則的不理解而產生的數據錯位問題。

下面會使用大量的例子來進行描述,並對有爭議的部分使用圖形調試器來反匯編着色器代碼加以驗證。

1. C++中的結構體數據是以字節流的形式傳輸給HLSL的

例1.1

若C++結構體和HLSL常量緩沖區如下:

// cpp
struct S1
{
    XMFLOAT3 p1;
    XMFLOAT3 p2;
};

// HLSL
cbuffer C1
{
    float4 v1;
    float4 v2;
}

則最終C1兩個向量接收到的數據如下:
(p1.x, p1.y, p1.z, p2.x)
(p2.y, p2.z, empty, empty)

2. HLSL常量緩沖區中的向量不允許拆分

例2.1

// cpp
struct S1
{
    XMFLOAT3 p1;
    XMFLOAT3 p2;
};

// HLSL
cbuffer C1
{
    float3 v1;
    float4 v2;
}

v1將被單獨打包成一個4D向量,確保常量緩沖區的內存按128位對齊。

C1的內存布局為:

(v1.x, v1.y, v1.z, empty)
(v2.x, v2.y, v2.z, v2.w)

這時用S1結構體的數據再傳輸給C1,結果如下:

(p1.x, p1.y, p1.z, p2.x)
(p2.y, p2.z, empty, empty)

例2.2

// HLSL
cbuffer C1
{
    float2 v1;
    float4 v2;
    float2 v3;
}

v2無法拆分來填充v1的空位,而是單獨起一行向量,這樣C1的內存布局為:

(v1.x, v1.y, empty, empty)
(v2.x, v2.y, v2.z, v2.w)
(v3.x, v3.y, empty, empty)

3. HLSL常量緩沖區中多個相鄰的變量若有空缺則優先打包進同一個4D向量中

例3.1

// HLSL
cbuffer C1
{
    float v1;
    float2 v2;
    float v3;
    float2 v4;
    float v5;
}

C1的內存布局為:

(v1.x, v2.x, v2.y, v3.x)
(v4.x, v4.y, v5.x, empty)

打包順序是從最上面的變量開始往下的。

例3.2

// HLSL
cbuffer C1
{
    float2 v1;
    float v2;
    float3 v3;
}

C1的內存布局為:

(v1.x, v1.y, v2.x, empty)
(v3.x, v3.y, v3.z, empty)

4. 對於在常量緩沖區的結構體,也會進行打包操作

通過幾個例子來進行觀察

例4.1

// HLSL
struct S1
{
    float2 p1;
    float3 p2;
    float p3;
};

cbuffer C1
{
    float v1;
    S1 v2;
    float3 v3;
}

C1的內存布局為:

(v1.x, empty, empty, empty)
(v2.p1.x, v2.p1.y, empty, empty)
(v2.p2.x, v2.p2.y, v2.p2.z, v2.p3.x)
(v3.x, v3.y, v3.z, empty)

例4.2

// HLSL
struct S1
{
    float4 p1;
    float p2;
};

cbuffer C1
{
    S1 v1;
    float2 v2;
}

C1的內存布局為:
(v1.p1.x, v1.p1.y, v1.p1.z, v1.p1.w)
(v1.p2.x, v2.x, v2.y, empty)

所以,結構體常量前面的所有常量都會被打包成4D向量,但結構體常量的最后一個成員可能會和后續的常量打包成4D向量。

5. 對於在常量緩沖區的數組,需要特殊對待

數組中的每一個元素都會獨自打包,但對於最后一個元素來說如果后續的變量不是數組、結構體且還有空缺,則可以進行打包操作

例5.1

// HLSL
cbuffer C1
{
    float v1[4];
}

C1的內存布局為:

(v1[0].x, empty, empty, empty)
(v1[1].x, empty, empty, empty)
(v1[2].x, empty, empty, empty)
(v1[3].x, empty, empty, empty)

可以看到,一個本應該是16字節的數組變量實際上變成了64字節的4個4D向量,造成內存的大量浪費。如果真的要使用這種數組,下面的聲明方式通過強制轉換,可以保證沒有空間浪費(C++不允許這么做):

// HLSL
cbuffer C1
{
    float4 v1;
}
static float packArray[4]  = (float[4])v1;

例5.2

// HLSL
cbuffer C1
{
    float2 v1[4];
    float2 v2;
}

C1的內存布局實際上為:

(v1[0].x, v1[0].y, empty, empty)
(v1[1].x, v1[1].y, empty, empty)
(v1[2].x, v1[2].y, empty, empty)
(v1[3].x, v1[3].y, v2.x, v2.y)

例5.3

// HLSL
struct S1
{
    float p1;
    int p2;
};

cbuffer C1
{
    S1 v1[4];
    float v2;
    float3 v3;
}

C1的內存布局實際上為:

(v1[0].p1, v1[0].p2, empty, empty)
(v1[1].p1, v1[1].p2, empty, empty)
(v1[2].p1, v1[2].p2, empty, empty)
(v1[3].p1, v1[3].p2, v2.x, empty)
(v3.x, v3.y, v3.z, empty)

例5.4

// HLSL
struct S1
{
    float p1;
    int p2;
};

cbuffer C1
{
    float v1[2];
    S1 v2;
}

C1的內存布局為:

(v1[0].x, empty, empty, empty)
(v1[1].x, empty, empty, empty)
(v2.p1, v2.p2, empty, empty)

使用VS的圖形調試器來分析HLSL常量緩沖區的內存布局

首先確保着色器、常量緩沖區都已經綁定到渲染管線,如果該常量緩沖區被綁定到像素着色階段,就應該在圖形調試器中對像素着色器代碼進行調試。

例4.1的驗證

開啟反匯編后,找到之前所放的常量緩沖區:
image

在這些樣例中已經確保了常量緩沖區前面的所有值都已經打包好(16字節對齊)。

這里v1的偏移值為2416,然后可以看到結構體對象v2內的p1偏移值為2432,說明v1單獨被打包成16字節向量。然后p1無法和p2打包,所以p1單獨打包成16字節。

然后p2p3被打包,因為v3的偏移值為2464p2的偏移值為2448

所以內存布局如下:

(v1.x, empty, empty, empty)
(v2.p1.x, v2.p1.y, empty, empty)
(v2.p2.x, v2.p2.y, v2.p2.z, v2.p3.x)
(v3.x, v3.y, v3.z, empty)

例4.2的驗證

image

v1的偏移值為2416p1構成單獨的4D向量,p2會和后續的v2打包成新的4D向量,而不是單獨打包。

所以內存布局如下:
(v1.p1.x, v1.p1.y, v1.p1.z, v1.p1.w)
(v1.p2.x, v2.x, v2.y, empty)

例5.2的驗證

image

如果數組的每個元素都單獨打包的話,理論上這個數組所占字節數為64,但這里數組的最后一個float2和下面的float2打包成一個4D向量。

所以內存布局如下:

(v1[0].x, v1[0].y, empty, empty)
(v1[1].x, v1[1].y, empty, empty)
(v1[2].x, v1[2].y, empty, empty)
(v1[3].x, v1[3].y, v2.x, v2.y)

例5.3的驗證

image

可以看到結構體數組的每個結構體元素都被單獨打包成1個4D向量,但數組最后一個元素跟v2打包到一起。

內存布局如下:

(v1[0].p1, v1[0].p2, empty, empty)
(v1[1].p1, v1[1].p2, empty, empty)
(v1[2].p1, v1[2].p2, empty, empty)
(v1[3].p1, v1[3].p2, v2.x, empty)
(v3.x, v3.y, v3.z, empty)

例5.4的驗證

image

因為數組的后面是結構體,而結構體要求前面的所有變量都要先打包好,所以數組的2個元素分別單獨打包。

內存布局如下:

(v1[0].x, empty, empty, empty)
(v1[1].x, empty, empty, empty)
(v2.p1, v2.p2, empty, empty)

記一次打包問題引發的錯誤

在一次進行圖形調試的時候,發現原本設置平行光和點光燈的數目為1,到了圖形調試器卻變成了點光燈和聚光燈的數目為1

C++代碼如下:


struct CBNeverChange
{
	DirectionalLight dirLight[10];  // 已按16字節對齊
	PointLight pointLight[10];      // 已按16字節對齊
	SpotLight spotLight[10];        // 已按16字節對齊
	int numDirLight;
	int numPointLight;
	int numSpotLight;
	int pad;
};

// ...

mCBNeverChange.numDirLight = 1;
mCBNeverChange.numPointLight = 1;
mCBNeverChange.numSpotLight = 0;

HLSL代碼如下:

cbuffer CBNeverChange : register(b3)
{
	DirectionalLight gDirLight[10];
	PointLight gPointLight[10];
	SpotLight gSpotLight[10];
	int gNumDirLight;
	int gNumPointLight;
	int gNumSpotLight;
}

在圖形調試器查看C++提供的字節數據,可以看到最后四個32位的傳入是沒有問題的
image

經過一番折騰,翻到像素着色器的反編譯,發現里面有常量緩沖區數據偏移信息:

仔細比對的話可以發現從gNumDirLight開始的字節偏移量出現了不是我想要的結果,本應該是2400的值,結果卻是2396,導致原本賦給gNumDirLightgNumPointLight為1的值,卻賦給了gNumPointLightgNumSpotLight。這也是我為什么要寫出這篇文章的原因。

常量緩沖區聲明技巧

首先重新總結之前的打包規則:
1. C++中的結構體數據是以字節流的形式傳輸給HLSL的;
2. HLSL常量緩沖區中的向量不允許拆分;
3. HLSL常量緩沖區中多個相鄰的變量若有空缺則優先打包進同一個4D向量中;
4. HLSL常量緩沖區中,結構體常量前面的所有常量都會被打包成4D向量,內部也進行打包操作,但結構體的最后一個成員可能會和后續的常量打包成4D向量;
5. 數組中的每一個元素都會獨自打包,但對於最后一個元素來說如果后續的變量不是數組、結構體且還有空缺,則可以進行打包操作。

所以避免出現潛在問題的辦法如下:
1. 若要使用數組,數組的類型最好能按16字節對齊
2. 結構體的總大小也需要按16字節對齊。

DirectX11 With Windows SDK完整目錄

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM