TOP > PROGRAM

Direct3D 12 予習

1 はじめに…

先日,Windows 10 Technical Previewがインストールできるようになり,ようやくDirect3D 12が触れるようになりました。
触れるといっても正式版ではないのでAPIは大幅に変更される可能性もあります。そんなわけで,予習と題してプレビュー版を少し触ってDirect3D 12がどんなものかを勉強していくことにしてみます。
実装例

2 Direct3D 12とは

Direct3D 12は久しぶりのDirectXのメジャーバージョンアップになります。詳しい説明はビデオを見て頂くのが良いかと思います。

DirectX:Evolving Microsoft’s Graphics Platform
https://channel9.msdn.com/Blogs/DirectX-Developer-Blog/DirectX-Evolving-Microsoft-s-Graphics-Platform?wt.mc_id=player

Direct3D 12 API Preview
http://channel9.msdn.com/events/Build/2014/3-564

あとは,4gamerとかにわかりやすく説明されているので,そちらを参照してください。
Direct3D 12ですが,11と比べて何が良いかというとドローコールによるCPU側の負荷が減るというメリットがある反面,いままでドライバーがやっていた部分をアプリケーション側で制御する必要があり,APIがPS4やXB1のようにコンシューマーライクなAPIになるのでコーディングが若干面倒くさくなるといったデメリットがあります。
正式版が出ていない状況なので,今回は省略しDirect3D 12の正式版がリリースされたらその辺の話をしようと思います。 …というか自分自身がまだよくわかっとらんのですよ。
ちなみに,MSDNにドキュメントも上がっていますが若干APIが無いもの等あるので注意してください。

Direct3D 12 Graphics
https://msdn.microsoft.com/en-us/library/dn903821%28v=vs.85%29.aspx

3 初期化処理

まずは,レンダーターゲットをクリアするプログラムまでを書いてみます。
初期化部分の説明から始めていきます。今回のサンプルは本当に画面をクリアするだけなのでパイプラインステートオブジェクトやルートシグネチャは使わないので,初期化は下記のような流れになります。

初期化の流れ
図 1: 初期化の流れ

まずは,デバイスを作成します。これはDirect3D 11と同じですね。
つづいて,Direct3D 11のDeviceContextにあたる部分を生成していきます。Direct3D 11ではDeviceContextという形で色々隠ぺいしてくれていましたが,Direct3D 12では,CommandQueue, CommandAllocator, CommandListなどが対応するものとなります。
今回はDirect3D 11スタイルでやっていくので,これらはD3D12_COMMAND_LIST_TYPE_DIRECTを指定作っておきます。Bundleについて別の機会で説明しますので,ここでは特に触れずに,コマンドバッファに直済みしていくD3D12_COMMAND_LIST_TYPE_DIRECTを設定しておきます。
あと注意してほしいのは,CreateSwapChain()ですが,Direct3D 11のように第1引数にDeviceを設定するとエラーログが表示され正しくない設定だと怒られます(参考)。ひげねこさんによる説明だとCommnadQueueを渡せばよいということだそうです。
実際のコードは下記のようになります。

//-------------------------------------------------------------------------------------------------
//      D3D12の初期化処理です.
//-------------------------------------------------------------------------------------------------
bool App::InitD3D()
{
    HRESULT hr = S_OK;

    // ウィンドウ幅を取得.
    RECT rc;
    GetClientRect( m_hWnd, &rc );
    u32 w = rc.right - rc.left;
    u32 h = rc.bottom - rc.top;

    UINT flags = 0;

#if defined(DEBUG) || defined(_DEBUG)
    ID3D12Debug* pDebug;
    D3D12GetDebugInterface(IID_ID3D12Debug, (void**)&pDebug);
    if (pDebug)
    {
        pDebug->EnableDebugLayer();
        pDebug->Release();
    }
    flags |= DXGI_CREATE_FACTORY_DEBUG;
#endif

    hr = CreateDXGIFactory2(flags, IID_IDXGIFactory4, (void**)m_Factory.GetAddress());
    if (FAILED(hr))
    {
        ELOG("Error : CreateDXGIFactory() Failed.");
        return false;
    }

    hr = m_Factory->EnumAdapters(0, m_Adapter.GetAddress());
    if (FAILED(hr))
    {
        ELOG("Error : IDXGIFactory::EnumAdapters() Failed.");
        return false;
    }

    // デバイス生成.
    hr = D3D12CreateDevice(
        m_Adapter.GetPtr(),
        D3D_FEATURE_LEVEL_11_0,
        IID_ID3D12Device,
        (void**)m_Device.GetAddress() );

    // 生成チェック.
    if ( FAILED( hr ) )
    {
        // Warpアダプターで再トライ.
        m_Adapter.Reset();
        m_Device.Reset();

        hr = m_Factory->EnumWarpAdapter(IID_PPV_ARGS(m_Adapter.GetAddress()));
        if (FAILED(hr))
        {
            ELOG("Error : IDXGIFactory::EnumWarpAdapter() Failed.");
            return false;
        }

        // デバイス生成.
        hr = D3D12CreateDevice(
            m_Adapter.GetPtr(),
            D3D_FEATURE_LEVEL_11_0,
            IID_ID3D12Device,
            (void**)m_Device.GetAddress());
        if (FAILED(hr))
        {
            ELOG("Error: D3D12CreateDevice() Failed.");
            return false;
        }
    }

    // コマンドアロケータを生成.
    hr = m_Device->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT, 
        IID_ID3D12CommandAllocator,
        (void**)m_CmdAllocator.GetAddress() );
    if ( FAILED( hr ) )
    {
        ELOG( "Error : ID3D12Device::CreateCommandAllocator() Failed." );
        return false;
    }

    // コマンドキューを生成.
    {
       D3D12_COMMAND_QUEUE_DESC desc;
       ZeroMemory( &desc, sizeof(desc) );
       desc.Type        = D3D12_COMMAND_LIST_TYPE_DIRECT;
       desc.Priority    = 0;
       desc.Flags       = D3D12_COMMAND_QUEUE_FLAG_NONE;

       hr = m_Device->CreateCommandQueue( &desc, IID_ID3D12CommandQueue, (void**)m_CmdQueue.GetAddress() );
       if ( FAILED( hr ) )
       {
           ELOG( "Error : ID3D12Device::CreateCommandQueue() Failed." );
           return false;
       }
    }

    // スワップチェインを生成.
    {
        DXGI_SWAP_CHAIN_DESC desc;
        ZeroMemory( &desc, sizeof(desc) );
        desc.BufferCount                        = m_BufferCount;
        desc.BufferDesc.Format                  = m_SwapChainFormat;
        desc.BufferDesc.Width                   = w;
        desc.BufferDesc.Height                  = h;
        desc.BufferDesc.RefreshRate.Numerator   = 60;
        desc.BufferDesc.RefreshRate.Denominator = 1;
        desc.BufferUsage                        = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT;
        desc.OutputWindow                       = m_hWnd;
        desc.SampleDesc.Count                   = 1;
        desc.SampleDesc.Quality                 = 0;
        desc.Windowed                           = TRUE;
        desc.SwapEffect                         = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
        desc.Flags                              = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

        // アダプター単位の処理にマッチするのは m_Device ではなく m_CmdQueue なので,m_CmdQueue を第一引数として渡す.
        hr = m_Factory->CreateSwapChain( m_CmdQueue.GetPtr(), &desc, m_SwapChain.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : IDXGIFactory::CreateSwapChain() Failed." );
            return false;
        }
    }

    // デスクリプタヒープの生成.
    {
        D3D12_DESCRIPTOR_HEAP_DESC desc;
        ZeroMemory( &desc, sizeof(desc) );

        desc.NumDescriptors = 1;
        desc.Type           = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        desc.Flags          = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

        hr = m_Device->CreateDescriptorHeap( &desc, IID_ID3D12DescriptorHeap, (void**)m_DescriptorHeap.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateDescriptorHeap() Failed." );
            return false;
        }
    }

    // コマンドリストの生成.
    {
        hr = m_Device->CreateCommandList(
            1,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            m_CmdAllocator.GetPtr(),
            nullptr,
            IID_ID3D12GraphicsCommandList,
            (void**)m_CmdList.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateCommandList() Failed." );
            return false;
        }
    }

    // バックバッファからレンダーターゲットを生成.
    {
        hr = m_SwapChain->GetBuffer( 0, IID_ID3D12Resource, (void**)m_ColorTarget.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : IDXGISwapChain::GetBuffer() Failed." );
            return false;
        }

        m_ColorTargetHandle = m_DescriptorHeap->GetCPUDescriptorHandleForHeapStart();
        m_Device->CreateRenderTargetView( m_ColorTarget.GetPtr(), nullptr, m_ColorTargetHandle );
    }

    // フェンスの生成.
    {
        m_EventHandle = CreateEvent( 0, FALSE, FALSE, 0 );

        hr = m_Device->CreateFence( 0, D3D12_FENCE_FLAG_NONE, IID_ID3D12Fence, (void**)m_Fence.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateFence() Failed." );
            return false;
        }
    }

    // ビューポートの設定.
    {
        m_Viewport.TopLeftX = 0;
        m_Viewport.TopLeftY = 0;
        m_Viewport.Width    = FLOAT(w);
        m_Viewport.Height   = FLOAT(h);
        m_Viewport.MinDepth = 0.0f;
        m_Viewport.MaxDepth = 1.0f;
    }

    // 正常終了.
    return true;
}

かなり初期化コード長いですね(汗)。
知識のない初心者は間違いなく心折れますね。

4 描画処理

さて,いよいよ画面クリア処理です。
Direct3D 11のように初期化は長いですが,実際の描画コマンドをつくる処理は比較的に短いです。
Direct3D 12でも11と同じようにまずビューポートを設定しておきます。この時にD3D11と違い,リソースのバリアを張る必要があります。このあたり人に説明できるまでちゃんと理解していないです。間違いを恐れずにいうと,GPUの読み書き/書き込みするタイミングを考えずに描画コマンドを実行すると書き込み中にコマンドを実行してしまって,描画結果がおかしくなったりとかハードウェア的におかしくなったりとかするので,複数のアクセスを処理する必要があります。そのあたりをするのがこのリソースバリアなのかなぁと個人的には思っています。間違っている可能性もあるので,もし詳しい人がいらっしゃったらメール等で指摘していただけるとありがたいです。
そんなわけで,リソース関係をいじる場合にはリソースバリアを考える必要があります。今回のサンプルではレンダーターゲットのみなので,ビューポートを設定するタイミングと,レンダーターゲットをクリアする箇所は意識する必要があります。

//-------------------------------------------------------------------------------------------------
//      フレーム描画処理です.
//-------------------------------------------------------------------------------------------------
void App::OnFrameRender()
{
    // ビューポートを設定.
    m_CmdList->RSSetViewports( 1, &m_Viewport );
    SetResourceBarrier( m_CmdList.GetPtr(), m_ColorTarget.GetPtr(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);

    // カラーバッファをクリア.
    float clearColor[] = { 0.39f, 0.58f, 0.92f, 1.0f };
    m_CmdList->ClearRenderTargetView( m_ColorTargetHandle, clearColor, 0, nullptr );
    SetResourceBarrier( m_CmdList.GetPtr(), m_ColorTarget.GetPtr(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);

    // 画面に表示.
    Present( 0 );
}

SetResourceBarrier()メソッドはMSDNのサンプルを基に実装しました。サンプルは下記ページの下側にあります。
https://msdn.microsoft.com/en-us/library/dn859356%28v=vs.85%29.aspx

//-------------------------------------------------------------------------------------------------
//      リソースバリアの設定.
//-------------------------------------------------------------------------------------------------
void App::SetResourceBarrier
(
    ID3D12GraphicsCommandList* pCmdList,
    ID3D12Resource* pResource,
    D3D12_RESOURCE_STATES stateBefore,
    D3D12_RESOURCE_STATES stateAfter
)
{
    D3D12_RESOURCE_BARRIER desc = {};
    desc.Type                   = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
    desc.Transition.pResource   = pResource;
    desc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
    desc.Transition.StateBefore = stateBefore;
    desc.Transition.StateAfter  = stateAfter;

    pCmdList->ResourceBarrier( 1, &desc );
}

クリアの描画コマンドを積んだら,実際にコマンドリストを実行をしてGPUに処理してもらい,画面に表示するという作業を行う必要があります。
この処理を行っているのが下記のPresent()メソッドです。まず,ID3D12GraphicsCommandLst::Close()メソッドで描画コマンドの記録が終了したことを教えてあげます。つぎにID3D12CommandQueue::ExecuteCommandLists()で記録したしたコマンドリストを実行し,GPU側に転送します。転送自体はすぐに終わるのですが,GPU側ではすぐに処理が終わらない場合があるので,ID3D12Fence::Signal()でシグナル状態にして,SetEventOnCompletion()メソッドを使用してイベントが終了した時に与える数値を設定します。今回のサンプルでは,待ち状態が0で,完了時を1とするため,SetEventOnCompletion(1, m_EventHandle)を設定しています。CommandQueueの実行が終わった時に1をシグナルとするために,ID3D12CommandQueue::Signal()で1を設定します。あとは,WaitForSingleObject()メソッドで,完了を待機します。
完了したら,描画コマンドが実行済みでバックバッファが更新された状態になるので,IDXGISwapChain::Present()でバックバッファをフロントバッファに表示し,バッファを交換します。
これでコマンドリストを実行して,画面に表示まで終わったので現在コマンドリストに積まれている描画コマンドは不要になるので,次のフレームの描画を行うためにコマンドリストとコマンドを積むために使用したメモリをリセットしてやります。これを行っているのが,ID3D12CommandAllocator::Reset(), ID3D11GraphicsCommnadList::Reset()です。

//-------------------------------------------------------------------------------------------------
//      コマンドを実行して画面に表示します.
//-------------------------------------------------------------------------------------------------
void App::Present( u32 syncInterval )
{
    ID3D12CommandList* cmdList = m_CmdList.GetPtr();

    // コマンドリストへの記録を終了し,コマンド実行.
    m_CmdList->Close();
    m_CmdQueue->ExecuteCommandLists( 1, &cmdList );

    // コマンドの実行の終了を待機する
    m_Fence->Signal( 0 );
    m_Fence->SetEventOnCompletion( 1, m_EventHandle );
    m_CmdQueue->Signal( m_Fence.GetPtr(), 1 );
    WaitForSingleObject( m_EventHandle, INFINITE );

    // 画面に表示する.
    m_SwapChain->Present( syncInterval, 0 );

    // コマンドリストとコマンドアロケータをリセットする.
    m_CmdAllocator->Reset();
    m_CmdList->Reset( m_CmdAllocator.GetPtr(), nullptr );
}

5 おわりに…

今回は,一番簡単な画面クリアまでの処理を行いました。
たったこれだけの処理をするだけでも,結構大変だなぁと思いました。次回の予習ではポリゴン描画を取り扱う予定です。

6 サンプルコード

本ソースコードおよびプログラムはMIT Licenseに準じます。 プログラムの作成にはWindows 10 Technical Preview Build 10041 および Microsoft Visual Studio 2015 CTPを用いています。 D3D12_Simple