Device Removalの処方箋 - 補足資料

これは補完資料です

この記事は、CEDEC2020での講演 “Direct3D 12 Device Removalの処方箋” において、時間内に説明することができなかった部分に関して解説するためのものです。 CEDEC2020で当該講演を聴講された方に向けて書いています。この記事単体では不完全です。タイムシフト視聴や、CEDiLにアクセス可能な方は、先にそちらをご覧になることをお勧めします。

DEVICE_REMOVEDとは

  • DXGIとD3D12API返すHRESULTに設定されるエラー

    • 正式にはDXGIのエラーコード。DXGI_ERROR_DEVICE_REMOVED
    • 殆どの場合は、IDXGISwapChain::Present()呼び出しの際に返される
    • ID3D12DeviceD3D12の一部のメソッド、リソースの作成、Mapなどを実行した際にも返される
    • ID3D12Device::GetDeviceRemovedReasonの呼び出しでも返される
  • ID3D12Device::GetDeviceRemovedReasonを呼び出すことで以下の様な具体的なエラー原因が取得できる。

    • DXGI_ERROR_DEVICE_HUNG
    • DXGI_ERROR_DEVICE_REMOVED
    • DXGI_ERROR_DEVICE_RESET
    • DXGI_ERROR_DRIVER_INTERNAL_ERROR
    • DXGI_ERROR_INVALID_CALL
  • FormatMessage()や、_com_errorでエラーの意味を取得できる
    Device Removed Reason for 887a0006 DXGI_ERROR_DEVICE_HUNG The GPU will not respond to more commands, most likely because of an invalid command passed by the calling application.

DEVICE_REMOVEDが発生する原因について

DEVICE_REMOVEDは、D3D12APIを通じて、GPUやドライバーで発生したエラーの結果に過ぎない。OSやD3D12ランタイムが、コンテキストの実行を継続するべきでは無いと判断した場合に発生する。 ただ、 Alex DunnがGDC2018で説明した通り、大きく分けて2つの種類にカテゴライズする事ができる。

  • TDR(Timeout Detection and Recovery)によるDEVICE_REMOVED
    ドライバーやGPUがOSに対して一定時間内に応答しなかった場合に、OSが発生させるDEVICE_REMOVED。OSはシステム全体のHungを避けるため、DEVICE_REMOVEDを発生させてドライバーをリセットする。

    • ドライバーのコードパスで想定していない長時間の処理があった場合
    • シェーダー内で長時間処理がかかった場合(シェーダー内無限ループ等)
    • Signal,Waitの設定ミスで長時間Fenceが解決しなかった場合
  • エラーの検出によるDEVICE_REMOVED
    何らかの看過できないエラーの発生に伴いOSやD3D12ランタイムが発生させるDEVICE_REMOVED。

    • GPUで発生したPage Fault 存在しないリソースへのアクセスや、宣言した利用用途と異なるアクセス。
    • 不正な上書き等によるCommand Listの破損 結果的にドライバーやGPUが不正な実行コマンドを受け取る。
    • D3D12ランタイムやドライバーによるエラーの検出 許可されていないリソースステートのリソースへのアクセス。各種リソースのアラインメント違反。

GPUとCPUの時間のずれ

ここでは、CPUコードのデバッグと、DEVICE_REMOVEDの追跡の決定的な違いについて説明する。 CPUの実行コードは、デバッガがアタッチされている状況下では即時的であり、エラーが発生すれば直ちにプログラムの実行を停止して、デバッガに処理を返すことで、エラーが起きた瞬間の状況が分かる。
これに対して、DEVICE_REMOVEDの発生は、CPUのコードと全く同期しないタイミングで発生する。そのため、CPUがDEVICE_REMOVEDを受け取った瞬間にデバッガで処理を止める事にはほとんど意味がない。

以下のスクリーンショットはGPUViewというツールでCPUとGPUの処理時間を示したものになる。画面左から右に時間の経過を表している。中央の大きなスタックの中でハイライトされているのは、 あるGPU処理の塊となる、バケットである。ご覧の通り画面の左端で生成されたバケットは、画面の右側でスタックの最下段に到達している。この時点GPUの処理の対象となる。この間3フレーム分の時間が経過している。 もし、このGPU処理のなかでDEVICE_REMOVEDが発生したら、CPUがそのエラーを受け取る可能性があるのは、この時点以後となるので、CPUから見るとコマンド生成から3フレーム以上遅れてDEVICE_REMOVEDを受け取る事になる。

GPUとCPUの処理時間のずれ

これが、DEVICE_REMOVEDの追跡が難しい原因の一つである。

DEVICE_REMOVEDの対処法

GPU上で発生する様々なエラーをデバッグする方法として、D3D12APIは以下の方法を提供している

  • Debug Layer
    昔からあるが、DEVICE_REMOVEDの原因の追跡において最も有効な方法の一つ
  • GPU Based Validation
    比較的新しく導入されたDebug Layerの拡張。CPU側のValidationでは追跡できない問題を検出する
  • DRED1.2
    新しく導入されたDEVICE_REMOVEDの追跡方法

上記3つのうち、先の二つは、DEVICE_REMOVEDが発生する前に起きているD3D12上のエラーの追跡に使うのに対して、 DREDは、DEVICE_REMOVEが発生した後に、発生した箇所を見つけ出すためのもので、用途が完全に異なる。どちらも有用なので組み合わせて使う。

Debug Layer

DEVICE_REMOVEDに対する処方の第一候補は、Debug Layerである。これを有効にすることにより、D3DのランタイムがValidationを積極的に行い、Debug Outputにメッセージを送出するようになる。 DEVICE_REMOVEDが発生する前に出力されるDebug Layerのメッセージは、DEVICE_REMOVEDの発生原因を調査する上での貴重な手がかりになる。

Debug Layerの有効化

Debug Layerはアプリケーション自身で有効にすることもできるし、外部から強制的に有効にすることもできる。
外部から強制的に有効にする際は、dxcpl.exe(GUIツール)やd3dconfig.exe(コマンドラインツール)を用いる。インストールはWindows10の、Settings→Add an optional feature→ Add a feature→ Graphics Toolsを選択する事で行う。

dxcplのインストール

外部からDebug Layerを有効にする際は、dxcpl.exeかd3dconfg.exeを用いて、ターゲットとなるアプリケーションの名前を事前に登録し、Debug Layerを強制的に有効にする設定にする。設定内容はdxcplとd3dconfigで共有され、システム全体で有効になるので注意が必要である。

デバッグ対象アプリケーションを登録する
デバッグ対象アプリケーションを登録する

アプリケーション内部で設定する場合は、CreateDeviceを実行する前に、ID3D12Debugインターフェースを取得して、EnableDebugLayer()を呼び出す事で有効にできる。 この場合は、dxcpl.exeやd3dconfig.exeによるターゲットアプリケーション名の登録は必要ない。登録してある場合は、debug-layerの設定はApplication Controlledに設定することでAPIから明示的に有効にした場合のみDebug Layerが有効になる。

// Create Deviceの前に設定する
{
    ComPtr<ID3D12Debug1> debug1;
    if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug1))))
    {
        debug1->EnableDebugLayer();
    }
}

Debug Layerの出力

Debug Layerが有効になっている状態では、アプリケーションのD3DAPIの使用において何らかの間違いが検出されれば、エラーの内容がデバッグ出力ストリームに文字列として出力される。出力メッセージはVisual StudioやDbgviewなどのツールを使って確認することができる。出力内容は、その深刻度に応じてグループ分けされている。

  • Info
    リソースの確保や開放などを通知する。デフォルトでMuteされている。
  • Warning
    APIの仕様から逸脱していないが、パフォーマンスの問題や、バグの発生の原因になりそうな状況を通知する。
  • Error
    APIの仕様から逸脱した状況が検出された場合に通知する。ただ、これが出力されるから、直ちにDEVICE_REMOVALが発生するという訳ではない。
  • Corruption
    リソースやオブジェクト(オブジェクト自身というよりは、多くはそのハンドル等)が破損していることが検出された場合に通知する。
  • Message
    上記に当てはまらない情報を通知する(メモリ不足等)

以下は、例としてResourceBarrierの遷移前リソースステートの指定が間違っていた場合に出力されたエラーである。ちなみにこのプログラムは、Debug Layerが無効な状態でも有効な状態でも正常に動作した。

D3D12 ERROR: ID3D12CommandList::ResourceBarrier: Before state (0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]) of resource (0x000001AE3B886890:'MyColorTex') (subresource: 0) specified by transition barrier does not match with the current resource state (0x400: D3D12_RESOURCE_STATE_COPY_DEST) (assumed at first use) [ RESOURCE_MANIPULATION ERROR #527: RESOURCE_BARRIER_BEFORE_AFTER_MISMATCH]
D3D12 ERROR: ID3D12CommandQueue::ExecuteCommandLists: Using ResourceBarrier on Command List (0x000001AE3B802060:'MyCommandList_Direct'): Before state (0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]) of resource (0x000001AE3B886890:'MyColorTex') (subresource: 0) specified by transition barrier does not match with the state (0x400: D3D12_RESOURCE_STATE_COPY_DEST) specified in the previous call to ResourceBarrier [ RESOURCE_MANIPULATION ERROR #527: RESOURCE_BARRIER_BEFORE_AFTER_MISMATCH]

Debug Layerはこのエラーを二か所で検出した。一つはID3D12CommandList::ResourceBarrier()呼び出し時に、もう一つは、ID3D12CommandQueue::ExecuteCommandLists()呼び出し時に検出した。しかしこれは、この種のエラーは常に二か所で検出されるという意味ではない。コマンドリストは他のコマンドリストの生成タイミングと関係なく生成する事ができ、その際のコマンドリスト作成時のリソースのステートは未確定になる場合がある。そのためDebug Layerは複数の箇所で可能な限りエラーの特定を試みる。上記の場合では、コマンドリスト作成時の対象リソースの事前ステートが確定できたので、ID3D12CommandList::ResourceBarrier()の呼び出し時にエラーが出力出来たという事である。
また、ステートが間違っていたリソースの名前が、‘MyColorTex’といった様に表示されるが、これはアプリケーション自身が、ID3D12Object::SetName()を通じて設定したものである。D3D12アプリケーションを開発し、各種デバッグ機能を使う予定がある場合は、可能な限り全てのD3D12Objectに名前をつけるべきである。すると、上記の様にエラーが発生した際のメッセージによって原因となったリソースの特定が簡単に行えるようになる。Command ListやDescriptor Heapなどにもしっかりと名前を付けると、上記の様にエラーが発生したコマンドリスト名からエラーがどのレンダリングパスで発生したのかが特定できる場合もある。また、PIXやNSightといったフレームプロファイラを使う場合にもこれらの名前付けは有用である。

次の例は、RenderTargetを設定したクリアカラー以外でクリア場合に発生する警告である。これはエラーではないので無視しても構わない。しかし、このようにパフォーマンスの向上を考える場合に有用なメッセージが得られる場合もある。

D3D12 WARNING: ID3D12CommandList::ClearRenderTargetView: The application did not pass any clear value to resource creation. The clear operation is typically slower as a result; but will still clear to the desired value. [ EXECUTION WARNING #820: CLEARRENDERTARGETVIEW_MISMATCHINGCLEARVALUE]

ID3D12InfoQueueについて

Debug Layerは、時にはアプリケーションが意図して記述しているコードに対してもメッセージを出力する場合がある。その場合は、アプリケーションが無視するべきと考えるメッセージを、D3D12InfoQueueを使ってフィルタリングできる。以下のコードスニペットは、GPUが書き込みしている可能性のあるリソースがCPUから読み込み可能な状態でMapされている場合に出力される警告を抑制するためのものである。

ComPtr<ID3D12InfoQueue> d3dInfoQueue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&d3dInfoQueue))))
{
    // Suppress individual messages by their ID.
    D3D12_MESSAGE_ID denyIds[] =
    {
        D3D12_MESSAGE_ID_EXECUTECOMMANDLISTS_GPU_WRITTEN_READBACK_RESOURCE_MAPPED,
    };

    D3D12_INFO_QUEUE_FILTER filter = {};
    filter.DenyList.NumIDs = _countof(denyIds);
    filter.DenyList.pIDList = denyIds;
    d3dInfoQueue->AddStorageFilterEntries(&filter);
    OutputDebugString(L"Warning: GPUTimer is disabling an unwanted D3D12 debug layer warning: D3D12_MESSAGE_ID_EXECUTECOMMANDLISTS_GPU_WRITTEN_READBACK_RESOURCE_MAPPED.");
}

Microsoft DirectX SDK Sampleより引用

メッセージのフィルタリングは、InfoQueueを通じてではなく、dxcpl/d3dconfigを使っても同様のフィルタリングの設定が可能だが、メッセージのフィルタリングはアプリケーションごとに行われるべきであるので、通常はアプリケーションのコードに記述されるべきである。ちなみに、InfoQueueの設定は、dxcpl/d3dconfigの設定でオーバーライドされるので、InfoQueueを使って制御したいときは、dxcpl/d3dconfigにアプリケーションを登録してはいけない。 以下はID3D12InfoQueueのその他の機能についてである。

  • InfoQueueのデフォルト設定では、Infoレベルのメッセージはフィルタリングされているので、Infoレベルのメッセージを取得する必要がある場合はフィルタの設定を一旦クリアする必要がある。

  • フィルターにはStorageFilterとRetrievalFilterの二種類がある。
    StorageFitlerは、エラーがメッセージキューにストアするときに適用されるフィルタ。フィルターを通過できなければ、メッセージキューにストアされない。 RetrievalFilterはメッセージを取得する際に適用されるフィルタ。メッセージキューにストアされているメッセージを破壊せずに、特定の種類のメッセージのみを抽出したいときなどに使う。

  • SetMuteDebugOutputでデバッグ出力ストリームへの出力を停止できる。 アプリケーション側で出力されるエラーのハンドリングを全て行う場合などで、デバッグ出力ストリームへの出力が不要な場合は抑止できる。

  • 特定のエラーが検出された時や、エラーの深刻度によって、DebugBreakすることが可能。 Debug LayerはCPU側のD3D12ランタイムがエラーを検出しているので、エラーが発生するタイミングは、CPU処理と同期したタイミングが多い。したがって、DebugBreakすることは有効である。 しかし、DebugBreakがかかるのは、D3Dのランタイム側のスレッドでかかる場合もあるので、追跡するには、マルチスレッドのデバッギングが必要になる。

GPU Based Validationの有効化

DEVICE_REMOVEDへの処方の第二候補は、GPU Based Validationの有効化である。GPU Based Validation(以下GBV)は、その名の通り、GPU側での実行時に行うValidationである。 GBVもアプリケーション自身で有効にすることもできるし、dxcplなどで強制的に有効にすることもできる。この点はDebug Layerと同様である。なお、Debug Layerが有効化されていないと動作しないので、Debug Layerの拡張機能と考える事もできる。

{
    ComPtr<ID3D12Debug1> debug1;
    if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug1))))
    {
       debug1->EnableDebugLayer();
       debug1->SetEnableGPUBasedValidation(true);
    }
}

先ほど説明したDebug Layerは主にCommandListに命令を積み、ExecuteCommandListを呼び出すまでに行われるValidation。対してGBVはシェーダー実行時に行われるValidationになる。 未定義のDescriptorや、廃棄済みのリソースへのアクセス。不適切なリソースステートでのアクセスなど、CommandList作成時には、リソースの状況が未定で、検出できないエラーを実行時に検出する。 メッセージは既存のDebug Layerと同様に出力されるが、その出力のタイミングはコマンドリストを生成したCPU処理と同期しない。したがって、エラーメッセージが出力された瞬間のCPU処理を検証しても意味がない。

D3D12_MESSAGE_ID enumerationを確認すれば、GBVで出力されるメッセージのIDには、 “GPU_BASED_VALIDATION"が含まれるのが分かる。これで実際にどのようなエラーが検出可能なのか分かる。

GBVは、シェーダーコードとPSOにパッチを充てる形で実現する。これらには、いくつかのモードがあり選択することができる。GBVの設定は以下のAPIと構造体を通じて設定を行う。

ID3D12DebugCommandList1::SetDebugParameter()
typedef struct D3D12_DEBUG_DEVICE_GPU_BASED_VALIDATION_SETTINGS {
  UINT                                                   MaxMessagesPerCommandList;
  D3D12_GPU_BASED_VALIDATION_SHADER_PATCH_MODE           DefaultShaderPatchMode;
  D3D12_GPU_BASED_VALIDATION_PIPELINE_STATE_CREATE_FLAGS PipelineStateCreateFlags;
} D3D12_DEBUG_DEVICE_GPU_BASED_VALIDATION_SETTINGS;

以下はシェーダーのパッチモードの選択である

  • NONE
    シェーダーコードにValidationコードを挿入しないモード。 CommonStatePromotionによるリソースステートの遷移をトラッキングすることができない。そればかりかGBVを混乱させる恐れがある。

  • TRACKING_ONLY_SHADERS
    リソースステートの遷移のみをチェックするためのコードが挿入される。

  • CREATE_UNGUARDED_VALIDATION_SHADERS
    GBVのValidationコードが挿入される。Validationによるエラーが検出され、無効なリソースに対するアクセスや範囲外アクセスがあっても該当コードを実行する。結果、DEVICE_REMOVEDなどを引き起こすかもしれない。これがデフォルトのシェーダーパッチモード。

  • CREATE_GUARDED_VALIDATION_SHADERS
    GBVのValidationコードが挿入される。Validationによるエラーが検出された場合は、該当のリソースアクセスを避ける。

PipelineStateCreateFlagsでは、事前にPatchされたPSOを生成するかどうかを制御できる。 デフォルトでは、パッチがあてられたPSOの初回使用時にコンパイルされる挙動なので、CommandListのRecordingが遅くなる。FRONT_LOADを設定することで予めコンパイルされる設定になる。

以下はGBVによって検出されたエラーの一例。UAVの範囲外にシェーダーがアクセスしたことで出力された。この種のバグは、CPU側のDebug Layerでは検出できないが、GBVならば検出できる。

DescriptorTableのUAVに設定したUAVバッファに対する範囲外アクセス (RootSignature1.1を使用。Range Flagは D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE)
D3D12 ERROR: GPU-BASED VALIDATION: Draw, Resource access out of bounds: Resource: 0x000001C6F8F91A60:'DummyResource_256_bytes_UAV_buffer', Descriptor Type: UAV, Highest byte offset from view start accessed: [439737], Bytes available in view: 256. Results undefined because descriptor is declared static in root signature, which allows hardware/driver the option of converting the access to a root descriptor. Unlike descriptor heap descriptors, root descriptors do not have defined behavior for an out of bounds access. Index of Descriptor Range: 1, Shader Stage: PIXEL, Root Parameter Index: [0], Draw Index: [0], Shader Code: <debug info not available>, Asm Instruction Range: [0xbc-0xdf], Asm Operand Index: [2], Command List: 0x000001C6F8E6DA10:'MyCommandList_Direct', SRV/UAV/CBV Descriptor Heap: 0x000001C6F8D8AB60:'Unnamed ID3D12DescriptorHeap Object', Sampler Descriptor Heap: <not set>, Pipeline State: 0x000001C6F8BC81B0:'Unnamed ID3D12PipelineState Object',  [ EXECUTION ERROR #1005: GPU_BASED_VALIDATION_RESOURCE_ACCESS_OUT_OF_BOUNDS]

ここで、GBVの話から少しそれるが、このエラーについて詳しく考えてみたいと思う。また、これらの出来事は私のローカル環境で観測されたに過ぎないことも明記しておく。 上記のエラーメッセージを要約すると以下の通りと思われる。

リソースへの範囲外アクセス。リソース:`ummyResource_256_bytes_UAV_buffer` デスクリプタタイプ:UAV 最高でオフセット[439737]にアクセスした。Viewでアクセス可能なのは 256. 
アクセスの結果は未定義です。なぜなら、デスクリプタはRootSignatureで`static`として宣言されており、ハードウェアやドライバーはこの(メモリ)アクセスをルートデスクリプタにコンバートする選択肢が許されているからです。
デスクリプタヒープのデスクリプタと異なり、ルートでスクリプタには範囲外アクセスの挙動の定義がありません。

このUAVはDescriptorTableに定義したが、RangeFlagに、D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTEを設定した。このフラグが設定されたものはドライバーの最適化対象になる可能性があり、RootDescriptor(RootTableに直接定義するDescriptor)にコンバートされる可能性がある。 実際にコンバートされた場合は、範囲外アクセスは未定義動作となるので、エラーになっているという訳である。しかし、実際はリソースのアクセス範囲チェックがされていた(つまり、RootDescriptorへのコンバートは行われていなかった)ので、DEVICE_REMOVEDが発生するような致命的な事態にはならなかった。

次に、このUAVが設定されているDescriptorTableのRangeFlagに、D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILEを設定するとどうなるかというと、エラーが全く出力されなくなった。これは、DirectXの仕様として、RootSignature1.1のDescriptorTableに定義されたUAVで、D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILEを設定された場合、もしくはRootSignature1.0で定義されたUAVの場合は、リソースアクセスの範囲チェックが行われる決まりがある。範囲外の読み出しはゼロを返され、範囲外への書き込みは行われない。DirectXの仕様に則った動作なのでエラーが発生しないというわけである。

次は、DescriptorTableを介さずに、直接RootTableにUAVを定義して、範囲外アクセスを起こすと以下のメッセージが出力された。

RootTableに設定したUAVバッファに対する範囲外アクセス
D3D12 ERROR: GPU-BASED VALIDATION: Draw, Root descriptor access out of bounds (results undefined): Resource: 0x000001A7600AF410:'DummyResource_256_bytes_UAV_buffer', Root Descriptor Type: UAV, Highest byte offset from view start accessed: [803581], Bytes available from view start based on remaining resource size: 256. Shader Stage: PIXEL, Root Parameter Index: [1], Draw Index: [0], Shader Code: <debug info not available>, Asm Instruction Range: [0xc8-0xeb], Asm Operand Index: [2], Command List: 0x000001A75F82C5B0:'MyCommandList_Direct', SRV/UAV/CBV Descriptor Heap: 0x000001A75F9DEA70:'Unnamed ID3D12DescriptorHeap Object', Sampler Descriptor Heap: <not set>, Pipeline State: 0x000001A75FDC5DE0:'Unnamed ID3D12PipelineState Object',  [ EXECUTION ERROR #961: GPU_BASED_VALIDATION_ROOT_DESCRIPTOR_ACCESS_OUT_OF_BOUNDS]

さらに、DEVICE_REMOVED発生した。
D3D12: Removing Device.
D3D12 ERROR: ID3D12Device::RemoveDevice: Device removal has been triggered for the following reason (DXGI_ERROR_DEVICE_HUNG: The Device took an unreasonable amount of time to execute its commands, or the hardware crashed/hung. As a result, the TDR (Timeout Detection and Recovery) mechanism has been triggered. The current Device Context was executing commands when the hang occurred. The application may want to respawn and fallback to less aggressive use of the display hardware). [ EXECUTION ERROR #232: DEVICE_REMOVAL_PROCESS_AT_FAULT]

先ほどとエラーメッセージが異なり、エラーのIDが異なるので注意が必要である。以上の出来事をまとめると以下の様になる。

  • DescriptorTableに定義した場合
    #1005: GPU_BASED_VALIDATION_RESOURCE_ACCESS_OUT_OF_BOUNDS
    こちらのエラーは、VOLATILEでないDescriptorTableに定義されたリソースに対する範囲外アクセスで発生したエラー。 ハードウェアやドライバーが、範囲外アクセスを未定義動作にすることが許されている状態だが、実際に範囲外アクセスをするかは実装次第。
  • RootTableに直接定義した場合
    #961: GPU_BASED_VALIDATION_ROOT_DESCRIPTOR_ACCESS_OUT_OF_BOUNDS
    こちらは、DescriptorTableではなく、RootTableに定義されたリソースの範囲外アクセスで発生したエラー。 RootTableにUAVやSRVを定義した場合、リソースのサイズは格納されない事が知られており、通常は範囲外アクセスへのチェックも行われない事が知られている。しかし、GBVを有効にすることでこれらの範囲外アクセスがValidatorにより検出され、 エラーが出力されたという状態。

このように、エラーメッセージから学べる事もあるので、Debug LayerやGBVを有効にするのはおすすめである。

Debug Layerのその他の機能

Synchronized Command Queue Validation

Debug Layerを有効にすることで、Synchronized Command Queue Validationという機能がでデフォルトで有効になる。 この機能によって、FenceのWaitが設定されたコマンドリストにおいて、Waitの条件が満たされるまで、GPUへのコマンド送出をしなくなる。 これにより、Waitが設定されている以降のコマンドにおけるリソースステートをCPU側でも確認することができ、結果として、コマンド送出時にリソースステートのValidationをより厳密に行う事ができる。 Disableにすることによって、FenceのSignalとWaitを多用したQueueの組み立てをしている場合に限り、Debug Layer使用時の若干のパフォーマンス向上が期待できるが、そもそもDebug Layerはパフォーマンスを追求するためのものでは無いのでDisableにするメリットは殆どない。

DebugDevice / DebugCommandQueue / DebugCommandList

Debug Layerが有効な状態では、Device, CommandQueue, CommandListからQueryInterfaceすることで、表題のインターフェースが取得できる。 主な機能は以下の通り。

  • ID3D12DebugDevice::ReportLiveDeviceObjects()
    現在有効なオブジェクトをデバッグ出力ストリームに出力する。
  • ID3D12DebugCommandList::AssertResourceState()
    リソースのステートが、呼び出し引数に与えたステートと等しいかを返す。
    Common State Promotionを使う場合は、これでState PromotionやDecayの確認をするとデバッグしやすい。
  • ID3D12DebugCommandQueue::AssertResourceState()
    リソースのステートが、呼び出し引数に与えたステートと等しいかを返す。
    CommandQueuから直接リソースを操作するAPIがある関係上、CommandQueuからもリソースのステートが確認できる。

Device Removed Extended Data 1.2

Device Removed Extended Dataとは、実際にDEVICE_REMOVEDが発生した後に、発生のより詳しい状況を知るための機構である。通常はDEVICE_REMOVEDが発生しても、得られる情報はせいぜいHRESULTのエラーコードぐらいで、デバッグの指標となる情報はほとんどない。しかし、DREDを活用すれば、DEVICE_REMOVEDが発生した時にGPUが実行していたコマンドや、原因となったメモリアクセスについて知ることができる場合がある。 Debug Layerとは機能的に独立しているので、使用にあたりDebug Layerを有効にする必要はない。また、Debug Layerほど処理オーバーヘッドが大きくないので、常時有効にしてアプリケーションを開発することができる。 以下は、DREDの主要機能を有効にするためのコードスニペットである。DRED自体はWindowsSDKの10.0.18362.1より使用可能だが、一部重要な機能が未実装なので、WindowsSDKの10.0.19041.0以後の導入とWindows10 20H1の導入を推奨する。

// Try enabling DRED even in release code
{
  ComPtr<ID3D12DeviceRemovedExtendedDataSettings1> d3dDredSettings1;
  if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&d3dDredSettings1)))) {
    // Turn on AutoBreadcrumbs and Page Fault reporting
    d3dDredSettings1->SetAutoBreadcrumbsEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);
    d3dDredSettings1->SetBreadcrumbContextEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);
    d3dDredSettings1->SetPageFaultEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);
  }
}

Auto BreadcrumbsとBreadcrumb Contextについて

Breadcrumbsは、パンくずのことで、所謂通ってきた道を見失わないためにパンくずを撒きながら森の中を歩いた童話にちなんでいる。Auto Breadcrumbsは、明示的にAPIを呼び出してパンを撒かなくても自動的に道標なるイベント(D3D12のAPI呼び出し)を自動的に記録するための機能である。 Auto Bredcrumbsが記録するのは、基本的には、CommandListを介して実行するコマンド群である。詳細は D3D12_AUTO_BREADCRUMB_OP enumerationで確認できる。 そして、DEVICE_REMOVEDが発生する直前に実行したメソッドを指し示すことで、DEVICE_REMOVEDが発生した瞬間にGPUが実行していたオペレーションが分かる仕組みになっている。

しかし、Auto Bredcrumbsは実行したコマンドの種類を記録するだけなので、連続する一連のDrawなどでは、実際にどのDrawコールが問題を引き起こしたか分からない。 Breadcrumb Contextは、Auto Breadcrumbによって記録されたオペレーションに関連する情報を記録した文字列が取得できるDRED1.2で導入された新しい機能である。 具体的には、Pixのマーカーがセットされた場合は、そのマーカーの文字列が記録される。これにより、大幅にレンダリング箇所の特定が行いやすくなった。

GPU Page Faultについて

GPU Page Faultは、GPU上で発生する不正なメモリアクセスで、これが発生するとDEVICE_REMOVEDとなる。DREDはGPU Page Faultの情報を記録する。まずはGPU Page Faultを理解するためにGPU仮想アドレス空間について簡単に説明する。

GPU仮想アドレス空間について

GpuMmuは、WDDM2.0(Windows Display Driver Model 2.0)でサポートされている、主にディスクリートGPU(VRAMとシステムメモリが物理的に独立しているGPU)のための 仮想アドレスモデルである。このモデルでは、プロセスごとに、GPU仮想アドレス空間がCPUの仮想アドレス空間とは別に存在して、物理アドレスに変換するためのMMUも、CPUのMMUとは別に存在している。 GPU仮想アドレス空間は、その名の通りGPU上で実行されるシェーダー等からメモリアクセスをする際に使用されるアドレス空間である。CPU側(D3D12APIやドライバー)でのリソース確保や解放によって、物理メモリが確保または破棄されて、アドレス変換テーブルが更新される。 アドレス変換テーブルが更新される際にはGPU側と同期して、GPU側と同じアドレス変換情報を共有することで、GPU上での仮想アドレスにおけるメモリアクセスを実現している。 図にある通り、物理リソースへのアクセスはアドレステーブルによる変換を介して行う。また、マップされるメモリは、VRAMでもSysMemでも構わない。GPUはどちらに配置されているリソースでも、透過的にアクセスすることができる。

GPU Page Faultが起きるケース

GPUがPage Faultを起こすのは、アクセスが許されないページにアクセスした場合や、そもそもメモリがマッピングされていないアドレスにアクセスした場合である。主に具体的なケースとして考えられるのは、以下の通りである。

  • DrawcallやDispatch,Copy処理などにおいて、すでに破棄したリソースを参照した場合。
  • DrawcallやDispatch,Copy処理などにおいて、Evictしたリソースや、Non-Regidentなタイルリソースを参照した場合。
  • DrawcallやDispatchで、未初期状態のDescriptorTableや、誤ったDescriptorTableを参照した場合。
  • DrawcallやDispatchで、可変長のDescriptorTableで、シェーダーが実際に配置されているテーブルの範囲を逸脱してアクセスした場合。
  • DrawcallやDispatchでRootTableに配置したUAVやSRVに対して誤った範囲でアクセスした場合。

GPU Page Faultで得られる情報について

DREDは、PageFaultが発生したアドレス空間に確保されているオブジェクトが有れば、そのオブジェクト名(SetNameで付けた名前)が記録される。 またAllocationTypeとして、そのアドレス空間に配置されたオブジェクトが、 どのような種類であるかを知ることができる。 また、そのアドレス空間を使っていて、直近で解放されたリソースがあれば、そのリソースの情報が取得できる。これは、解放されたリソースに対して、シェーダー等がアクセスした場合に発生するPage Faultを知るのに特に有用である。 しかし、GPU Page Faultはあくまで、GPU仮想アドレス変換時のエラーでしかないので、アクセスしたアドレスに有効なページがあればアクセス自体が成立するため、GPU page faultにならない。したがって、すべての不正アクセスを検出するわけではない。 たとえば、EvictしたリソースはVRAMが特に逼迫した状況になるまではリソースのページアウトが起きないため、そのままVRAM上に配置されていることが多い。結果Page Faultも起きない上に、正しくレンダリングされるため、問題に気づけない。

DREDで得られる情報で何が分かるか

DREDは、一見するとDEVICE_REMOVEDの発生原因についての十分な情報を提供してくれるように思えるが実際は違う。 AutoBreadCrumbは、エラーが発生していた時に実行していたコンテキストに過ぎず、実際にエラーの原因がその中にあるとは限らない。 Page Faultも同様で、Page Faultは発生した一つのアクセス例外に過ぎず、何がアクセス例外の原因となったかは分からない。たとえば、それが古いDescriptor Tableを参照したことによるのか、 破損したDescriptor Tableを参照したことによるのか、参照しているリソースを開放してしまったことによるのかは分からない。

しかし、DEVICE_REMOVEDが頻発する状況下では、DREDで複数のクラッシュの情報を集約することは非常に有効である。例えば、もしも、PageFaultがいつも同じリソースとアドレスで発生するとしたら、 プログラムのロジックが安定的な間違いを犯している可能性が高いと考えられる。また、そうではなく、PageFaultがいろいろなリソースやアドレスで発生するとしたら、リソースやDescritorTableを管理しているスレッドと GPUの実行コンテキストのレースコンディションを調べる価値があると考えられる。AutoBreadCrumbも同様で、毎回同じドローコールでDEVICE_REMOVEDが発生しているならば、 該当ドローコールのロジックや、実行分岐制御に関わる変数やリソースを調べるべきだが、異なるドローコールでランダムにDEVICE_REMOVEDが発生するならば、コマンドリストの破損の可能性が考えられる。

以下はCommandList作成時には存在していたTextureがExecuteCommandListsの前に解放された場合に発生するGPU Page Faultによって発生した、DEVICE_REMOVEDの際に取得できたDREDの情報である。なお、DREDの情報はデバッグ出力ストリームに自動的に出力されないので、 自身でデータにアクセスして、何らかの形で表示する必要がある。

DXGI_ERROR_DEVICE_HUNG
The GPU will not respond to more commands, most likely because of an invalid command passed by the calling application.
==== Auto Breadcrubs ====
QueueNameW: MyCommandQueue
QueuePtr: 0x2bad9c40330
BreadcrumbCount: 0
BreadcrumbContextsCount: 0
LastBreadcrumbValues: 0
==== Auto Breadcrubs ====
QueueNameW: MyCommandQueue
QueuePtr: 0x2bad9c40330
CommandListNameW: MyCommandList_Direct
CommandListPtr: 0x2bad9e379f0
BreadcrumbCount: 7
BreadcrumbContextsCount: 3
LastBreadcrumbValues: 5
  0|D3D12_AUTO_BREADCRUMB_OP_SETMARKER|==Frame Start==
  1|D3D12_AUTO_BREADCRUMB_OP_SETMARKER|Set viewport and render targets
  2|D3D12_AUTO_BREADCRUMB_OP_RESOURCEBARRIER
  3|D3D12_AUTO_BREADCRUMB_OP_CLEARRENDERTARGETVIEW
  4|D3D12_AUTO_BREADCRUMB_OP_SETMARKER|Draw - Triangle
<<<<<<Something wrong happned here...>>>>>>
  5|D3D12_AUTO_BREADCRUMB_OP_DRAWINSTANCED
  6|D3D12_AUTO_BREADCRUMB_OP_RESOURCEBARRIER
====Page fault information ====
PageFaultGPUVA: 0x70fc000
==Existing Allocation Node Info
==Recent Freeed Allocation Node Info
ObjectNameW: DummyResource_256_bytes_UAV_buffer
AllocationType: D3D12_DRED_ALLOCATION_TYPE_RESOURCE
IUnknownPtr: 0x0x2bad9e8f9c0
D3D12app.exe has triggered a breakpoint.

Dump File について

DREDの情報はユーザーモードダンプからも抽出することができる。まずは、 プロセスがCrashした際に、FullDumpが作られる様に事前に設定し、ダンプファイルをwindbgで読み込む。 windbg.exeはWindows10のSDKに同梱されている。通常は、“C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe"に配置されるはずである。 そこで、 MicrosoftがGitHubで公開しているスクリプトを読み込むことで、DREDの情報に容易にアクセスできる。 手順は該当のリポジトリでも確認できるが非常に簡単である。プロセスがクラッシュした際のフルダンプを読み込み、以下のコマンドを実行するだけである。

.scriptload <<path to script file>>\d3ddred.js
!d3ddred

以下が、Windbg上で実際にDRED情報を表示した例である。取得できる情報は、DREDのAPIで取得できる情報と同一である。

Windbg上で、DRED1.2の情報を確認する

最後に

これら全てを駆使しても簡単に判明しないDEVICE_REMOVEDも存在すると思うが、DEVICE_REMOVEDを手さぐり的に解決する時代は終わりを迎えようとしていると言えると思う。

shikihuiku
shikihuiku

リアルタイムレンダリングが好き

Related