画面は、クリア色の青のまま。三角形は、いつまで待っても出てきません。戻り値のHRESULTを確かめてもS_OK。クラッシュもしないし、エラーも出ない。DirectXの描画は、間違えても、黙って何も描かないことがよくあります。
そこで頼りになるのが、Direct3D 11の「デバッグレイヤー」です。ただ、「これを付ければ、原因が全部分かる」という書き方を見かける一方で、実際にどこまで拾えるのかを、数字で示した情報は少なめです。そこで、描画されない原因を8通り作って、デバッグレイヤーの有無で、見え方がどう変わるかを実測しました。結論を先に言うと、ほとんどは教えてくれますが、何も言ってくれないケースもありました。
検証は、Visual Studio 2022付属のMSVC(19.44、x64、デバッグビルド)で、画面を出さない、小さな実験用プログラムを作って行いました。
デバッグレイヤーとは?
デバッグレイヤーとは、
・Direct3Dの呼び出しを、裏でチェックして、規則に反した使い方を、メッセージで知らせてくれる仕組み
です。デバイスを作るときに、D3D11_CREATE_DEVICE_DEBUGフラグを付けるだけで、有効になります。
▼デバイスの作成(実験用プログラムから抜粋)
UINT flags = useDebug ? D3D11_CREATE_DEVICE_DEBUG : 0;
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, &dev, &fl, &ctx);
if (FAILED(hr)) { std::printf("デバイス作成に失敗: 0x%08X\n", (unsigned)hr); return 1; }メッセージは、Visual Studioのデバッガーで動かしているときは、「出力」ウィンドウにD3D11 ERROR: ...という形で出ます。今回は、結果を文字として残したいので、プログラムの中からID3D11InfoQueueを使って、溜まったメッセージを取り出して表示しました。
▼溜まったメッセージを取り出す関数
static void DumpMessages(ID3D11Device* dev)
{
ID3D11InfoQueue* q = nullptr;
if (FAILED(dev->QueryInterface(__uuidof(ID3D11InfoQueue), (void**)&q)))
{
std::printf("(デバッグレイヤー無し: メッセージを取り出す窓口がありません)\n");
return;
}
UINT64 n = q->GetNumStoredMessages();
std::printf("デバッグレイヤーのメッセージ: %llu 件\n", (unsigned long long)n);
for (UINT64 i = 0; i < n; ++i)
{
SIZE_T len = 0;
q->GetMessage(i, nullptr, &len);
std::vector<char> buf(len);
D3D11_MESSAGE* m = (D3D11_MESSAGE*)buf.data();
q->GetMessage(i, m, &len);
const char* sev = m->Severity == D3D11_MESSAGE_SEVERITY_ERROR ? "ERROR"
: m->Severity == D3D11_MESSAGE_SEVERITY_WARNING ? "WARNING"
: m->Severity == D3D11_MESSAGE_SEVERITY_INFO ? "INFO" : "CORRUPTION";
std::printf(" [%s] %s\n", sev, m->pDescription);
}
q->Release();
}なお、デバッグレイヤーを使うには、Windowsの「グラフィックスツール」(オプション機能)が必要で、入っていない環境では、デバイスの作成が失敗します。今回の環境には、入っていたので、この失敗は再現していません。
実験:描画されない原因を、8通り作る
64×64の描画先(画面には出さない)に、赤い三角形を1つ描いて、その中央のピクセルの色を読み戻す、という小さなプログラムです。背景は暗い青なので、中央が赤なら成功、青のままなら「何も描かれていない」と、数字で判定できます。シェーダーは、色を定数バッファで受け取るだけの、最小のものです。
▼シェーダー(プログラム内の文字列)
static const char* kShader = R"(
cbuffer Params : register(b0) { float4 tint; };
struct VSIn { float3 pos : POSITION; };
struct VSOut { float4 pos : SV_POSITION; };
VSOut VS(VSIn i) { VSOut o; o.pos = float4(i.pos, 1); return o; }
float4 PS(VSOut i) : SV_TARGET { return tint; }
)";描画の部分では、ケース番号に応じて、1つだけ、わざと設定を飛ばします。
▼描画の部分(ケースごとに、1つだけ飛ばす)
// 描画
float clear[4] = { 0.0f, 0.0f, 0.5f, 1.0f }; // 背景は暗い青
ctx->ClearRenderTargetView(rtv, clear);
if (cs != 3) ctx->OMSetRenderTargets(1, &rtv, nullptr);
D3D11_VIEWPORT vp{ 0, 0, 64, 64, 0, 1 };
if (cs != 4) ctx->RSSetViewports(1, &vp);
if (cs != 1) ctx->IASetInputLayout(layout);
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
UINT stride = 12, offset = 0;
if (cs != 2) ctx->IASetVertexBuffers(0, 1, &vb, &stride, &offset);
ctx->VSSetShader(vs, nullptr, 0);
ctx->PSSetShader(ps, nullptr, 0);
if (cs != 6) ctx->PSSetConstantBuffers(0, 1, &cb);
ctx->Draw(3, 0);同じケースを、デバッグレイヤー無し・有りの両方で、実行しました。
結果の一覧
| ケース | 何を飛ばしたか | 画面(中央) | デバッグレイヤー有りのメッセージ |
|---|---|---|---|
| 0 | (正常) | 赤い三角形 | 0件 |
| 1 | 入力レイアウトの設定 | 青のまま | ERROR(入力アセンブラが無い) |
| 2 | 頂点バッファの設定 | 青のまま | 0件(何も言わない) |
| 3 | レンダーターゲットの設定 | 青のまま | WARNING(RTVが無い) |
| 4 | ビューポートの設定 | 青のまま | WARNING(ビューポートが無い) |
| 5 | 定数バッファのサイズ(20バイト) | 黒い三角形 | ERROR×2(16の倍数ではない) |
| 6 | 定数バッファの設定 | 黒い三角形 | 0件(何も言わない) |
| 7 | オブジェクトの解放(2個) | 赤い三角形 | 終了前の報告で、2個が見つかる |
メッセージが出たケース
ケース1:入力レイアウトを設定し忘れた
ケース1: 入力レイアウトを設定し忘れた / デバッグレイヤー: 無効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=127 (背景の青のまま(何も描かれていない))
(デバッグレイヤー無し: メッセージを取り出す窓口がありません)ケース1: 入力レイアウトを設定し忘れた / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=127 (背景の青のまま(何も描かれていない))
デバッグレイヤーのメッセージ: 1 件
[ERROR] ID3D11DeviceContext::Draw: The Vertex Shader expects application provided input data (which is to say data other than hardware auto-generated values such as VertexID or InstanceID). Therefore an Input Assembler object is expected, but none is bound.レイヤー無しでは、「青のまま」とだけ分かって、手がかりはありません。有りでは、「頂点シェーダーは、アプリが渡す入力データを期待しているので、入力アセンブラ(の設定)が必要なのに、何も設定されていません」という内容のERRORが出ました。入力レイアウトをIASetInputLayoutで設定していない、と読み取れます。
ケース5:定数バッファのサイズが、16の倍数ではない
ケース5: 定数バッファのサイズが16の倍数でない(20バイト) / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x80070057 (失敗)
描画後の中央のピクセル: R=0 G=0 B=0 (黒く塗られている(三角形は描かれたが、色が0))
デバッグレイヤーのメッセージ: 2 件
[ERROR] ID3D11Device::CreateBuffer: The Dimensions are invalid. For ConstantBuffers, marked with the D3D11_BIND_CONSTANT_BUFFER BindFlag, the ByteWidth (value = 20) must be a multiple of 16.
[ERROR] ID3D11Device::CreateBuffer: CreateBuffer returning E_INVALIDARG, meaning invalid parameters were passed.CreateBufferの戻り値は、レイヤーが無くても0x80070057(E_INVALIDARG)でした。ただ、どの引数が、なぜ不正なのかは、戻り値だけでは分かりません。レイヤー有りでは、「定数バッファのByteWidth(今回は20)は、16の倍数でなければならない」と、原因そのものが書かれていました。しかも、戻り値を確認していなかった場合、バッファは作られないまま描画が進んで、黒い三角形になります。この16バイトの決まりについては、定数バッファの16バイト境界の記事で詳しく書いています。
ケース3・4:WARNINGが出る
ケース3: レンダーターゲットを設定し忘れた / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=127 (背景の青のまま(何も描かれていない))
デバッグレイヤーのメッセージ: 1 件
[WARNING] ID3D11DeviceContext::Draw: The Pixel Shader expects a Render Target View bound to slot 0, but none is bound. This is OK, as writes of an unbound Render Target View are discarded. It is also possible the developer knows the data will not be used anyway. This is only a problem if the developer actually intended to bind a Render Target View here.ケース4: ビューポートを設定し忘れた / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=127 (背景の青のまま(何も描かれていない))
デバッグレイヤーのメッセージ: 1 件
[WARNING] ID3D11DeviceContext::Draw: There are no viewports currently bound. If any rasterization to RenderTarget(s) and/or DepthStencil is performed, results will be undefined.どちらも、ERRORではなくWARNINGでした。ケース3のメッセージには、「未設定の描画先への書き込みは捨てられるので、問題ない場合もあるが、描画先を設定するつもりだったなら、問題」という趣旨の説明が付いています。WARNINGは、「エラーではないが、意図どおりか確認して」という知らせです。ERRORだけを見て、WARNINGを読み飛ばすと、この2つを見逃します。
【重要】何も出ないケースがある
ケース2:頂点バッファを設定し忘れた
ケース2: 頂点バッファを設定し忘れた / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=127 (背景の青のまま(何も描かれていない))
デバッグレイヤーのメッセージ: 0 件画面は「青のまま」で、何も描かれていないのに、メッセージは0件でした。頂点バッファを設定していなくても、入力レイアウトさえ設定してあれば、レイヤーは何も言いませんでした。
ケース6:定数バッファをシェーダーに設定し忘れた
ケース6: 定数バッファをシェーダーに設定し忘れた / デバッグレイヤー: 有効
定数バッファのCreateBuffer: hr=0x00000000 (成功)
描画後の中央のピクセル: R=0 G=0 B=0 (黒く塗られている(三角形は描かれたが、色が0))
デバッグレイヤーのメッセージ: 0 件こちらも、0件です。三角形は描かれたものの、色を決める定数バッファが無いので、色は(0, 0, 0)、つまり黒になりました。シェーダーから見ると、未設定の定数バッファは、「全部0」として読まれるだけで、規則違反ではないのです。
この2つは、デバッグレイヤーの「メッセージが無い」ことが、「正しい」ことを意味しない、という例です。三角形が出ないときの確認の順番は、三角形が出ない原因の記事で、実測つきで整理しています。
解放し忘れを、見つける
デバッグレイヤーには、もう1つ、便利な機能があります。ID3D11Debug::ReportLiveDeviceObjectsを呼ぶと、まだ生きているオブジェクトを、一覧で報告してくれます。ケース7では、頂点バッファと入力レイアウトを、わざと解放しませんでした。
--- 生きているオブジェクトを報告させる ---
[WARNING] Live ID3D11Device at 0x000001F84B03B610, Refcount: 6
(中略: デバイスの内部で使われるオブジェクトが、7行続く)
[WARNING] Live ID3D11InputLayout at 0x000001F84F477FC0, Refcount: 1, IntRef: 0
[WARNING] Live ID3D11Buffer at 0x000001F84F547900, Refcount: 1, IntRef: 0
(中略: 種類ごとの集計のうち、関係する行だけ)
[WARNING] Live ID3D11InputLayout : 1
[WARNING] Live ID3D11Buffer : 1ID3D11InputLayoutとID3D11Bufferが、Refcount: 1, IntRef: 0の行で、1個ずつ報告されました。つまり、こちらが解放し忘れたのは、この2個です。Refcount: 0の行は、デバイスの内部で使われるオブジェクトで、ClearStateとFlushのあとで、自然に破棄されるものです(メッセージにも、そう説明されています)。作ったバッファの数が合わないときは、これを終了処理で呼ぶだけで、解放し忘れに気づけます。
エラーが出た瞬間に、止める
メッセージを後から読むより、ERRORが出た瞬間に、デバッガーで止めるほうが、原因の行を特定しやすくなります。SetBreakOnSeverityを使います。
▼ERRORが出た瞬間にブレークする設定
if (useDebug && argc > 3 && std::strcmp(argv[3], "break") == 0)
{
ID3D11InfoQueue* iq = nullptr;
dev->QueryInterface(__uuidof(ID3D11InfoQueue), (void**)&iq);
iq->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_ERROR, TRUE); // ERRORが出た瞬間に、ブレークする
iq->Release();
std::printf("SetBreakOnSeverity(ERROR) を有効にした\n");
std::fflush(stdout);
}ケース1をこの設定で動かしたところ、「有効にした」という表示のあと、プログラムが止まって、続きの出力は出ませんでした。
終了コード: 0x87a
--- 標準出力 ---
ケース1: 入力レイアウトを設定し忘れた / デバッグレイヤー: 有効
SetBreakOnSeverity(ERROR) を有効にしたデバッガー無しで起動したので、終了コードだけが残りました。Visual Studioのデバッグ実行(F5)なら、Drawを呼んだその行で止まって、呼び出し履歴から、原因のコードをたどれます(今回の実験では、デバッガー付きの実行は確認していません)。
デバッグレイヤーの、よくある失敗例と対処法
①「メッセージが無い」ので、正しいと判断する
実験のとおり、0件でも、描画が間違っているケースがありました。メッセージが無いときは、「レイヤーが拾えないタイプの間違い」を疑って、設定を1つずつ確認します。
②WARNINGを読み飛ばす
描画先やビューポートの未設定は、WARNINGでした。ERRORだけを見ていると、見逃します。
③リリースビルドでも、有効にしたままにする
デバッグレイヤーは、チェックのぶん、動作が重くなります。#ifdef _DEBUGで、デバッグビルドのときだけ、D3D11_CREATE_DEVICE_DEBUGを付けるのが、一般的です。
④戻り値を確認しない
ケース5のように、CreateBufferなどの戻り値がFAILEDでも、確認せずに進むと、あとで「黒い三角形」のような、遠回りな症状になります。レイヤーのメッセージは、戻り値を確認したうえで、理由を知るために使います。
注意点
- デバッグレイヤーは、
D3D11_CREATE_DEVICE_DEBUGをデバイス作成時に付けるだけで有効になる(「グラフィックスツール」が必要) - メッセージは、デバッガー付きなら「出力」ウィンドウに出る。
ID3D11InfoQueueで、プログラムから取り出すこともできる - 今回の8ケースでは、ERROR 2件、WARNING 2件が拾えて、2件(頂点バッファ・定数バッファの未設定)は、何も出なかった
WARNINGも読む。レンダーターゲットとビューポートの未設定は、WARNINGだったReportLiveDeviceObjectsで、解放し忘れが分かる。Refcount: 1, IntRef: 0の行が、自分のコードの解放漏れ- 今回の検証は、MSVC 19.44、Direct3D 11、ハードウェアドライバー、画面を出さない実験用プログラム。メッセージの文面は、ドライバーやWindowsの版で変わりうる
結論
- 8ケース中、ERRORが2種類(入力レイアウト・定数バッファのサイズ)、WARNINGが2種類(描画先・ビューポート)で、原因が文章で分かった
- 頂点バッファと定数バッファの設定漏れは、0件。メッセージが無くても、安心できない
- 戻り値が
E_INVALIDARGのときは、理由はレイヤーのメッセージに書かれている - 解放し忘れは、
ReportLiveDeviceObjectsで、2個とも見つかった
デバッグレイヤーは、「付けておけば、勝手に原因を言い当ててくれる」ものではなく、「規則違反を、文章にしてくれる」ものでした。メッセージが出たときは、迷わず従う。出ないときは、設定を1つずつ確かめる。この2つを使い分けるだけで、「何も描かれない」ときの調べ方が、ずっと楽になります。

