23.コマンドリストと描画処理

前回途中だった、「配置オブジェクトの初期化処理」の続きです。
今回は「コマンドリスト」についてです。
また、初期化処理の説明の後、実際の描画処理まで、一気に説明します。

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関数」で、以下がその実体です。

void NormalTextureBox::OnCreate() {
    m_CubeMesh = MeshResource::CreateCube(1.0f);
    wstring DataDir;
    App::GetApp()->GetDataDirectory(DataDir);
    wstring strTexture = DataDir + L"wall.jpg";
    m_WallTex = TextureResource::CreateTextureResource(strTexture);
    m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);
    m_DrawContext->CreateConstantBuffer(sizeof(m_ConstantBufferData));
    m_DrawContext->CreateDefault3DPipelineCmdList<VertexPositionNormalTexture, VSPNTStatic, PSPNTStatic>();
    ZeroMemory(&m_ConstantBufferData, sizeof(m_ConstantBufferData));
    //各行列をIdentityに初期化
    m_ConstantBufferData.World = Matrix4X4EX::Identity();
    m_ConstantBufferData.View = Matrix4X4EX::Identity();
    m_ConstantBufferData.Projection = Matrix4X4EX::Identity();
    //初期値更新
    m_DrawContext->UpdateConstantBuffer(reinterpret_cast<void**>(&m_ConstantBufferData), sizeof(m_ConstantBufferData));
    //テクスチャ設定
    m_DrawContext->SetTextureResource(m_WallTex);
}

今回は以下のブロックの説明になります。

    m_DrawContext->CreateDefault3DPipelineCmdList<VertexPositionNormalTexture, VSPNTStatic, PSPNTStatic>();

コマンドリストとは何か
「コマンドリスト」はパイプライン(グラフィックパイプライン)の設定、描画処理を「コマンド」としてカプセル化したものです。
つまり「この頂点シェーダを設定しなさい」「このピクセルシェーダを設定しなさい」「このコンスタントバッファを入力しなさい」などなど、描画処理には多くの設定や、GPUへのデータ入力が必要ですが、その処理を「コマンド」という単位で扱うもの、と考えられます。
「Dx12」ほ描画は、この「コマンドリスト」を各オブジェクト単位で積み上げていって、それらを「実行する」という考え方で、一気に描画処理をします。
ですので、「オブジェクトの初期化処理」の「コマンドリスト関連」は、毎ターンごとに積み上げられる「コマンドリスト」の下準備をすると考えられます。

「m_DrawContext->CreateDefault3DPipelineCmdList関数」の実体は以下のようになってます。この関数はテンプレート関数です。

template<typename Vertex, typename VS, typename PS>
void CreateDefault3DPipelineCmdList() {
    m_PipelineState = PipelineState::CreateDefault3D<Vertex, VS, PS>(GetRootSignature(), m_PineLineDesc);
    m_CommandList = CommandList::CreateDefault(m_PipelineState);
    CommandList::Close(m_CommandList);
}

テンプレート引数として「頂点データ型」「頂点シェーダ型」「ピクセルシェーダ型」を渡します。
この関数では渡された型に合わせて、「パイプラインステート」を作成します。また続いて、そのパイプライステートをもとに、「コマンドリスト」を作成します。
「パイプラインステート」と「コマンドリスト」はVSPSDrawContextクラスのメンバ変数として以下のように宣言されています。

ComPtr<ID3D12PipelineState> m_PipelineState;
ComPtr<ID3D12GraphicsCommandList> m_CommandList;

Implイディオムはあるのですが、この変数は通常のメンバ変数になっています。テンプレートを経由するのでイディオムは使いにくいので、このようになってますが、アクセサを付けることでイディオムに入れることも可能なので、将来はそうなるかもしれません。
「パイプラインステート」を作成する関数「PipelineState::CreateDefault3D」は、「namespace PipelineState」にあるユーティリティ関数です。
実体は以下のようになってます。

template<typename Vertex, typename VS, typename PS>
static inline ComPtr<ID3D12PipelineState> CreateDefault3D(const ComPtr<ID3D12RootSignature>& rootSignature, D3D12_GRAPHICS_PIPELINE_STATE_DESC& RetDesc) {

    CD3DX12_RASTERIZER_DESC rasterizerStateDesc(D3D12_DEFAULT);
    //裏面カリング
    rasterizerStateDesc.CullMode = D3D12_CULL_MODE_NONE;

    ZeroMemory(&RetDesc, sizeof(RetDesc));
    RetDesc.InputLayout = { Vertex::GetVertexElement(), Vertex::GetNumElements() };
    RetDesc.pRootSignature = rootSignature.Get();
    RetDesc.VS =
    {
        reinterpret_cast<UINT8*>(VS::GetPtr()->GetShaderComPtr()->GetBufferPointer()),
        VS::GetPtr()->GetShaderComPtr()->GetBufferSize()
    };
    RetDesc.PS =
    {
        reinterpret_cast<UINT8*>(PS::GetPtr()->GetShaderComPtr()->GetBufferPointer()),
        PS::GetPtr()->GetShaderComPtr()->GetBufferSize()
    };
    RetDesc.RasterizerState = rasterizerStateDesc;
    RetDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    RetDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    RetDesc.SampleMask = UINT_MAX;
    RetDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    RetDesc.NumRenderTargets = 1;
    RetDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    RetDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
    RetDesc.SampleDesc.Count = 1;
    return CreateDirect(RetDesc);
}

コードを見てもらえばわかるように、「D3D12_GRAPHICS_PIPELINE_STATE_DESC構造体」の中身を、このオブジェクトにあうように初期化して、それをもとに、「CreateDirect(RetDesc);呼び出し」で「ComPtr<ID3D12PipelineState>型の変数」を作成します。
「D3D12_GRAPHICS_PIPELINE_STATE_DESC構造体」は非常に重要なので、あとで調べたり、あるいは一部のみ変更して「パイプラインステート」を再作成することも可能なように「VSPSDrawContext」のメンバとして保存しておきます。なので上記関数の最後の引数「RetDesc」は、その参照に書き込まれることを前提としています。

「パイプラインステート」を作成したら、続いて「コマンドリスト」です。以下がコマンドリストを作成する実体です。

static inline  ComPtr<ID3D12GraphicsCommandList> CreateDefault(const ComPtr<ID3D12PipelineState>& pipelineState) {    //デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    ComPtr<ID3D12GraphicsCommandList> Ret;
    ThrowIfFailed(Dev->GetDevice()->CreateCommandList(
        0,
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        Dev->GetCommandAllocator().Get(),
        pipelineState.Get(),
        IID_PPV_ARGS(&Ret)),
        L"コマンドリストの作成に失敗しました",
        L"Dev->GetDevice()->CreateCommandList()",
        L"CommandList::CreateDefault()"
    );
    return Ret;
}

このように「コマンドリスト」を作成するには「パイプラインステート」が必要になります。ここで作成している「コマンドリスト」は「コマンドリストの箱」のようなものです。実際に描画処理に使用したりはしていません。(まだ初期化処理なので)。

このようにして作成した「コマンドリスト」は、「CreateDefault3DPipelineCmdList関数」にあるように、いったんクローズしておきます。

template<typename Vertex, typename VS, typename PS>
void CreateDefault3DPipelineCmdList() {
    //中略
    CommandList::Close(m_CommandList);
}

ここまでの実装で「20.オブジェクト側デスクプリタヒープの作成」に記述がある「実行の流れ図」の「各オブジェクトの初期化」が終わったことになります。
以下に同じ図を示します。

2016081401

コマンドリストによる描画
ここからは「ターンごと」の「描画処理」ということになります。「ターン」はどこで作り出しているかというと、「BaseCrossDx12」プロジェクトの「WinMain.cpp」です。その中の「MainLoop関数」が実体になります。
「MainLoop関数」には「while文」で作成されている「メッセージループ」がありますが、この中の

    App::GetApp()->UpdateDraw(1);

という呼び出しが、「更新と描画」になります。実体は以下の内容です。

void App::UpdateDraw(unsigned int SyncInterval) {
    if (!m_SceneInterface) {
        //シーンがが無効なら
        throw BaseException(
            L"シーンがありません",
            L"if(!m_SceneInterface)",
            L"App::UpdateDraw()"
        );
    }

    // シーン オブジェクトを更新します。
    m_InputDevice.ResetControlerState();
    m_Timer.Tick([&]()
    {
        m_SceneInterface->OnUpdate();
    });
    // 初回更新前にレンダリングは行わない。
    if (GetFrameCount() == 0)
    {
        return;
    }
    m_SceneInterface->OnDraw();
    // バックバッファからフロントバッファに転送
    m_DeviceResources->Present(SyncInterval,0);
}

ここでは、シーン(親クラス)である「m_SceneInterface」を介して、「OnUpdate関数」「OnDraw関数」を呼び出しています。
「OnUpdate関数」はこのサンプルにおいては、立方体のワールド行列を追加回転させる処理をしています。今は「Dx12」の説明なので、「OnUpdate関数」のほうはあまり重要ではないので触れません。
問題は「OnDraw関数」呼び出しと「m_DeviceResources->Present(SyncInterval,0)」呼び出しです。後者は「プレゼント」になります。(以前説明しました)。

シーンに対して「OnDraw関数」呼び出しを行うと、「BaseCrossDx12」プロジェクトの「Scene.cpp」にある「Scene::OnDraw関数」が実行されます。以下が実体です。

void Scene::OnDraw() {
    //描画デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    Dev->ClearDefultViews(Color4(0, 0, 0, 1.0));
    //デフォルト描画の開始
    Dev->StartDefultDraw();
    m_NormalTextureBox->OnDraw();
    //デフォルト描画の終了
    Dev->EndDefultDraw();
}

ここでは、アプリケーションクラスからデバイスリソースを取り出し、画面を黒で塗りつぶしています。
そしてその後、「描画開始」の関数を呼び出しています。以下が実体です。「DeviceResources::StartDefultDraw関数」です、

//--------------------------------------------------------------------------------------
/*!
@brief 通常描画の開始(未定義)
@return なし
*/
//--------------------------------------------------------------------------------------
virtual void StartDefultDraw() {}

これを見て、「え」と思った人もいると思います。そうです。何もこの関数では行っていません。
この呼び出しは「Dx11版」との、設計の互換をとるために呼び出しているので、「Dx12版」は何も行ってないのです。「Dx11版」はここにレンダリングターゲットの準備とか、そういったものが入ります。
同様、「Dev->EndDefultDraw関数」の何も行ってません。
なぜなにも必要がないかというと、Dx12版はここまで記述してきたように、GPUへのインターフェイスをかなり自作しているのです。「Dx11版」はゲームエンジン(DirectX11側)でいろいろやってくれるのですが、「Dx12版」はやってくれません。
逆に、これまで準備してきた、「デスクプリタテーブル」とか「ルートシグネチャ」を使って描画するようになります。(しかしこの考え方も将来変わる可能性があります)

ということで、一番重要なのは「m_NormalTextureBox->OnDraw()」だということがわかります。
以下がその実体です。

void NormalTextureBox::OnDraw() {
    //行列の定義
    //ワールド行列の決定
    m_ConstantBufferData.World.AffineTransformation(
        m_Scale,            //スケーリング
        Vector3(0, 0, 0),       //回転の中心(重心)
        m_Qt,               //回転角度
        m_Pos               //位置
    );
    //転置する
    m_ConstantBufferData.World.Transpose();
    //ビュー行列の決定
    m_ConstantBufferData.View.LookAtLH(Vector3(0, 2.0, -5.0f), Vector3(0, 0, 0), Vector3(0, 1.0f, 0));
    //転置する
    m_ConstantBufferData.View.Transpose();
    //射影行列の決定
    float w = static_cast<float>(App::GetApp()->GetGameWidth());
    float h = static_cast<float>(App::GetApp()->GetGameHeight());
    m_ConstantBufferData.Projection.PerspectiveFovLH(XM_PIDIV4, w / h, 1.0f, 100.0f);
    //転置する
    m_ConstantBufferData.Projection.Transpose();
    //ライティング
    m_ConstantBufferData.LightDir = Vector4(0.5f, -1.0f, 0.5f, 1.0f);
    //エミッシブ
    m_ConstantBufferData.Emissive = Color4(0, 0, 0, 0);
    //ディフューズ
    m_ConstantBufferData.Diffuse = Color4(1.0f, 1.0f, 1.0f, 1.0f);
    //更新
    m_DrawContext->UpdateConstantBuffer(&m_ConstantBufferData, sizeof(m_ConstantBufferData));
    //描画
    m_DrawContext->DrawIndexed<VertexPositionNormalTexture>(m_CubeMesh);

}

こうしてソースを見てみると、ほとんどの処理が「コンスタントバッファ」のインスタンス「m_ConstantBufferData」に値を代入している処理なのがわかります。その内容は「OnUpdate」によって確定した「ワールド行列」と「カメラ」を抽象化した「ビュー行列」「射影行列の設定」です(ほかに色やライティングも入力してます)
「コンスタントバッファ」に設定が終わったら

    //更新
    m_DrawContext->UpdateConstantBuffer(&m_ConstantBufferData, sizeof(m_ConstantBufferData));

として「コンスタントバッファの更新」を行います。これは、クリエイト時にマップされたコンスタントバッファのアップロードヒープへ、cpp側からコピーしてます。以下、「m_DrawContext->UpdateConstantBuffer」の実体です。

void VSPSDrawContext::UpdateConstantBuffer(void* SrcBuffer, UINT BufferSize) {
    pImpl->CbvSrvUavDescriptorHeapChk();
    if (!pImpl->m_pConstantBuffer || !pImpl->m_ConstantBufferUploadHeap.Get()) {
        ThrowBaseException(
            L"コンスタントバッファかコンスタントバッファアップロードヒープが初期化されていません",
            L"if (!pImpl->m_pConstantBuffer || !pImpl->m_ConstantBufferUploadHeap.Get())",
            L"SrvSmpCbvDeviceContext::UpdateConstantBuffer()"
        );
    }
    memcpy(pImpl->m_pConstantBuffer, SrcBuffer, BufferSize);
}

ここでは、「pImpl->m_pConstantBuffer」へ「SrcBuffer」から「BufferSize」だけコピーしてます。
「pImpl->m_pConstantBuffer」はクリエイト時に設定したように、コンスタントバッファのアップロードヒープにマップされたポインタです。ですからこのポインタを介してデータをコピーすれば、そのまま「コンスタントバッファのアップロードヒープ」を変更できます。

少し本題からそれますが、ちょっとこの処理は不安を感じています。コンスタントバッファアップロードヒープをクリエイト時にマップしたということは、ゲームが動いている間、マップされていることになります。通常こういったGPUのリソースを使う場合、使用時にマップして、使い終わったらアンマップするのですが、「DirectX-Graphics-Samples」においても「コンスタントバッファ」と「コンスタントバッファアップロードヒープ」間は、アンマップは行われてないようです。ですので、今後、この処理がもとで何か不具合が起こるかもしれないと考えています。
このように、自分としてはちょっと問題がありそうだけど、公式のサンプル(「DirectX-Graphics-Samples」はまさに公式です)などで、自分の納得しない記述があった場合、少し頭に入れておくのがいいでしょう。
もちろん、このまま何の問題もなく動くのならいいのですが、例えばオブジェクトを100個に増やしたとたんに落ちるとか、別のオブジェクトのコンスタントバッファが影響してるとか、そういう現象が起こったら、このあたりのコードを見直すといいと思います。
話を戻します。
このようにして「コンスタントバッファの更新」が行われて、最後に

void NormalTextureBox::OnDraw() {
    //中略

    //描画
    m_DrawContext->DrawIndexed<VertexPositionNormalTexture>(m_CubeMesh);

}

が行われます。その実体は以下です。

template<typename Vertex>
void DrawIndexed(const shared_ptr<MeshResource>& Mesh) {
    ResetPipeLine();
    Mesh->UpdateResources<Vertex>(m_CommandList);
    UpdateShaderResource();
    DrawIndexedBase(Mesh);
}

この関数には「メッシュ」を渡します。「メッシュ」は「MeshResource型」で「TextureResource」と同じように「BaseResource」の派生クラスとして実装されてます。
このクラスは、前に「プリミティブメッシュ」のところで説明したように、頂点バッファをカプセル化しています。
まず

    ResetPipeLine();

では、

    //コマンドリストのリセット
    CommandList::Reset(m_PipelineState, m_CommandList);

が実行されます。初期化時に作成したパイプラインステートとコマンドリストを初期化時に戻すわけです。
続いて、

    Mesh->UpdateResources<Vertex>(m_CommandList);

では、もしメッシュの内容が変更された場合、メッシュのアップロードヒープの内容を変更します。内容更新にはコマンドリストが必要なので渡しています。
続く

    UpdateShaderResource();

はテクスチャの内容が変更されたときに、テクスチャのアップロードヒープの内容を更新しなければいけないのでそれを行っています。
この関数は、追いかけていくと「TextureResource型」の関数を呼んでいるのがわかります。
そして最後に

    DrawIndexedBase(Mesh);

を呼び出します。
この処理は以下です。

void VSPSDrawContext::DrawIndexedBase(const shared_ptr<MeshResource>& Mesh) {
    m_CommandList->SetGraphicsRootSignature(pImpl->m_RootSignature.Get());
    if (pImpl->m_SamplerDescriptorHeap.Get()) {
        ID3D12DescriptorHeap* ppHeaps[] = { pImpl->m_CbvSrvUavDescriptorHeap.Get(), pImpl->m_SamplerDescriptorHeap.Get() };
        m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
    }
    else {
        ID3D12DescriptorHeap* ppHeaps[] = { pImpl->m_CbvSrvUavDescriptorHeap.Get() };
        m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
    }
    for (size_t i = 0; i < pImpl->m_GPUDescriptorHandleVec.size(); i++) {
        m_CommandList->SetGraphicsRootDescriptorTable(i, pImpl->m_GPUDescriptorHandleVec[i]);
    }
    auto Dev = App::GetApp()->GetDeviceResources();
    m_CommandList->RSSetViewports(1, &Dev->GetViewport());
    m_CommandList->RSSetScissorRects(1, &Dev->GetScissorRect());

    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
        Dev->GetRtvHeap()->GetCPUDescriptorHandleForHeapStart(),
        Dev->GetFrameIndex(),
        Dev->GetRtvDescriptorSize());
    CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(
        Dev->GetDsvHeap()->GetCPUDescriptorHandleForHeapStart()
    );
    m_CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);

    m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_CommandList->IASetIndexBuffer(&Mesh->GetIndexBufferView());
    m_CommandList->IASetVertexBuffers(0, 1, &Mesh->GetVertexBufferView());
    m_CommandList->DrawIndexedInstanced(Mesh->GetNumIndicis(), 1, 0, 0, 0);

    //コマンドリストのクローズ
    CommandList::Close(m_CommandList);
    //デバイスにコマンドリストを送る
    Dev->InsertDrawCommandLists(m_CommandList.Get());

}

様々な処理を行ってますが、よく見るとコマンドリストに、今まで初期化時に作成した「ルートシグネチャ」「デスクプリタヒープ」などに加え「ビューポート」「レンダリングターゲット」「デプスステンシル」などなど、ようは、初期化時に作成した様々なオブジェクトをコマンドリストに登録しているのがわかります。

そして設定が終わったら

    m_CommandList->DrawIndexedInstanced(Mesh->GetNumIndicis(), 1, 0, 0, 0);

で描画をします。しかし実はこの段階では描画は行われてません。

    //コマンドリストのクローズ
    CommandList::Close(m_CommandList);
    //デバイスにコマンドリストを送る
    Dev->InsertDrawCommandLists(m_CommandList.Get());
}

でコマンドリストをクローズし、「Dev->InsertDrawCommandLists関数」にコマンドリストを渡してます。
この関数は「DeviceResourcesクラス」のメンバ関数で、様々なオブジェクトで作成されるコマンドリストをコレクションするクラスです。
コレクションはどのようにするかというと、単純にコマンドロイスとの配列にpush_backします。

void DeviceResources::InsertDrawCommandLists(ID3D12CommandList* Tgt) {
    pImpl->m_DrawCommandLists.push_back(Tgt);
}

こんな感じです。「pImpl->m_DrawCommandLists」は

    //コマンドリスト実行用の配列
    vector<ID3D12CommandList*> m_DrawCommandLists;

このような配列で、ここにコマンドリストのポインタを毎ターンごとに積み上げていきます。

では実際に積み上げたコマンドリストはどうするかというと、「プレゼント」の処理で実行されます。
以下は「DeviceResources::Present関数」です。

void DeviceResources::Present(unsigned int SyncInterval, unsigned int  Flags) {

    ThrowIfFailed(pImpl->m_PresentCommandList->Reset(pImpl->m_CommandAllocator.Get(), pImpl->m_PipelineState.Get()),
        L"コマンドリストのリセットに失敗しました",
        L"pImpl->m_CommandList->Reset()",
        L"Dx12DeviceResources::ClearDefultViews()"
    );
    //プレゼント用のバリアを張る
    pImpl->m_PresentCommandList->ResourceBarrier(1,
        &CD3DX12_RESOURCE_BARRIER::Transition(pImpl->m_RenderTargets[pImpl->m_FrameIndex].Get(),
            D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
    ThrowIfFailed(pImpl->m_PresentCommandList->Close(),
        L"コマンドリストのクローズに失敗しました",
        L"m_CommandList->Close()",
        L"Dx12DeviceResources::ClearDefultViews()"
    );
    InsertDrawCommandLists(pImpl->m_PresentCommandList.Get());
    // Execute the command list.
    pImpl->m_CommandQueue->ExecuteCommandLists(pImpl->m_DrawCommandLists.size(), &pImpl->m_DrawCommandLists[0]);
    // Present the frame.
    ThrowIfFailed(pImpl->m_SwapChain->Present(SyncInterval, Flags),
        L"スワップチェーンのプレゼントに失敗しました",
        L"pImpl->m_SwapChain->Present(1, 0)",
        L"Dx12DeviceResources::OnDraw()"
    );
    pImpl->WaitForPreviousFrame();
    pImpl->m_DrawCommandLists.clear();
}

ここでは、最初にプレゼント時の「バリアを張る」というコマンドリストを作成して、コマンドリストのコレクションに追加します。
そして、

    // Execute the command list.
    pImpl->m_CommandQueue->ExecuteCommandLists(pImpl->m_DrawCommandLists.size(), &pImpl->m_DrawCommandLists[0]);
    // Present the frame.
    ThrowIfFailed(pImpl->m_SwapChain->Present(SyncInterval, Flags),
        L"スワップチェーンのプレゼントに失敗しました",
        L"pImpl->m_SwapChain->Present(1, 0)",
        L"Dx12DeviceResources::OnDraw()"
    );

がまさに「コマンドリストの実行」そして「バックバッファとフロントバッファの差し替え(プレゼント)」です。ここでやっと「コマンドキュー」も出てきました。
表示が終わったら、1つ前のフレームの実行を待って

    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関連」をまとめてみたいと思います。

カテゴリー

ピックアップ記事

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