C++で渡した色が、シェーダーの中では、別の色になっています。配列の1つ目だけが合っていて、2つ目以降は全部0。CreateBufferは、理由も言わずに失敗します。
原因は、HLSL側が決める16バイトの境界のルールと、C++の構造体の並び方が、食い違っていることです。実際に、C++のオフセットと、HLSLが決めた位置を並べて確かめ、ずれたときの画面の色や形を、画素の値で測りました。
16バイト境界のルールとは?
定数バッファは、GPUの中で、16バイト(float41つ分)を1つのまとまりとして扱います。HLSLでは、変数をこのまとまりに、次のルールで詰めます。
・1つの変数が、16バイトの境目をまたいではいけない。またぎそうなときは、次のまとまりの先頭から始まる
一方、C++の構造体は、そんな制約を知らずに、変数をぴったり詰めます。そのため、同じ順番で変数を書いても、C++とHLSLで、変数の位置が違うことがあります。
実験A:色と明るさを渡す
シェーダーの定数バッファは、次のようにしました。画面の左半分にtint(色)、右半分にintensity(明るさ、灰色)を出します。
// 定数バッファの16バイト境界の実験(シーンA)。
// 画面の左半分に tint(色)、右半分に intensity(明るさ)を出す。
// float3 は 16バイトの境目をまたげない。そのため float time; float2 resolution; のあとの float3 tint は 16バイト目から始まる。
cbuffer Params : register(b7) {
float time;
float2 resolution;
float3 tint;
float intensity;
};C++側は、同じ順番で、素直に構造体を書きました。
▼TroubleDemo.cpp(C++側の構造体)
// C++側で「素直に」並べた構造体(これがずれる)
struct CbA_natural {
float time;
XMFLOAT2 resolution;
XMFLOAT3 tint;
float intensity;
};C++の構造体のオフセット(offsetof)と、HLSLが実際に決めた位置を、並べて出力しました。HLSL側は、シェーダーのリフレクション(D3DReflect)で取り出せます。
[cbuf-cpp] natural : time=0 resolution=4 tint=12 intensity=24 sizeof=28
[cbuf-cpp] pad : time=0 resolution=4 tint=16 intensity=28 sizeof=32
[cbuf-cpp] alignas : time=0 resolution=4 tint=16 intensity=28 sizeof=32
[cbuf-hlsl] cbuffer Params size=32
[cbuf-hlsl] time offset= 0 size= 4
[cbuf-hlsl] resolution offset= 4 size= 8
[cbuf-hlsl] tint offset=16 size=12
[cbuf-hlsl] intensity offset=28 size= 4
[cbuf-create] ByteWidth=28 -> hr=0x80070057 FAILED
[cbuf-create] ByteWidth=32 -> hr=0x00000000 OK| 変数 | C++のオフセット(素直に並べた) | HLSLの位置 | 一致 |
|---|---|---|---|
time | 0 | 0 | ○ |
resolution(float2) | 4 | 4 | ○ |
tint(float3) | 12 | 16 | × |
intensity(float) | 24 | 28 | × |
tintは、12バイト目から始めると16バイトの境目をまたぐので、HLSLは16バイト目から始めるtintは、C++では12バイト目から始まりますが、HLSLでは16バイト目から始まります。12バイト目から始めると、12〜24バイトとなり、16バイトの境目をまたいでしまうからです。これに引きずられて、intensityも、24バイト目と28バイト目でずれています。
実際に出た画面

渡した値は、色が(0.20, 0.60, 0.90)、明るさが0.80です。画素値にすると、色が(51, 153, 229)、明るさが(204, 204, 204)のはずです。
| 左半分(色) | 右半分(明るさ) | |
|---|---|---|
| 期待した値 | (51, 153, 229) | (204, 204, 204) |
| 素直に並べたとき | (153, 229, 204) | (0, 0, 0) |
| パディングを入れたとき | (51, 153, 229) | (204, 204, 204) |
alignas(16)を使ったとき | (51, 153, 229) | (204, 204, 204) |
素直に並べたときの左半分は、(153, 229, 204)でした。これは偶然ではなく、計算で説明できます。HLSLはtintを16バイト目から3つ読みます。ところが、C++がそこに置いていたのは、tintの2番目と3番目、そしてintensityでした。つまり、(0.60, 0.90, 0.80)で、255倍すると、まさに(153, 229, 204)になります。右半分は、HLSLが28バイト目を読みますが、C++はそこに何も入れていない(0)ので、真っ黒になりました。
直し方は2つ
1つ目は、C++側で、自分でパディング(穴埋め)を入れる方法です。
▼TroubleDemo.cpp(直し方1)
// 直し方1: 手でパディングを入れる
struct CbA_pad {
float time;
XMFLOAT2 resolution;
float pad0;
XMFLOAT3 tint;
float intensity;2つ目は、alignas(16)で、変数を16バイトの境界に置く方法です。
▼TroubleDemo.cpp(直し方2)
// 直し方2: alignas(16) で16バイト境界に置く
struct CbA_align {
float time;
XMFLOAT2 resolution;
alignas(16) XMFLOAT3 tint;
float intensity;
};どちらも、オフセットは、HLSLと同じ(tintは16、intensityは28)になり、画素値は期待どおりでした(上の表)。
もう1つの罠:バッファの大きさは、16の倍数
素直に並べた構造体のサイズは、28バイトです。そのまま定数バッファを作ろうとすると、どうなるかを試しました(実行ログの、下の2行です)。
[cbuf-create] ByteWidth=28 -> hr=0x80070057 FAILED
[cbuf-create] ByteWidth=32 -> hr=0x00000000 OKByteWidthが28のときは、0x80070057(E_INVALIDARG)で失敗し、16の倍数の32にすると成功しました。定数バッファの大きさは、16の倍数でなければなりません。
実験B:配列で渡す
次は、float weights[4]という配列です。4本の棒グラフの高さを、0.2、0.4、0.6、0.8で渡します(ぼかしフィルターの重みなどで、よく使う形です)。
#ifdef PACKED
cbuffer Params : register(b7) {
float4 weights;
};
#else
cbuffer Params : register(b7) {
float weights[4];
};
#endifC++側は、素直にfloat weights[4]の構造体にしました。
▼TroubleDemo.cpp(シーンBの構造体)
// シーンB
struct CbB_array { float weights[4]; }; // float[4] のまま(ずれる)
struct CbB_f4arr { XMFLOAT4 weights[4]; }; // 要素ごとに16バイトを使う
struct CbB_packed { XMFLOAT4 weights; }; // float4 に4つまとめる[cbuf-hlsl] cbuffer Params size=64
[cbuf-hlsl] weights offset= 0 size=52HLSLでは、配列は要素ごとに16バイトを使います。float weights[4]の大きさが、4バイト×4=16バイトではなく、52バイト(16バイトを3つと、最後の要素の4バイト)になっているのは、そのためです。C++側が、4バイトずつ詰めて渡すと、2番目以降の要素は、まったく別の場所を読むことになります。
| 1本目 | 2本目 | 3本目 | 4本目 | |
|---|---|---|---|---|
| 期待した高さ | 0.2 | 0.4 | 0.6 | 0.8 |
float weights[4]のまま | 0.2 | 0.0 | 0.0 | 0.0 |
float4の配列にした | 0.2 | 0.4 | 0.6 | 0.8 |
float41つにまとめた | 0.2 | 0.4 | 0.6 | 0.8 |
ずれたときは、1本目だけが正しく(0.2)、2〜4本目は0でした(上の画像の下段・左)。直し方は2通り試しました。
- 要素ごとに
float4にする(XMFLOAT4 weights[4]、使うのは.xだけ):4本とも、期待した高さになった float41つに4つ詰める(HLSL側もfloat4 weightsにして、weights[i]で読む):4本とも、期待した高さになった。必要なメモリが、4分の1で済む
【重要】罠:エラーが出ず、「それらしい別の値」になる
今回の実験で、いちばん困ったのは、何のエラーも出ず、一見それらしい値で動くことでした。
- 実験Aでは、色が、緑がかった水色になっただけ(
(153, 229, 204))で、画面は普通に出る。エラーも警告もない - 実験Bでは、1本目だけが正しい。そのため、「値は渡せている」と判断して、見落としやすい
「2番目以降だけおかしい」「3つ目の変数だけおかしい」という症状は、16バイト境界を疑うサインです。そのときは、C++のoffsetofと、HLSLのリフレクション(D3DReflect)の位置を、並べて出力すると、どこから食い違うかが、すぐに分かります。
▼TroubleDemo.cpp(HLSLの配置を取り出す)
void TroubleDemo::ReflectLog(const char* path, const char* entry, const char* cbName) {
wchar_t wpath[512];
MultiByteToWideChar(CP_ACP, 0, path, -1, wpath, 512);
D3D_SHADER_MACRO defs[] = { { "PACKED", "1" }, { nullptr, nullptr } };
ID3DBlob* blob = nullptr;
ID3DBlob* err = nullptr;
HRESULT hr = D3DCompileFromFile(wpath, (scene == 1 && cas == 2) ? defs : nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
entry, "ps_5_0", 0, 0, &blob, &err);
if (FAILED(hr) || !blob) { if (err) err->Release(); return; }
ID3D11ShaderReflection* refl = nullptr;
if (SUCCEEDED(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), __uuidof(ID3D11ShaderReflection), (void**)&refl))) {
ID3D11ShaderReflectionConstantBuffer* cb = refl->GetConstantBufferByName(cbName);
D3D11_SHADER_BUFFER_DESC bdsc = {};
if (cb && SUCCEEDED(cb->GetDesc(&bdsc))) {
DemoLog("[cbuf-hlsl] cbuffer %s size=%u", cbName, bdsc.Size);
for (UINT i = 0; i < bdsc.Variables; i++) {
D3D11_SHADER_VARIABLE_DESC vd = {};
cb->GetVariableByIndex(i)->GetDesc(&vd);
DemoLog("[cbuf-hlsl] %-10s offset=%2u size=%2u", vd.Name, vd.StartOffset, vd.Size);
}
}
refl->Release();
}
blob->Release();
}定数バッファのよくある失敗例と対処法
①float3のあとに、float以外を置く
float3の直後にfloatを置くのは、16バイトに収まるので、ずれません(今回のtintとintensityの関係)。ただし、float3のあとにfloat2やfloat3を置くと、またぐので、次のまとまりに送られます。
②floatの配列を、そのまま渡す
配列の要素は、すべて16バイトを使います。floatの配列は、float4の配列にするか、float4にまとめます。
③構造体のサイズが16の倍数になっていない
CreateBufferがE_INVALIDARGで失敗します。alignas(16)や、パディングで、構造体全体を、16の倍数に整えます。
注意点
- HLSLは、変数が16バイトの境目をまたがないように詰める。C++は、そうしない
- 配列は、要素ごとに16バイトを使う(
float weights[4]は52バイト) - 定数バッファの大きさは、16の倍数(28バイトだと
E_INVALIDARGで失敗した) - ずれてもエラーは出ない。「2番目以降だけおかしい」は、このずれを疑う
offsetofとD3DReflectで、C++とHLSLの位置を並べて確認できる- 今回の検証は、DirectX11、HLSL(SM 5.0)、GeForce RTX 3050 Laptop。行列(
matrix)などの、他の型の配置は、今回は調べていない
結論
- HLSLの定数バッファは、16バイト単位で、境目をまたがないように詰める。C++の構造体は、そうならない
float3の位置が、C++では12、HLSLでは16とずれ、色が(153, 229, 204)、明るさが0(真っ黒)になった- 配列は要素ごとに16バイト。ずれたときは、1本目だけが正しく、残りは0だった
- 直し方は、パディング・
alignas(16)・float4にまとめる。確認はoffsetofとD3DReflect
定数バッファのずれは、エラーが出ないぶん、気づきにくい不具合です。ただ、位置を並べて出力するだけで、原因がはっきり見えます。「渡したはずの値がおかしい」ときは、まずオフセットを並べてみてください。

