動的な定数バッファーを D3D11_MAP_WRITE_NO_OVERWRITE で Map する

TL;DR

  • DirectX 11 で 動的な定数バッファーを Map して書き込むときは D3D11_MAP_WRITE_DISCARD を使おう。
  • ただし Direct3D 11.1 ランタイムから、デバイスが対応していれば、動的な定数バッファーでも D3D11_MAP_WRITE_NO_OVERWRITE が使用できる。
    • それ以前のランタイムでは、頂点バッファーとインデックスバッファーでのみ D3D11_MAP_WRITE_NO_OVERWRITE を使用できる。
    • デバイスが対応しているかどうかは ID3D11Device::CheckFeatureSupport で調べられる。
  • また Feature level が 9.1, 9.2, 9.3 のグラフィックスデバイスでは、ドライバー側でエミュレートするのでどの環境でも定数バッファーに D3D11_MAP_WRITE_NO_OVERWRITE が使える。
「動的なバッファへ書き込むときに D3D11_MAP_WRITE_NO_OVERWRITE で Map できる?」早見表
Vertex Buffer Index Buffer Constant Buffer
DirectX 11 (Feature level: 9.1, 9.2, 9.3)
DirectX 11.0 and earlier runtimes -
DirectX 11.1 runtime (Windows 8 and later) △ (デバイスによる)

ことの発端

古い Windows マシンで作っているゲームエンジンを動かしたところ、Direct3D 11 のエラーが発生しました。 動的な1 定数バッファー (Constant Buffer) へ書き込もうと Map したときにエラーが必ず起こるようです。 ほかのマシンでは問題なく動作していたのに、どうしてエラーが起きたのでしょうか。

今回、動作確認に使った Windows デスクトップは次の 2 つです。DirectX 診断ツール (dxdiag) の実行結果を載せています。

比較的に新しい Windows 環境(問題なく動いたマシン)
  • Operating System: Windows 10 Pro 64-bit
  • Processor: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz (8 CPUs)
  • Device: Intel(R) HD Graphics 4000
  • Feature Levels: 11_0, 10_1, 10_0, 9_3, 9_2, 9_1
古い Windows 環境(問題のあったマシン)
  • Operating System: Windows 10 Pro
  • Processor: Intel(R) Core(TM)2 Duo CPU E7500 @ 2.93GHz (2 CPUs)
  • Device: Intel(R) G41 Express Chipset
  • Feature Levels: 10_0, 9_1

NOTE: 母国語もまた苦痛
僕は、 定数バッファー よりも constant buffer という表記のほうが色々と考えなくて済むので好きなのですが、 MSDN の日本語ドキュメントでは前者が使われているようでしたので今回はそちらに従いました。 似たような話で、 depth stencil buffer を「深度ステンシルバッファー」と書くべきか「デプスステンシルバッファー」と書くべきか(あるいはそのままの英語表記にするか)よく迷います。 これに加えて古い慣例で、最後の長音を伸ばすか伸ばさないかなんて些細な取り決めもありますよね…。 可能なかぎり表記揺れのない文章を心がけたいものですが、英語で読み書きするときとは違うストレスを、こういった技術用語の和訳によって感じます。

問題のソースコードとその原因

次のソースコードに問題がありました:

constexpr D3D11_MAP mapType = D3D11_MAP_WRITE_NO_OVERWRITE;

D3D11_MAPPED_SUBRESOURCE mappedResource;
auto hr = deviceContext->Map(buffer.Get(), 0, mapType, 0, &mappedResource);

if (FAILED(hr)) {
    // FUS RO DAH!
}

ソースコード中の bufferD3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE そして D3D11_BIND_CONSTANT_BUFFER で作成した、いわゆる動的な定数バッファーオブジェクトです。 ID3D11DeviceContext::Map を呼び出した時に、エラーが返ってきます。 返ってきたハンドルの値は E_INVALIDARG で、このエラー内容は "One or more arguments are invalid" でした。

このエラーの原因は、定数バッファーを Map するときに D3D11_MAP_WRITE_NO_OVERWRITE を指定していたことでした。 試しに D3D11_MAP_WRITE_NO_OVERWRITE の代わりに D3D11_MAP_WRITE_DISCARD を指定したところ問題なく動きました。

特定の環境でエラーが出る理由

では新しいマシンで使えて、古いマシンでは使えなかったのは、どうしてでしょうか?

まず、D3D11_MAP_WRITE_NO_OVERWRITE は頂点バッファーとインデックスバッファーにしか利用できず、動的な定数バッファーを Map するときには使用できません。 ただし Windows 8 以降で利用可能な Direct3D 11.1 ランタイムでは、グラフィックスデバイスによって定数バッファーでも D3D11_MAP_WRITE_NO_OVERWRITE が使えるようになったそうです。

グラフィックスデバイスが動的な定数バッファーの D3D11_MAP_WRITE_NO_OVERWRITE に対応しているかどうかは、 ID3D11Device::CheckFeatureSupport で調べられます。ドライバーが対応している場合は D3D11_FEATURE_DATA_D3D11_OPTIONS::MapNoOverwriteOnDynamicConstantBuffer の値が TRUE になります。

D3D11_FEATURE_DATA_D3D11_OPTIONS options;

auto hr = device->CheckFeatureSupport(
    D3D11_FEATURE_D3D11_OPTIONS,
    &options,
    sizeof(D3D11_FEATURE_DATA_D3D11_OPTIONS));

if (FAILED(hr)) {
    // FUS RO DAH!
}

if (options.MapNoOverwriteOnDynamicConstantBuffer == TRUE) {
    // OK
}

これが FALSE の場合は、ドライバーが対応していないので上述したように Map 時に E_INVALIDARG の値が返ります。

また、 Feature level が 9.1, 9.2, 9.3 のグラフィックスデバイスでは、この MapNoOverwriteOnDynamicConstantBuffer が常に TRUE になります。 これは D3D11_MAP_WRITE_NO_OVERWRITE の動作をランタイム側でエミュレートするからです。

この問題の解決策を 3 つ紹介します2

解決策 1: D3D11_MAP_WRITE_DISCARD を使う

1 つ目は、動的な定数バッファーを Map するときには D3D11_MAP_WRITE_NO_OVERWRITE の代わりに D3D11_MAP_WRITE_DISCARD を使うことです。

constexpr D3D11_MAP mapType = D3D11_MAP_WRITE_DISCARD;

D3D11_MAPPED_SUBRESOURCE mappedResource;
auto hr = deviceContext->Map(buffer.Get(), 0, mapType, 0, &mappedResource);

これはとてもうまく動きます。

解決策 2: Feature level を下げたグラフィックスデバイスを作成する

2 つ目は、 Feature level を 9.1, 9.2 または 9.3 に落としてから ID3D11Device オブジェクトを作成する方法です。

const std::vector<D3D_FEATURE_LEVEL> featureLevels = {
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1,
};

auto hr = D3D11CreateDevice(
    adapter,
    driverType,
    nullptr,
    createDeviceFlags,
    featureLevels.data(),
    static_cast<UINT>(featureLevels.size()),
    D3D11_SDK_VERSION,
    &device,
    &featureLevel,
    &deviceContext);

ただし、これはあまりオススメできる方法ではありません。 Feature level が低いグラフィックスデバイスでは、利用できる機能が制限され、多くの制約がついてきます。 例えば、頂点シェーダーをコンパイルするときに、コンパイラーターゲットを vs_4_0_level_9_1 または vs_4_0_level_9_3 とする必要があります 3

事前にコンパイルしておいたシェーダーバイナリをアプリケーションで利用する場合、Feature level に沿ってシェーダーを複数用意するのは大変そうです。 試しに D3D_FEATURE_LEVEL_9_3 のデバイスを作り、 vs_4_0_level_9_1 ではなく通常の vs_4_0 でコンパイルしたシェーダーバイナリを使って ID3D11Device::CreateVertexShader() を実行すると、エラー E_INVALIDARG ("One or more arguments are invalid.") が返ってきました。

解決策 3: 動作環境を引き上げる

3 つ目の解決策は、ゲームの動作環境を新しいハードウェアのみサポートすることです。これはとてもうまくいきます。 例えば、動作環境を「Feature level は 11.0 以降、ドライバーモデルは WDDM 1.1 以降、OS は Windows 10 以降」のように引き上げると、ゲームエンジン側での対応が色々と軽減されます。

Steam が公開しているハードウェアとソフトウェアの調査報告 Steam Hardware & Software Survey を見ると、Steam ユーザーの 70 % が DirectX 12 の Feature level に対応した GPU を使っているそうです (2016 年 11 月現在)。古い環境でもゲームが動作するのは魅力的ですが、もし開発リソースが制限されている場合 30% のゲームプレイヤーをサポートするかどうかは検討したほうがよさそうです。

NOTE:
Steam Hardware & Software Survey に記載されている "DirectX 12 GPUs" や "DirectX 11 GPUs" は、 Feature level(s) ごとに分けているようです。つまり "DirectX 11 GPUs" に分類された GPU であれば Feature level は 11.0 以上になります。もちろん GPU が対応している Feature level が 11.0 であっても、高レベルなドライバーやソフトウェアのほうで対応していれば DirectX 12 のランタイムは動くので DirectX 12 の API も動きます。ドライバー開発者・ベンダーのみなさまに感謝を。

(補足) CheckFeatureSupport の実行結果

今回は次のようなコードで、 GPU が対応している Feature level のグラフィックスデバイスを作成しました。

constexpr std::vector<D3D_FEATURE_LEVEL> featureLevels = {
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1,
};

auto hr = D3D11CreateDevice(
    adapter,
    driverType,
    nullptr,
    createDeviceFlags,
    featureLevels.data(),
    static_cast<UINT>(featureLevels.size()),
    D3D11_SDK_VERSION,
    &device,
    &featureLevel,
    &deviceContext);

上述した新しいマシンと古いマシンではそれぞれ D3D_FEATURE_LEVEL_11_0D3D_FEATURE_LEVEL_10_0 のデバイスが作成されました。 また試しに、新しいマシンのほうで D3D_FEATURE_LEVEL_10_1 を指定してデバイスを作ってみました。 実際にこれらの各デバイスを作成して CheckFeatureSupport した結果を以下に載せます。

新しいマシン (11_0) 新しいマシン (10_1) 古いマシン (10_0)
OutputMergerLogicOp FALSE FALSE FALSE
UAVOnlyRenderingForcedSampleCount TRUE FALSE FALSE
DiscardAPIsSeenByDriver TRUE TRUE FALSE
FlagsForUpdateAndCopySeenByDriver TRUE TRUE FALSE
ClearView TRUE TRUE FALSE
CopyWithOverlap TRUE TRUE FALSE
ConstantBufferPartialUpdate TRUE TRUE FALSE
ConstantBufferOffsetting TRUE TRUE FALSE
MapNoOverwriteOnDynamicConstantBuffer TRUE TRUE FALSE
MapNoOverwriteOnDynamicBufferSRV TRUE TRUE FALSE
MultisampleRTVWithForcedSampleCountOne TRUE TRUE FALSE
SAD4ShaderInstructions TRUE FALSE FALSE
ExtendedDoublesShaderInstructions TRUE FALSE FALSE
ExtendedResourceSharing TRUE TRUE FALSE

参考文献


  1. 実行時にバッファの内容を何度も CPU 側から書き換えるという意味で 動的な (dynamic) という言葉を使っています。対して、バッファの初期化時 (ID3D11Device::CreateBuffer を呼び出したとき) にのみ内容を更新するものを 静的な(immutable/static) とここではしています。 

  2. ここでは省略していますが、もとより、動的な定数バッファーを Map するときに D3D11_MAP_WRITE_DISCARD の代わりに D3D11_MAP_WRITE_NO_OVERWRITE を必ずしも使う必要があるのかどうかは考える余地があります。また、アプリケーション上でシビアなバッファーの扱いが必要な場合は DirectX 12 を使ったほうがいいでしょう。 

  3. ID3D11Device::CreateVertexShader を参照。 

Leave a Reply