Cross概要ゲームダウンロードお問い合わせホーム実験と実装と23.コマンドリストと描画処理2016/8/16実験と実装と23.コマンドリストと描画処理TweetShare前回途中だった、「配置オブジェクトの初期化処理」の続きです。今回は「コマンドリスト」についてです。初期化処理の説明の後、実際の描画処理まで、一気に説明します。Dx12に関する情報は、マイクロソフト社の「DirectX-Graphics-Samples」が主な情報源となります。マイクロソフト社の「DirectX-Graphics-Samples」のGitHubサイトは以下になります。https://github.com/Microsoft/DirectX-Graphics-Samplesぜひ、興味ある方はダウンロードしてみてDx12研究を始めるといいと思います。この記事は、コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」から、コミット「weak_ptrからshared_ptr取得方法の修正」の間の作業です。GitHubサイトhttps://github.com/WiZFramework/BaseCrossを参照して下さい。その間にいくつかのコミットがありますが、後ろのコミットを開いていただいてそれと合わせ読んでいただくとわかりやすいと思います。配置されるオブジェクトは、「BaseCrossDx12」プロジェクトの「Character.h/cpp」に記述があります。「NormalTextureBoxクラス」です。このオブジェクトの初期化処理は、「NormalTextureBox::OnCreate関数」で、以下がその実体です。m_DrawContext->SetTextureResource(m_WallTex);}今回は以下のブロックの説明になります。PSPNTStatic>();コマンドリストとは何か「コマンドリスト」はパイプライン(グラフィックパイプライン)の設定、描画処理を「コマンド」としてカプセル化したものです。「この頂点シェーダを設定しなさい」「このピクセルシェーダを設定しなさい」「このコンスタントバッファを入力しなさい」などなど、描画処理には多くの設定や、GPUへのデータ入力が必要ですが、その処理を「コマンド」という単位で扱うもの、と考えられます。「Dx12」ほ描画は、この「コマンドリスト」を各オブジェクト単位で積み上げていって、それらを「実行する」という考え方で、一気に描画処理をします。「オブジェクトの初期化処理」の「コマンドリスト関連」は、毎ターンごとに積み上げられる「コマンドリスト」の下準備をすると考えられます。「m_DrawContext->CreateDefault3DPipelineCmdList関数」の実体は以下のようになってます。この関数はテンプレート関数です。CommandList::Close(m_CommandList);}テンプレート引数として「頂点データ型」「頂点シェーダ型」「ピクセルシェーダ型」を渡します。この関数では渡された型に合わせて、「パイプラインステート」を作成します。続いて、そのパイプライステートをもとに、「コマンドリスト」を作成します。「パイプラインステート」と「コマンドリスト」はVSPSDrawContextクラスのメンバ変数として以下のように宣言されています。m_CommandList;Implイディオムはあるのですが、この変数は通常のメンバ変数になっています。テンプレートを経由するのでイディオムは使いにくいので、このようになってますが、アクセサを付けることでイディオムに入れることも可能なので、将来はそうなるかもしれません。PipelineState」にあるユーティリティ関数です。実体は以下のようになってます。CreateDirect(RetDesc);}コードを見てもらえばわかるように、「D3D12_GRAPHICS_PIPELINE_STATE_DESC構造体」の中身を、このオブジェクトにあうように初期化して、それをもとに、「CreateDirect(RetDesc);呼び出し」で「ComPtr<ID3D12PipelineState>型の変数」を作成します。「D3D12_GRAPHICS_PIPELINE_STATE_DESC構造体」は非常に重要なので、あとで調べたり、あるいは一部のみ変更して「パイプラインステート」を再作成することも可能なように「VSPSDrawContext」のメンバとして保存しておきます。上記関数の最後の引数「RetDesc」は、その参照に書き込まれることを前提としています。「パイプラインステート」を作成したら、続いて「コマンドリスト」です。以下がコマンドリストを作成する実体です。Ret;}このように「コマンドリスト」を作成するには「パイプラインステート」が必要になります。ここで作成している「コマンドリスト」は「コマンドリストの箱」のようなものです。実際に描画処理に使用したりはしていません。(まだ初期化処理なので)。このようにして作成した「コマンドリスト」は、「CreateDefault3DPipelineCmdList関数」にあるように、いったんクローズしておきます。CommandList::Close(m_CommandList);}ここまでの実装で「20.オブジェクト側デスクプリタヒープの作成」に記述がある「実行の流れ図」の「各オブジェクトの初期化」が終わったことになります。以下に同じ図を示します。コマンドリストによる描画ここからは「ターンごと」の「描画処理」ということになります。「ターン」はどこで作り出しているかというと、「BaseCrossDx12」プロジェクトの「WinMain.cpp」です。その中の「MainLoop関数」が実体になります。App::GetApp()->UpdateDraw(1);という呼び出しが、「更新と描画」になります。実体は以下の内容です。オブジェクトを更新します。初回更新前にレンダリングは行わない。m_DeviceResources->Present(SyncInterval,0);}ここでは、シーン(親クラス)である「m_SceneInterface」を介して、「OnUpdate関数」「OnDraw関数」を呼び出しています。「OnUpdate関数」はこのサンプルにおいては、立方体のワールド行列を追加回転させる処理をしています。今は「Dx12」の説明なので、「OnUpdate関数」のほうはあまり重要ではないので触れません。問題は「OnDraw関数」呼び出しと「m_DeviceResources->Present(SyncInterval,0)」呼び出しです。後者は「プレゼント」になります。(以前説明しました)。シーンに対して「OnDraw関数」呼び出しを行うと、「BaseCrossDx12」プロジェクトの「Scene.cpp」にある「Scene::OnDraw関数」が実行されます。Dev->EndDefultDraw();}ここでは、アプリケーションクラスからデバイスリソースを取り出し、画面を黒で塗りつぶしています。その後、「描画開始」の関数を呼び出しています。{}これを見て、「え」と思った人もいると思います。何もこの関数では行っていません。この呼び出しは「Dx11版」との、設計の互換をとるために呼び出しているので、「Dx12版」は何も行ってないのです。「Dx11版」はここにレンダリングターゲットの準備とか、そういったものが入ります。同様、「Dev->EndDefultDraw関数」の何も行ってません。なにも必要がないかというと、Dx12版はここまで記述してきたように、GPUへのインターフェイスをかなり自作しているのです。「Dx11版」はゲームエンジン(DirectX11側)でいろいろやってくれるのですが、「Dx12版」はやってくれません。逆に、これまで準備してきた、「デスクプリタテーブル」とか「ルートシグネチャ」を使って描画するようになります。(しかしこの考え方も将来変わる可能性があります)ということで、一番重要なのは「m_NormalTextureBox->OnDraw()」だということがわかります。m_DrawContext->DrawIndexed<VertexPositionNormalTexture>(m_CubeMesh);}こうしてソースを見てみると、ほとんどの処理が「コンスタントバッファ」のインスタンス「m_ConstantBufferData」に値を代入している処理なのがわかります。sizeof(m_ConstantBufferData));として「コンスタントバッファの更新」を行います。これは、クリエイト時にマップされたコンスタントバッファのアップロードヒープへ、cpp側からコピーしてます。以下、「m_DrawContext->UpdateConstantBuffer」の実体です。BufferSize);}ここでは、「pImpl->m_pConstantBuffer」へ「SrcBuffer」から「BufferSize」だけコピーしてます。「pImpl->m_pConstantBuffer」はクリエイト時に設定したように、コンスタントバッファのアップロードヒープにマップされたポインタです。このポインタを介してデータをコピーすれば、そのまま「コンスタントバッファのアップロードヒープ」を変更できます。本題からそれますが、ちょっとこの処理は不安を感じています。コンスタントバッファアップロードヒープをクリエイト時にマップしたということは、ゲームが動いている間、マップされていることになります。通常こういったGPUのリソースを使う場合、使用時にマップして、使い終わったらアンマップするのですが、「DirectX-Graphics-Samples」においても「コンスタントバッファ」と「コンスタントバッファアップロードヒープ」間は、アンマップは行われてないようです。今後、この処理がもとで何か不具合が起こるかもしれないと考えています。このように、自分としてはちょっと問題がありそうだけど、公式のサンプル(「DirectX-Graphics-Samples」はまさに公式です)などで、自分の納得しない記述があった場合、少し頭に入れておくのがいいでしょう。このまま何の問題もなく動くのならいいのですが、例えばオブジェクトを100個に増やしたとたんに落ちるとか、別のオブジェクトのコンスタントバッファが影響してるとか、そういう現象が起こったら、このあたりのコードを見直すといいと思います。m_DrawContext->DrawIndexed<VertexPositionNormalTexture>(m_CubeMesh);}が行われます。DrawIndexedBase(Mesh);}この関数には「メッシュ」を渡します。「メッシュ」は「MeshResource型」で「TextureResource」と同じように「BaseResource」の派生クラスとして実装されてます。このクラスは、前に「プリミティブメッシュ」のところで説明したように、頂点バッファをカプセル化しています。m_CommandList);が実行されます。初期化時に作成したパイプラインステートとコマンドリストを初期化時に戻すわけです。Mesh->UpdateResources<Vertex>(m_CommandList);では、もしメッシュの内容が変更された場合、メッシュのアップロードヒープの内容を変更します。内容更新にはコマンドリストが必要なので渡しています。UpdateShaderResource();はテクスチャの内容が変更されたときに、テクスチャのアップロードヒープの内容を更新しなければいけないのでそれを行っています。この関数は、追いかけていくと「TextureResource型」の関数を呼んでいるのがわかります。DrawIndexedBase(Mesh);を呼び出します。Dev->InsertDrawCommandLists(m_CommandList.Get());}様々な処理を行ってますが、よく見るとコマンドリストに、今まで初期化時に作成した「ルートシグネチャ」「デスクプリタヒープ」などに加え「ビューポート」「レンダリングターゲット」「デプスステンシル」などなど、ようは、初期化時に作成した様々なオブジェクトをコマンドリストに登録しているのがわかります。0);で描画をします。実はこの段階では描画は行われてません。Dev->InsertDrawCommandLists(m_CommandList.Get());}でコマンドリストをクローズし、「Dev->InsertDrawCommandLists関数」にコマンドリストを渡してます。この関数は「DeviceResourcesクラス」のメンバ関数で、様々なオブジェクトで作成されるコマンドリストをコレクションするクラスです。コレクションはどのようにするかというと、単純にコマンドロイスとの配列にpush_backします。pImpl->m_DrawCommandLists.push_back(Tgt);}こんな感じです。m_DrawCommandLists;このような配列で、ここにコマンドリストのポインタを毎ターンごとに積み上げていきます。実際に積み上げたコマンドリストはどうするかというと、「プレゼント」の処理で実行されます。以下は「DeviceResources::Present関数」です。pImpl->m_DrawCommandLists.clear();}ここでは、最初にプレゼント時の「バリアを張る」というコマンドリストを作成して、コマンドリストのコレクションに追加します。);がまさに「コマンドリストの実行」そして「バックバッファとフロントバッファの差し替え(プレゼント)」です。ここでやっと「コマンドキュー」も出てきました。pImpl->m_DrawCommandLists.clear();で積み上げたコマンドリストの配列をクリアします。ここで、vector(配列)の仕様について一言。vector(配列)は、動的に内容が増やすことができます。100個のオブジェクトがあれば、おおむね100個のコマンドリストがあります。これをそれぞれのOnDrawからコマンドリストが積み上げられます。プレゼント時は、100個の配列になっています。それで描画したあと「クリア」したのでは、また、配列を0からくみ上げていくのでは時間がかかるのでは、と思うかもしれません。「vectorのような動的配列」に拒絶感を持つ人に多いと思います。vectorというのは、内部に「capacity(制限値)」というのを持っていて、それがメモリを占有している大きさと考えられます。この「capacity」はpush_backによってデータが追加されたときに、多くの実装では「1,2,4,8,16個」というように、倍々で容量を増やしていきます。「100個の配列」になった段階では、「capacity」は128になってると考えられます。その「capacity」は自動的に解放されません。clear()でも解放されません。1回広げた配列は、最後までそのサイズを維持するのです。vectorを操作するユーザーは「size()」を超えたアクセスはできません。このような仕様ですから「pImpl->m_DrawCommandLists.clear()」を実行しても、「capacity」は減らない、ということがわかります。いうことは、「pImpl->m_DrawCommandLists.clear()」を実行して。次のターンでやはり100回のpush_backが行われても、メモリを増やすなどの負荷は発生しない(つまり固定長配列と同じ)動きになります。ここまで、「Dx12の初期化」にはじまり、様々な「Dx12リソース」の作成など、とにかく長く果てしない戦いを説明してきました。これまでで一応、「回転する立方体」の表示はできるようになりました。ここまで根気よく読んでくださった皆様に感謝します。次回は、これまでだらだらと綴ってきた「Dx12関連」をまとめてみたいと思います。実験と実装とピックアップ記事2016/9/2244.プレイヤーの反発と追いかけるオブジェクト今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto...2016/9/2043.プレイヤーと固定ボックス前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。