DirectX11--深刻理解HLSL常量緩衝區打包規則

HLSL常量緩衝區打包規則

DirectX11 With Windows SDK完整目錄html

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。數組

儘管打包規則並不複雜,可是稍不留意就可能會致使由於打包規則的不理解而產生的數據錯位問題。佈局

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

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

例1.1

若C++結構體和HLSL常量緩衝區以下:code

// 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)htm

2. HLSL常量緩衝區中的向量不容許拆分

例2.1

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

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

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

C1的內存佈局爲:blog

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

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

(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,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索