19.Dx12デバイスの実装開始!

このブログのタイトルである「実験と実装と」は、この項から始まる「Dx12」の実装のためにつけられたようなものです。
BaseCrossのDx12については、マイクロソフト社の「DirectX-Graphics-Samples」を僕なりに研究し、実装したものです。まだ、新しいゲームエンジンであり、「DirectX-Graphics-Samples」以外の資料も少ない中、暗中模索のなかで、一定の方法での実装にたどり着きました(というか途中段階です)。
しかし、まだ現在の実装がベストなものであるかどうかはわかりません。もっと効率よく、そして速い方法があるかもしれません。それは今後の修正、検証の中で、よりベターな方法へ向かえばいいのかなと思っています。
もし、コードに「この書き方はふつうやらないよ」とか「こんな書き方があるよ」なんてお気づきの点がありましたら「バグも含めて」、「お問い合わせ」からお送りいただけるとありがたいです。
ブログの最初のほうの記事になりますように、僕も、公開されている情報しか取得できる立場にありません。「Windows10、SDK」「VS2015」「DirectX-Graphics-Samples」の3つの条件および情報の中で、BaseCrossを作成しております。

マイクロソフト社の「DirectX-Graphics-Samples」のGitHubサイトは以下になります。

https://github.com/Microsoft/DirectX-Graphics-Samples

ぜひ、興味ある方はダウンロードしてみてDx12研究を始めるといいと思います。

この記事は、
コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」
から、
コミット「weak_ptrからshared_ptr取得方法の修正」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。
その間にいくつかのコミットがありますが、後ろのコミットを開いていただいてそれと合わせ読んでいただくとわかりやすいと思います。

Dx12での開発ですので

Windows10
Windows10SDK

の両環境は必須になります。

この記事は「シンプルバージョンDx12」に関する記事です。
上記コミットの「SimplSampleTest」内「BaseCrossDx12.sln」を開き、以降の文章を読んでください。

Dx12の初期化
「DirectX-Graphics-Samples」にあるサンプルは「x64」をベースにしています(つまり64ビット)。
しかし、BaseCrossは32ビットオンリーとして作成します。これは、前バージョン(DxBase2015、DxBase2016)と(完全ではないにせよ)上位互換をとる必要があるのと、今後OpenGL環境との「クロス化」を考えた時に、現時点で「x64」をターゲットとして作成するのはまだ早急と考えたからです。
そうした時に、「DirectX-Graphics-Samples」のコードをそのまま32ビット環境で動かせるか、という「バージョンダウン的な」問題も出てきます。「DirectX-Graphics-Samples」に32ビットの選択肢がないのは、何らなの理由があるからでしょうし、それらは実装してみないとわかりません。

Dx12に限らず。グラフィックデバイスは「初期化処理」というのがつきものです。
以下に、BaceCrossで実装しているDx12の初期化を並べてみました。「DirectX-Graphics-Samples」内のサンプルを参考にしています。

これは「Dx12Lib」内の「DeviceResources.cpp」に記述があります。
「DeviceResources::Impl::CreateDeviceResources関数」です。

ここで「Implってなんだ」と思う人もいると思います。これは「イディオム」といって、ヘッダからメンバ変数を隠すアイディアの一つです。
「DeviceResources.h」を見てみるとわかると思いますが、「DeviceResources」宣言部にはアクセサや操作関数はありますが、メンバ変数の宣言はありません。
そのかわりprivate変数として、

    private:
        // pImplイディオム
        struct Impl;
        unique_ptr<Impl> pImpl;

という変数があります。「Impl」という謎の構造体の宣言だけあり、その「unique_ptr」があります。
この「Impl」はインプリメント(組み込み)構造体という意味で、cpp側でその宣言定義が行われています。
「DeviceResources.cpp」でその内容を見てみますと

    struct DeviceResources::Impl {
        static const UINT FrameCount = 2;
        //パイプラインobjects.
        D3D12_VIEWPORT m_Viewport;
        D3D12_RECT m_ScissorRect;
        ComPtr<IDXGISwapChain3> m_SwapChain;
        ComPtr<ID3D12Device> m_Device;
        ComPtr<ID3D12Resource> m_RenderTargets[FrameCount];
        ComPtr<ID3D12Resource> m_DepthStencil;

        ComPtr<ID3D12CommandAllocator> m_CommandAllocator;
        ComPtr<ID3D12CommandQueue> m_CommandQueue;
        //RenderTargerView Heap
        ComPtr<ID3D12DescriptorHeap> m_RtvHeap;
        //DepsStensilViewHeap
        ComPtr<ID3D12DescriptorHeap> m_DsvHeap;
        UINT m_RtvDescriptorSize;
        //クリア処理用のオブジェクト
        ComPtr<ID3D12RootSignature> m_RootSignature;
        ComPtr<ID3D12PipelineState> m_PipelineState;
        ComPtr<ID3D12GraphicsCommandList> m_CommandList;
        //プレゼントバリア用のコマンドリスト
        ComPtr<ID3D12GraphicsCommandList> m_PresentCommandList;
        //コマンドリスト実行用の配列
        vector<ID3D12CommandList*> m_DrawCommandLists;
        //同期オブジェクト
        UINT m_FrameIndex;
        HANDLE m_FenceEvent;
        ComPtr<ID3D12Fence> m_Fence;
        UINT64 m_FenceValue;
        float m_dpi;
        float m_aspectRatio;
        //構築と破棄
        Impl(HWND hWnd, bool isFullScreen, UINT Width, UINT Height);
        ~Impl();
        //リソースの構築
        void CreateDeviceResources(HWND hWnd, bool isFullScreen, 
                UINT Width, UINT Height);
        //アダプター取得
        void GetHardwareAdapter(_In_ IDXGIFactory2* pFactory, 
                _Outptr_result_maybenull_ IDXGIAdapter1** ppAdapter);
        //同期処理
        void WaitForPreviousFrame(bool ExceptionActive = true);
    };

のようになっていて、デバイスが保持するメンバ変数はこの中に閉じ込めてあります。
ではなぜこのようなまどろっこいことをしているのでしょうか?

BaseCrossは「Libファイル」を作成しませんが、「DirectXTex」のようにLibファイルを作成するプロジェクトは、そのライブラリを使うために「ヘッダを公開」する必要があります。
つまり、リンカはそのヘッダ情報と、libファイルを使って、実行ファイルの中にライブラリブロックを埋め込むわけです。
ヘッダにメンバ変数があった場合、そのライブラリを使うユーザーに「このクラスにはどのようなメンバ変数があるか」を知らせてしまいます。まあ、BaseCrossのように公開されてるソースであれば、別に知られてもいいのですが、中には「ブラックボックス」としてlibファイルを提供する場合もあります。
そんな時、メンバ変数が見えてしまう、ということが、ちょっとまずい場合があるのです。つまり、そのライブラリを使う側のプログラムから「強制的に」メンバ変数を書き換えてしまうことができてしまうのです。
「いやそんなことないでしょう。だってpublic変数ならまだしも、メンバ変数は通常priveteもしくはprotectedにするでしょ」と思うかもしれません。でも、実際には「privateを無効にする方法」があるんです。(方法は各自調べてください)
そんなことからヘッダに書くメンバ変数は、セキュリティ的な意味でもまずい部分があるわけです。

他にも、「ビルドするファイル数を減らせる」というメリットもあります。ヘッダに書くと、そのヘッダを変更した場合、そのヘッダをインクルードしているcppファイルをすべてコンパイルしなおします。しかし、Implを使うとImpleを変更しただけではそのcppしか影響を与えません。

僕自身の考えを言えば、セキュリティ的にも重要だし、コンパイル効率が上がるのも事実なので、それだけでもImplを使う理由になるのですが、もう一つ、ヘッダファイルがすっきりする、ということも大きいですね。
Implを使うようになってから、ヘッダファイルは、ほぼ、関数宣言だけの並びで記述できます。
そうすると、何をするクラスで公開されている関数は何なのか?が明確になり、スッキリします。僕はこの理由だけでもImpleは価値があると思います。

ただ1つ、テンプレートと共存するのが難しい、という問題点があります。
テンプレートを多用しているクラスだと、いくらpImplを使っても、コード実装がヘッダ部に記述せざるを得ません。これはテンプレートの宿命ですね。
なので、テンプレートを多用している「DxLib内App.h」の「Appクラス」はImplを持ってません。実装しようとしたのですが、テンプレートから呼び出すSub関数の地獄におちいり、Impl実装をあきらめました。

このように何事も「臨機応変」なのも重要かと思います。

Implポインタを初期化する方法は以下のようになります。

    DeviceResources::DeviceResources(HWND hWnd, bool isFullScreen, UINT Width, UINT Height) :
        pImpl(new Impl(hWnd, isFullScreen, Width, Height))
    {}

このように、Implを持つクラスのコンストラクタで、初期化します。後始末は、pImplはunique_ptrなので、DeviceResourcesクラスが破棄されたときに勝手に破棄されます。

Implの説明に時間をとってしまいましたが、以下、Dx12デバイスの初期化方法です。「DeviceResources::Impl::CreateDeviceResources」に記述があります。

1、DXGIFactoryの作成の作成
アダプター及びスワップチェーンを作成するのに必要なファクトリーです。

    //DXGIFactoryの作成
    ComPtr<IDXGIFactory4> factory;
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&factory)),
        L"DXGIFactoryの作成に失敗しました",
        L"CreateDXGIFactory1(IID_PPV_ARGS(&factory)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
        );

このように、ローカル上に「factory変数」を作成し「CreateDXGIFactory1関数」を呼び出します。
ここで使用している「ThrowIfFailed関数」は「DxLib」の「BaseHelper.h」に記述がある関数です。第1引数に「HRESULTを返す関数」を記述し、第2引数以降は「失敗した時のメッセージ文」を与えます。3つ与えられ、「エラー事象」「エラーが起こったブロック」「エラーが起こった関数名」などを記述しています。このメッセージはこうでなければならないというものではありません。
2番目のメッセージ文は「エラーが起こった行番号」でもいいかもしれません。

ここで作成している「factory」はこのあとの「アダプタの取得」と「スワップチェーンの作成」に使用して、以降は使用しません。ですのでローカル変数でいいわけです。

2、アダプタの取得
続いてアダプタを取得します。これはGPUをカプセル化したものと考えて差し支えないと思います。
ここではまず「ハードウェアアダプタの取得」を試みます。そして失敗したら「ラップアダプタの取得」を試みます。
両方失敗したら、Dx12には対応してないということで終了します。

    //ハードウェアアダプタの取得
    ComPtr<IDXGIAdapter1> hardwareAdapter;
    GetHardwareAdapter(factory.Get(), &hardwareAdapter);
    if (FAILED(D3D12CreateDevice(hardwareAdapter.Get(), 
        D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&m_Device)))) {
        //失敗したらラップアダプタの取得
        ComPtr<IDXGIAdapter> warpAdapter;
        ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)),
            L"ラップアダプタの作成に失敗しました",
            L"factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter))",
            L"Dx12DeviceResources::Impl::CreateDeviceResources()"
        );

        ThrowIfFailed(D3D12CreateDevice(warpAdapter.Get(), 
           D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&m_Device)),
            L"デバイスの作成に失敗しました",
            L"D3D12CreateDevice(warpAdapter.Get(),D3D_FEATURE_LEVEL_11_0,IID_PPV_ARGS(&m_Device))",
            L"Dx12DeviceResources::Impl::CreateDeviceResources()"
        );

    }

「GetHardwareAdapter関数」はサブ関数で、ハードウェアアダプタを取得する関数です。
2016年夏時点で、「Windows10は入っているけどDx12には対応してない」というアクセラレータは結構あります。
そうした場合「ハードウェアアダプタの取得」に失敗して「ラップアダプタ」でDx12環境を構築することになりますが、はっきり言いて「ラップアダプタ」では、BaseCrossのサンプルくらいは動作しますが、ゲーム制作に対応できるとは思えません。
やはり開発環境としては「ハードウェアアダプタの取得」で成功する環境でないとしんどいと思います。
ただ、「Dx12の学習」という側面だけを考えれば、「ラップアダプタ」でもある一定の学習は可能です。もちろんDx12のハードウェアの性能を充分に引き出すことはできませんが、何度か言及してますようにDx12はDx11とは「全くの別物」と考えられるほど違いがあります。
それは、Dx11の場合、ある程度DirectX側でやってくれた処理を、プログラマが行わなければならないところが大きいわけですが、逆に考えればそういった「アクセラレータに近い部分の処理をする」という学習には役立とと思います。

3、コマンドキューの作成
「Dx12」は「コマンドリスト」という「パイプライン描画処理定義」のようなものを積み上げていきます。各オブジェクトの描画はまさにこの「コマンドリスト」を作成することです。「コマンドリスト」は「コマンドアロケータ」という工場みたいなもので作成されて、「コマンドキュー」という順番待ち行列に追加されます。
ここではその「コマンドキュー」の作成を行います。
以下はそのコードです。

    D3D12_COMMAND_QUEUE_DESC queueDesc = {};
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    ThrowIfFailed(m_Device->CreateCommandQueue(&queueDesc, 
        IID_PPV_ARGS(&m_CommandQueue)),
        L"コマンドキューの作成に失敗しました",
        L"m_Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CommandQueue))",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

4、スワップチェーンの作成
「スワップチェーン」というのは、簡単に言えば「バックバッファとフロントバッファを管理しバックバッファへの描画が完了したら、フロントバッファとバックバッファを入れ替えることでディスプレイに表示する」てなことを行うオブジェクトです。(簡単じゃない・・・)
では、説明を変えましょう。
ディスプレイは、極端にいえば「フロントバッファ」と呼ばれる「メモリ」をそのまま表示する窓みたいなものです。「バッファ」はメモリのことです。ここまではいいですね。
で、その「フロントバッファ」に直接描画するとどうなるか?そのままディスプレイは表示しようとするので、「ちらつき」が起こるわけです。
そのため、「フロントバッファ」と同じイメージのバッファを別に作っておいて(それが「バックバッファ」です)、そこにターン毎に必要な情報を書き込み終わったら、フロントバッファと差し替えます。そうするといままで「バックバッファ」だったものは「フロントバッファ」になるわけですから、ディスプレイに表示されますね。
そのかわりそれまで「フロントバッファ」だったものは「バックバッファ」になるので、そこに書き込んでもちらつきが起こらないわけです。
その入れ替え作業を行うのが、スワップチェーンです。以下が「スワップチェーンの作成」のコードです。

    DXGI_SWAP_CHAIN_DESC swapChainDesc = {};
    swapChainDesc.BufferCount = FrameCount;
    swapChainDesc.BufferDesc.Width = Width;
    swapChainDesc.BufferDesc.Height = Height;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    swapChainDesc.OutputWindow = hWnd;
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.Windowed = TRUE;

    ComPtr<IDXGISwapChain> swapChain;
    ThrowIfFailed(factory->CreateSwapChain(m_CommandQueue.Get(), &swapChainDesc, &swapChain),
        L"スワップチェーンの作成に失敗しました",
        L"factory->CreateSwapChain(m_CommandQueue.Get(), &swapChainDesc, &swapChain)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    ThrowIfFailed(swapChain.As(&m_SwapChain),
        L"スワップチェーンのバージョン変更に失敗しました",
        L"swapChain.As(&m_SwapChain)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    //フレームインデックスを設定しておく
    m_FrameIndex = m_SwapChain->GetCurrentBackBufferIndex();

ここで注意したいのは

    swapChainDesc.BufferCount = FrameCount;

です。「FrameCount」は定数になっていて「2」が入っています。この値は「フロントバッファを含めた」フレーム数を入れます。2ですので「フロントバッファ」「バックバッファ」で合計2ということになります。
また、

    swapChainDesc.Windowed = TRUE;

はウインドウモードかフルスクリーンかを設定します。現時点ではDx12版はウインドウモード固定になっています。(フルスクリーンはのちに対応予定です)

5、デスクプリタヒープの作成
Dx12には新しく「デスクプリタヒープ」という概念があります。
Dx12にもDx11同様、「レンダーターゲットビュー」「デプスステンシルビュー」というレンダリング対象となるビューを持っているわけですが、これらを実際に使用するためには、「デスクプリタ」と呼ばれる「定義」も一緒に設定しなければなりません。
さらに言えば「デスクプリタ」はただ構造体を渡せばいいというものではなく「デスクプリタヒープ」という「定義領域」を作成し、それを設定します。
以下は「レンダーターゲットビュー」「デプスステンシルビュー」のための「デスクプリタヒープ」を作成してます。

    //レンダーターゲットビューのデスクプリタヒープ作成
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
    rtvHeapDesc.NumDescriptors = FrameCount;
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    ThrowIfFailed(m_Device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_RtvHeap)),
        L"レンダーターゲットビューのデスクプリタヒープ作成に失敗しました",
        L"m_Device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_RtvHeap)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    //デプスステンシルビューのデスクプリタヒープ作成
    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
    dsvHeapDesc.NumDescriptors = 1;
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    ThrowIfFailed(m_Device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_DsvHeap)),
        L"デプスステンシルビューのデスクプリタヒープ作成に失敗しました",
        L"m_Device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_DsvHeap)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    //レンダリングターゲットビューのデスクプリタのサイズを取得しておく
    m_RtvDescriptorSize = m_Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

ここで注意したいのは、「レンダーターゲットビューのデスクプリタヒープ」は「FrameCount」つまり2つ作成します。つまり「フロントバッファ」「バックバッファ」用です。

6、レンダーターゲットビューの作成
その後実際にレンダーターゲットビューを作成します。

    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_RtvHeap->GetCPUDescriptorHandleForHeapStart());
    //FrameCount数だけRTVのフレームリソースの作成
    for (UINT n = 0; n < FrameCount; n++)
    {
        ThrowIfFailed(m_SwapChain->GetBuffer(n, IID_PPV_ARGS(&m_RenderTargets[n])),
            L"RTVのフレームリソースの作成に失敗しました",
            L"m_SwapChain->GetBuffer(n, IID_PPV_ARGS(&m_RenderTargets[n])",
            L"Dx12DeviceResources::Impl::CreateDeviceResources()"
        );
        m_Device->CreateRenderTargetView(m_RenderTargets[n].Get(), nullptr, rtvHandle);
        rtvHandle.Offset(1, m_RtvDescriptorSize);
    }

ここで作成する「レンダーターゲットビュー」の数も、「デスクプリタヒープ」同様、「FrameCount」数、つまり2つになります。

        m_Device->CreateRenderTargetView(m_RenderTargets[n].Get(), nullptr, rtvHandle);

が実際に「レンダーターゲットビュー」を作成しているコードですが、ここに5で作成したデスクプリタヒープのハンドル(IDみたいなもの)を結びつけますこのハンドルは、

    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_RtvHeap->GetCPUDescriptorHandleForHeapStart());

で取得するわけですが、最初は「スタート位置」、そして

        rtvHandle.Offset(1, m_RtvDescriptorSize);

でハンドル位置をシフトさせ、2番目の「レンダーターゲットビュー」を作成します。
非常にややこしい操作ですが、これで、レンダーターゲットビューのデスクプリタヒープの「先頭と2番目のハンドル」に2つの「レンダーターゲットビュー」を結び付けた格好になります。

7、デプスステンシルビューの作成
続いて「デプスステンシルビュー」作成は、以下のコードになります。

    D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = {};
    depthStencilDesc.Format = DXGI_FORMAT_D32_FLOAT;
    depthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
    depthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;

    D3D12_CLEAR_VALUE depthOptimizedClearValue = {};
    depthOptimizedClearValue.Format = DXGI_FORMAT_D32_FLOAT;
    depthOptimizedClearValue.DepthStencil.Depth = 1.0f;
    depthOptimizedClearValue.DepthStencil.Stencil = 0;

    ThrowIfFailed(m_Device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_D32_FLOAT, 
             Width, Height, 1, 0, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL),
        D3D12_RESOURCE_STATE_DEPTH_WRITE,
        &depthOptimizedClearValue,
        IID_PPV_ARGS(&m_DepthStencil)
    ),
        L"デプスステンシルリソース作成に失敗しました",
        L"m_Device->CreateCommittedResource)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

    m_Device->CreateDepthStencilView(m_DepthStencil.Get(), &depthStencilDesc, m_DsvHeap->GetCPUDescriptorHandleForHeapStart());

こちらは「レンダーターゲットビュー」と違って1つだけ作成します。

8、コマンドアロケータの作成
「3、コマンドキューの作成」で説明しましたが、「コマンドリスト」は「コマンドアロケータ」によって作成されます。ですので、「コマンドアロケータ」を作成しておかなければなりません。以下のコードで「コマンドアロケータ」を作成します。

    ThrowIfFailed(m_Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, 
               IID_PPV_ARGS(&m_CommandAllocator)),
        L"コマンドアロケータの作成に失敗しました",
        L"m_Device->CreateCommandAllocator()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

9、特殊なルートシグネチャの作成
Dx12は「コマンドリスト」にパイプラインの設定を作成し、1ターンの終わりに「実行」することで、そのパイプライの設定ししたがって描画します。
しかし、それら一つの「コマンドリスト」というのは、ルートシグネチャという基本設定に合わせて積み上げられます。(そのように理解しています)
ここでは、「画面クリア」と「プレゼント(バックバッファとフロントバッファの差し替え)」用のルートシグネチャの作成を行います。「空のルートシグネチャ」という表現になっていますが、ようは、テクスチャもサンプラーもコンスタントバッファも設定しない「ルートシグネチャ」という意味です。

    CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
    rootSignatureDesc.Init(0, nullptr, 0, nullptr, 
      D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

    ComPtr<ID3DBlob> signature;
    ComPtr<ID3DBlob> error;

    ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, 
        D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error),
        L"空のルートシグネチャのシリアライズに失敗しました",
        L"D3D12SerializeRootSignature()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    ThrowIfFailed(m_Device->CreateRootSignature(0, signature->GetBufferPointer(),
        signature->GetBufferSize(), IID_PPV_ARGS(&m_RootSignature)),
        L"ルートシグネチャの作成に失敗しました",
        L"Dev->GetDevice()->CreateRootSignature)",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

10、画面クリア用のコマンドリストの作成
「9、特殊なルートシグネチャの作成」で作成したルートシグネチャに合わせて実行されるコマンドリストは「画面クリア」と「プレゼント」です。ここでは「画面クリア」用のコマンドリストを作成します。

    ThrowIfFailed(
        m_Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, 
             m_CommandAllocator.Get(), nullptr, IID_PPV_ARGS(&m_CommandList)),
        L"コマンドリストの作成に失敗しました",
        L"m_Device->CreateCommandList()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    ThrowIfFailed(m_CommandList->Close(),
        L"コマンドリストのクローズに失敗しました",
        L"m_CommandList->Close()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

コマンドリストは、作成したら「クローズ」しておきます。実際のコマンドリストの積み上げは「ターンごとの描画」で行います。
11、プレゼント用のコマンドリスト
10同様、特殊なコマンドリストとして「プレゼント用のコマンドリスト」を作成します。

    ThrowIfFailed(
        m_Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, 
             m_CommandAllocator.Get(), nullptr, IID_PPV_ARGS(&m_PresentCommandList)),
        L"コマンドリストの作成に失敗しました",
        L"m_Device->CreateCommandList()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );
    ThrowIfFailed(m_PresentCommandList->Close(),
        L"コマンドリストのクローズに失敗しました",
        L"m_CommandList->Close()",
        L"Dx12DeviceResources::Impl::CreateDeviceResources()"
    );

このように、初期化処理を行っています。
「DirectX-Graphics-Samples」のサンプルを読んでいる人はわかると思いますが、このサンプルに記述されている「LoadPipeline関数」が全体の初期化で「LoadAssets関数」に当たるのが個別オブジェクト等の初期化と考えられますが、どうしてもサンプルですと一つのオブジェクトの表現を、Dx12の様々な手法で表示する方法のサンプルなので、BaseCrossは参考にしつつも、「全体側(つまりDeviceResouceクラス側)」と「各オブジェクト側(シーンに配置されるオブジェクト)」を、どういう風に役割を分担したらいいか、悩んだ末、ここまでを全体の処理、としました。

またこれらの処理を終えた後、「同期オブジェクトの作成と同期処理」を行ってます。
この処理はAppクラスから、デバイスを初期化した後、DeviceResource::AfterInitContents()を呼び出して行っています。
この処理を分けた理由は、当初、配置オブジェクトを初期化後に、「同期オブジェクトの作成と同期処理」を行うべきかと考えたため別関数にしましたが、いろいろテストしてみると、「11、プレゼント用のコマンドリスト」の後に行っても今のところ問題ないようですので将来的には、この処理もここに記述される可能性があります。
DeviceResource::AfterInitContents()関数は以下です。

12、同期オブジェクトの作成と同期処理

void DeviceResources::AfterInitContents() {
    //同期オブジェクトの作成と同期処理
    {

        ThrowIfFailed(pImpl->m_Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, 
            IID_PPV_ARGS(&pImpl->m_Fence)),
            L"フェンスの作成に失敗しました",
            L"m_Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_Fence))",
            L"Dx12DeviceResources::Impl::CreateDeviceResources()"
        );
        pImpl->m_FenceValue = 1;

        //フレーム同期のためのイベントハンドルの作成
        pImpl->m_FenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
        if (pImpl->m_FenceEvent == nullptr)
        {
            ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()),
                L"ラストエラーの取得に失敗しました",
                L"HRESULT_FROM_WIN32(GetLastError())",
                L"Dx12DeviceResources::Impl::CreateDeviceResources()"
            );
        }
        pImpl->WaitForPreviousFrame();
    }
}

pImpl->WaitForPreviousFrame()関数は、同期をとるために毎ターン呼ばれる関数で、以下のような内容です。

void DeviceResources::Impl::WaitForPreviousFrame(bool ExceptionActive) {
    const UINT64 fence = m_FenceValue;
    if (ExceptionActive) {
        ThrowIfFailed(m_CommandQueue->Signal(m_Fence.Get(), fence),
            L"コマンドキューのシグナルに失敗しました",
            L"m_CommandQueue->Signal(m_Fence.Get(), fence)",
            L"Dx12DeviceResources::Impl::WaitForPreviousFrame()"
        );
        m_FenceValue++;
        // Wait until the previous frame is finished.
        if (m_Fence->GetCompletedValue() < fence)
        {

            ThrowIfFailed(m_Fence->SetEventOnCompletion(fence, m_FenceEvent),
                L"イベントの設定に失敗しました",
                L"m_Fence->SetEventOnCompletion(fence, m_FenceEvent)",
                L"Dx12DeviceResources::Impl::WaitForPreviousFrame()"
            );
            WaitForSingleObject(m_FenceEvent, INFINITE);
        }
        m_FrameIndex = m_SwapChain->GetCurrentBackBufferIndex();
    }
    else {
        //例外を投げない。デストラクタ用
        m_CommandQueue->Signal(m_Fence.Get(), fence);
        m_FenceValue++;
        if (m_Fence->GetCompletedValue() < fence)
        {
            m_Fence->SetEventOnCompletion(fence, m_FenceEvent);
            WaitForSingleObject(m_FenceEvent, INFINITE);
        }
        m_FrameIndex = m_SwapChain->GetCurrentBackBufferIndex();
    }
}

このように、デストラクタから呼ばれる場合と、毎ターン呼ばれる場合とで処理を分けてます。
と言いますのはデストラクタから呼び出された場合、例外を投げるとあまりいいことはないからです。

以上、DeviceResourceクラスにおけるDx12の初期化処理を紹介しました。
これらの処理は、これで正しいのかどうかは、いろいろ実装してみないとわかりません。「DirectX-Graphics-Samples」のサンプルは「個別のパターンのオブジェクト」については非常によく整理されているのですが、実際にゲームには「スプライト」もあれば「3Dオブジェクト」「エフェクト」など様々なタイプが同居します。
そうした場合「DirectX-Graphics-Samples」のように「決めうち」で記述するわけにもいかないので、しばらく「実験と実装と」が続きそうです。

次回は、個別のオブジェクトの描画の仕組みについて述べます。

 

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP