DirectX12を勉強してみる 第2回目 画面のクリア
はじめに
前回はウインドウ表示までやりました。今回は、ウインドウの画面をDirectX12を使って黒色にクリアしていきます。
注意: 今回の記事は長すぎるので、説明を簡単に済ませている場所があります。ご了承ください。
開発環境
エディタ: Visual Studio 2022 Pro
言語: C++
クラスの作成
D3D12App.cpp
、D3D12App.h
を作成しました。
D3D12App.h
#pragma once
#include <d3d12.h>
#include <dxgi1_6.h>
#include "Direct3DHelper/d3dx12.h"
#include <wrl.h>
#include <string>
#include <vector>
#include <iostream>
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
using Microsoft::WRL::ComPtr;
class D3D12App
{
public:
/// <summary>
/// コンストラクタ
/// </summary>
D3D12App(unsigned int width, unsigned int height);
/// <summary>ウィンドウ幅を取得</summary>
unsigned int GetWindowWidth() const { return window_width; }
/// <summary>ウィンドウ高さを取得</summary>
unsigned int GetWindowHeight() const { return window_height; }
/// <summary>初期化処理</summary>
void OnInit(HWND hWnd);
/// <summary>描画処理</summary>
void OnRender();
/// <summary>終了処理(リソース解放など)</summary>
void OnDestroy();
private:
// --- ウィンドウ情報 ---
unsigned int window_width; // クライアント領域の幅
unsigned int window_height; // クライアント領域の高さ
// --- 定数 ---
static const int FRAME_COUNT = 2; // バックバッファ数
// --- GPU 同期制御用 ---
int _fenceValue = 0; // フェンス値
HANDLE _fenceEvent = nullptr;// フェンス用イベントハンドル
// --- DirectX12 の主要オブジェクト ---
ComPtr<ID3D12Device> _device; // D3D12 デバイス
ComPtr<IDXGIFactory7> _dxgiFactory; // DXGI ファクトリ
ComPtr<IDXGISwapChain4> _swapChain; // スワップチェイン
ComPtr<ID3D12GraphicsCommandList> _commandList; // コマンドリスト
ComPtr<ID3D12CommandQueue> _commandQueue; // コマンドキュー
ComPtr<ID3D12CommandAllocator> _commandAllocator; // コマンドアロケータ
ComPtr<ID3D12DescriptorHeap> _rtvHeaps; // RTV ディスクリプタヒープ
ComPtr<ID3D12Resource> _renderTargets[FRAME_COUNT]; // バックバッファ
ComPtr<ID3D12Fence> _fence; // フェンス
// --- 初期化用ヘルパー関数群 ---
void ThrowIfFailed(HRESULT hr); // HRESULT チェック
void EnableDebugLayer(); // デバッグレイヤー有効化
void CreateDevice(); // デバイス作成
void CreateCommand(); // コマンド関連作成
void CreateSwapChain(HWND hWnd); // スワップチェイン作成
void CreateDescriptorHeap(); // ディスクリプタヒープ作成
void LinkSwapChaninAndDescriptor(); // RTV とバックバッファをリンク
void CreateFence(); // フェンス作成
// --- デバイス選択 ---
ComPtr<IDXGIAdapter> FindAdapter(); // 使用する GPU アダプタを選択
};
インクルードとライブラリ指定
#pragma once
#include <d3d12.h>
#include <dxgi1_6.h>
#include "Direct3DHelper/d3dx12.h"
#include <wrl.h>
#include <string>
#include <vector>
#include <iostream>
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
using Microsoft::WRL::ComPtr;
・#pragma once
:ヘッダの多重インクルード防止。
・<d3d12.h>
と <dxgi1_6.h>
:Direct3D12(API)と DXGI(スワップチェイン / アダプタ列挙等)用のヘッダ。ここで使っている dxgi1_6.h
は比較的新しい DXGI インターフェースを含みます(環境による互換性に注意)。
・"d3dx12.h"
:Microsoft 公式のユーティリティヘッダ。CD3DX12_***
など便利なラッパ型(リソースバリア作成など)を提供します。プロジェクトに置くか、DirectX-Headers 等から入手しておきます。
・<wrl.h>
と using Microsoft::WRL::ComPtr;
:ComPtr<T>
は COM の参照カウント管理を自動化するスマートポインタ。手動で Release()
する必要がなく安全です。
・#pragma comment(lib, "...")
:リンク時に明示的に d3d12/dxgi をリンクします(Visual Studio 環境では便利)。
クラス公開インターフェース(public 部分)
D3D12App(unsigned int width, unsigned int height);
unsigned int GetWindowWidth() const;
unsigned int GetWindowHeight() const;
void OnInit(HWND hWnd);
void OnRender();
void OnDestroy();
・コンストラクタはウィンドウのクライアント幅・高さを受け取り、内部で扱う想定。
・OnInit(HWND)
:Direct3D の各オブジェクトを作る初期化。順序が非常に重要。
・OnRender()
:フレーム単位での描画処理(コマンド記録→実行→Present→同期)。
・OnDestroy()
:終了処理。GPU が完了するまで待ってハンドルを閉じるなど。
メンバ変数の解説(重要)
unsigned int window_width, window_height;
static const int FRAME_COUNT = 2;
int _fenceValue = 0;
HANDLE _fenceEvent = nullptr;
・window_width
/ window_height
:レンダリング解像度(クライアント領域サイズ)。
・FRAME_COUNT
:バックバッファ数(ヘッダでは2=ダブルバッファ)。スワップチェインと一致させる必要があります。
・_fenceValue
/ _fenceEvent
:CPU⇄GPU 同期に使うフェンス値とイベントハンドル。実運用ではフェンス値は UINT64
を使うのが一般的(将来的なオーバーフロー回避)。
DirectX オブジェクト
ComPtr<ID3D12Device> _device;
ComPtr<IDXGIFactory7> _dxgiFactory;
ComPtr<IDXGISwapChain4> _swapChain;
ComPtr<ID3D12GraphicsCommandList> _commandList;
ComPtr<ID3D12CommandQueue> _commandQueue;
ComPtr<ID3D12CommandAllocator> _commandAllocator;
ComPtr<ID3D12DescriptorHeap> _rtvHeaps;
ComPtr<ID3D12Resource> _renderTargets[FRAME_COUNT];
ComPtr<ID3D12Fence> _fence;
・_device
:GPU(D3D12)デバイス。これがほぼ全てのオブジェクト作成の起点。
・_dxgiFactory
:DXGI ファクトリ。アダプタ列挙や SwapChain 作成に使う。
・_swapChain
:ウィンドウ表示のためのスワップチェイン(ここでは IDXGISwapChain4
を使っている)。
・_commandList
:コマンド記録用。レンダリング命令はここに書く。
・_commandQueue
:GPU にコマンドリストを送るキュー。
・_commandAllocator
:コマンドリストの領域確保に使う(Reset の時に必要)。
・_rtvHeaps
:RTV(レンダーターゲットビュー)用ディスクリプタヒープ。
・_renderTargets
:バックバッファ(FRAME_COUNT
分)。GetBuffer()
→CreateRenderTargetView()
して使います。
・_fence
:GPU コマンドの完了確認に使うフェンスオブジェクト。
ヘルパー関数群(private)
void ThrowIfFailed(HRESULT hr);
void EnableDebugLayer();
void CreateDevice();
void CreateCommand();
void CreateSwapChain(HWND hWnd);
void CreateDescriptorHeap();
void LinkSwapChaninAndDescriptor();
void CreateFence();
ComPtr<IDXGIAdapter> FindAdapter();
・ThrowIfFailed
:HRESULT
をチェックして失敗時に例外を投げる。実装例は一般的です(if(FAILED(hr)) throw std::runtime_error("...");
)。
・EnableDebugLayer
:デバッグビルド時に D3D12 のデバッグレイヤーを有効にする。これを呼ぶと InfoQueue にエラー/warning が流れてデバッグが楽になります。
・CreateDevice
:FindAdapter()
で選択したアダプタ(もしくは WARP)で D3D12CreateDevice
を呼ぶ処理を行う想定。
・CreateCommand
:_commandAllocator
、_commandList
、_commandQueue
を作る。コマンドリストは作ったら通常一度 Close()
しておくパターンが多いです。
・CreateSwapChain(hwnd)
:_dxgiFactory->CreateSwapChainForHwnd(...)
等でスワップチェインを生成する関数。SwapEffect
はモダンでは DXGI_SWAP_EFFECT_FLIP_DISCARD
を使うのが推奨。
・CreateDescriptorHeap
:RTV(および DSV/CBV/SRV など将来の用途)のディスクリプタヒープを作る。
・LinkSwapChainAndDescriptor
: スワップチェインの各バッファに対して GetBuffer()
→ CreateRenderTargetView()
を行い、_renderTargets
と _rtvHeaps
を関連づける処理です。
・CreateFence
:CreateFence(...)
と CreateEvent(...)
を行い、CPU 側で GPU の完了を待てるようにします。
・FindAdapter
:GPU アダプタ選択。
Windowクラスとmain処理の追加
前回作成したmain.cpp
,Window.cpp
,Window.h
にDirectX用の処理を追加します。
#pragma once
#include <Windows.h>
#include "D3D12App.h"
class Window
{
public:
//~ Add 以下のコードを修正しました。
static void Run(D3D12App* d3dApp, HINSTANCE hInstance, int nCmdShow);
//~
private:
static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);
};
#include "Window.h"
LRESULT CALLBACK Window::WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, msg, wp, lp);
}
}
void Window::Run(D3D12App* d3dApp, HINSTANCE hInstance, int nCmdShow)
{
// ウインドウクラス
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.hInstance = hInstance;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = L"DX12Sample";
RegisterClassEx(&wc);
// ウインドウ調整
RECT rect = { 0,0, d3dApp->GetWindowWidth(), d3dApp->GetWindowHeight()};
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
// ウインドウ作成
HWND hWnd = CreateWindow(wc.lpszClassName, L"DX12Sample", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, hInstance, nullptr);
// DirectX12 初期化
d3dApp->OnInit(hWnd); //~ Add このコードを追加
// ウインドウ表示
ShowWindow(hWnd, nCmdShow);
// メインループ
MSG msg = {};
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// DirectX12 メイン処理
d3dApp->OnRender(); //~ Add このコードを追加
}
// DirectX12 終了処理
d3dApp->OnDestroy(); //~ Add このコードを追加
// ウインドウ登録解除
UnregisterClass(wc.lpszClassName, wc.hInstance);
}
#include "Window.h"
#include "D3D12App.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
D3D12App* d3dApp = new D3D12App(1280, 720);
Window::Run(d3dApp, hInstance, nCmdShow);
return 0;
}
D3D12App.cpp
#include "D3D12App.h"
D3D12App::D3D12App(unsigned int width, unsigned int height) : window_width(width), window_height(height)
{
}
void D3D12App::OnInit(HWND hWnd)
{
EnableDebugLayer();
CreateDevice();
CreateCommand();
CreateSwapChain(hWnd);
CreateDescriptorHeap();
LinkSwapChaninAndDescriptor();
CreateFence();
}
void D3D12App::OnRender()
{
// コマンドリストとコマンドアロケーターのクリア
{
ThrowIfFailed(_commandAllocator->Reset());
ThrowIfFailed(_commandList->Reset(_commandAllocator.Get(), nullptr));
}
// レンダーターゲットの設定
{
// 現在のバックバッファインデックスを取得
auto backBufferIdx = _swapChain->GetCurrentBackBufferIndex();
// リソースバリアの設定
auto startResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[backBufferIdx].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
_commandList->ResourceBarrier(1, &startResourceBarrier);
//レンダーターゲットの設定
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart(), backBufferIdx, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
_commandList->OMSetRenderTargets(1, &rtvHandle, true, nullptr);
// 画面クリア
float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
// リソースバリアの設定
auto endResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[backBufferIdx].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
_commandList->ResourceBarrier(1, &endResourceBarrier);
_commandList->Close();
}
// 命令を実行
{
ID3D12CommandList* cmdLists[] = { _commandList.Get() };
_commandQueue->ExecuteCommandLists(1, cmdLists);
// 画面のスワップ
_swapChain->Present(1, 0);
}
// 処理終了の待機
{
ThrowIfFailed(_commandQueue->Signal(_fence.Get(), ++_fenceValue));
if (_fence->GetCompletedValue() != _fenceValue)
{
ThrowIfFailed(_fence->SetEventOnCompletion(_fenceValue, _fenceEvent));
// イベントが発生するまで待ち続ける
WaitForSingleObject(_fenceEvent, INFINITE);
}
}
}
void D3D12App::OnDestroy()
{
CloseHandle(_fenceEvent);
}
ComPtr<IDXGIAdapter> D3D12App::FindAdapter()
{
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(_dxgiFactory.ReleaseAndGetAddressOf())));
// アダプタ列挙用
std::vector<ComPtr<IDXGIAdapter>> adapters;
// 最終的に取得するアダプター
ComPtr<IDXGIAdapter> findAdapter = nullptr;
for (int i = 0; _dxgiFactory->EnumAdapters(i, findAdapter.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; i++)
{
adapters.push_back(findAdapter);
}
for (auto adapter : adapters)
{
DXGI_ADAPTER_DESC adpDesc = {};
// アダプタの説明オブジェクトを取得
adapter->GetDesc(&adpDesc);
std::wstring strDesc = adpDesc.Description;
// 探すアダプタを見つける
if (strDesc.find(L"NVIDIA") != std::wstring::npos)
{
findAdapter = adapter;
break;
}
}
return findAdapter;
}
void D3D12App::ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
throw std::runtime_error("HRESULT failed");
}
}
void D3D12App::EnableDebugLayer()
{
#ifdef _DEBUG
ComPtr<ID3D12Debug> debugLayer = nullptr;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(debugLayer.ReleaseAndGetAddressOf()))))
{
debugLayer->EnableDebugLayer();
}
#endif
}
void D3D12App::CreateDevice()
{
// アダプタを取得
ComPtr<IDXGIAdapter> targetAdapter = FindAdapter();
D3D_FEATURE_LEVEL levels[] =
{
D3D_FEATURE_LEVEL_12_2,
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
};
D3D_FEATURE_LEVEL targetLevel;
for (auto level : levels)
{
if (SUCCEEDED(D3D12CreateDevice(targetAdapter.Get(), level, IID_PPV_ARGS(_device.ReleaseAndGetAddressOf()))))
{
targetLevel = level;
break;
}
}
}
void D3D12App::CreateCommand()
{
// コマンドアロケーターの作成
ThrowIfFailed(_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(_commandAllocator.ReleaseAndGetAddressOf())));
// コマンドリストの作成
ThrowIfFailed(_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _commandAllocator.Get(), nullptr, IID_PPV_ARGS(_commandList.ReleaseAndGetAddressOf())));
ThrowIfFailed(_commandList->Close());
// コマンドキューの作成
{
D3D12_COMMAND_QUEUE_DESC cmdQueueDesc = {};
cmdQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
cmdQueueDesc.NodeMask = 0;
cmdQueueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
cmdQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(_device->CreateCommandQueue(&cmdQueueDesc, IID_PPV_ARGS(_commandQueue.ReleaseAndGetAddressOf())));
}
}
void D3D12App::CreateSwapChain(HWND hWnd)
{
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.Width = window_width;
swapChainDesc.Height = window_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.Stereo = false;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_BACK_BUFFER;
swapChainDesc.BufferCount = 2;
swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
ThrowIfFailed(_dxgiFactory->CreateSwapChainForHwnd(_commandQueue.Get(), hWnd, &swapChainDesc, nullptr, nullptr, (IDXGISwapChain1**)_swapChain.ReleaseAndGetAddressOf()));
}
void D3D12App::CreateDescriptorHeap()
{
D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
heapDesc.NodeMask = 0;
heapDesc.NumDescriptors = 2;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(_device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(_rtvHeaps.ReleaseAndGetAddressOf())));
}
void D3D12App::LinkSwapChaninAndDescriptor()
{
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < FRAME_COUNT; i++)
{
ThrowIfFailed(_swapChain->GetBuffer(i, IID_PPV_ARGS(_renderTargets[i].ReleaseAndGetAddressOf())));
_device->CreateRenderTargetView(_renderTargets[i].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
}
}
void D3D12App::CreateFence()
{
ThrowIfFailed(_device->CreateFence(_fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(_fence.ReleaseAndGetAddressOf())));
_fenceEvent = CreateEvent(nullptr, false, false, nullptr);
}
OnInit — 初期化の起点
void D3D12App::OnInit(HWND hWnd)
{
EnableDebugLayer();
CreateDevice();
CreateCommand();
CreateSwapChain(hWnd);
CreateDescriptorHeap();
LinkSwapChaninAndDescriptor();
CreateFence();
}
1, デバッグレイヤーを有効化(デバッグビルドのみ)。
2, GPU デバイスを作成(CreateDevice
)。
3, コマンドキュー/コマンドアロケータ/コマンドリストを作る(CreateCommand
)。
4, スワップチェインをウィンドウ向けに作成(CreateSwapChain
)。
5, レンダーターゲット用のディスクリプタヒープ(RTV)作成。
6, スワップチェインのバッファを取得して RTV を作る(LinkSwapChaninAndDescriptor
)。
7, フェンス(GPU同期)を作る。
次からは上から順に処理を説明します。
ThrowIfFailed と EnableDebugLayer
void D3D12App::ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
throw std::runtime_error("HRESULT failed");
}
}
単純なエラーチェック。実際は HRESULT
の値をログに入れる、std::system_error
と組み合わせるなどの拡張が便利です。
void D3D12App::EnableDebugLayer()
{
#ifdef _DEBUG
ComPtr<ID3D12Debug> debugLayer = nullptr;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(debugLayer.ReleaseAndGetAddressOf()))))
{
debugLayer->EnableDebugLayer();
}
#endif
}
デバッグビルドで D3D12 の debug layer を有効化。これによりランタイムのエラーや警告が InfoQueue に出るので、実装バグを検出しやすくなります。Visual Studio の環境によってはデバッグレイヤがないこともあるので、SUCCEEDED
チェックで安全にしています。
FindAdapter — アダプタ列挙と選択
ComPtr<IDXGIAdapter> D3D12App::FindAdapter()
{
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(_dxgiFactory.ReleaseAndGetAddressOf())));
std::vector<ComPtr<IDXGIAdapter>> adapters;
ComPtr<IDXGIAdapter> findAdapter = nullptr;
for (int i = 0; _dxgiFactory->EnumAdapters(i, findAdapter.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; i++)
{
adapters.push_back(findAdapter);
}
for (auto adapter : adapters)
{
DXGI_ADAPTER_DESC adpDesc = {};
adapter->GetDesc(&adpDesc);
std::wstring strDesc = adpDesc.Description;
if (strDesc.find(L"NVIDIA") != std::wstring::npos)
{
findAdapter = adapter;
break;
}
}
return findAdapter;
}
目的:システムの GPU(アダプタ)を列挙して、Description
に "NVIDIA"
を含むアダプタを優先的に返しています(NVIDIA 系が欲しい場合の単純な選び方)。
CreateDevice — デバイス作成
ComPtr<IDXGIAdapter> targetAdapter = FindAdapter();
D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_12_2, ..., D3D_FEATURE_LEVEL_10_0 };
D3D_FEATURE_LEVEL targetLevel;
for (auto level : levels)
{
if (SUCCEEDED(D3D12CreateDevice(targetAdapter.Get(), level, IID_PPV_ARGS(_device.ReleaseAndGetAddressOf()))))
{
targetLevel = level;
break;
}
}
FindAdapter()
で選んだアダプタに対してサポートされている最高の D3D_FEATURE_LEVEL
を探し、D3D12CreateDevice
を呼んでいます。
CreateCommand — コマンド系の作成
ThrowIfFailed(_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(_commandAllocator.ReleaseAndGetAddressOf())));
ThrowIfFailed(_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _commandAllocator.Get(), nullptr, IID_PPV_ARGS(_commandList.ReleaseAndGetAddressOf())));
ThrowIfFailed(_commandList->Close());
コマンドアロケータ、コマンドリストを生成。作成直後に Close()
しておくのは一般的なパターン(描画前に Reset()
して再利用する)。
D3D12_COMMAND_QUEUE_DESC cmdQueueDesc = {};
cmdQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
cmdQueueDesc.NodeMask = 0;
cmdQueueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
cmdQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(_device->CreateCommandQueue(&cmdQueueDesc, IID_PPV_ARGS(_commandQueue.ReleaseAndGetAddressOf())));
コマンドキューは CreateSwapChainForHwnd
の際に渡します(注意:スワップチェイン作成で Queue を渡すのが D3D12 のルール)。
CreateSwapChain — スワップチェイン作成
void D3D12App::CreateSwapChain(HWND hWnd)
{
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.Width = window_width;
swapChainDesc.Height = window_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.Stereo = false;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_BACK_BUFFER;
swapChainDesc.BufferCount = 2;
swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
ThrowIfFailed(_dxgiFactory->CreateSwapChainForHwnd(_commandQueue.Get(), hWnd, &swapChainDesc, nullptr, nullptr, (IDXGISwapChain1**)_swapChain.ReleaseAndGetAddressOf()));
}
・バッファのサイズはウィンドウのクライアント領域と一致させます。
・DXGI_FORMAT_R8G8B8A8_UNORM
は一番標準的なカラー形式(8bit x 4 = 32bit)。
・マルチサンプル (MSAA) を使わない設定。(swapChainDesc.Stereo = false;
)
・基本の描画であれば 1 サンプル で十分。(swapChainDesc.SampleDesc.Count = 1;
)
・描画先として使用する用途を指定。(swapChainDesc.BufferUsage = DXGI_USAGE_BACK_BUFFER;
)
・バッファを 2 枚持つことで「描画中」と「表示中」を切り替える ダブルバッファリング を実現。
(swapChainDesc.BufferCount = 2;
)
・SwapEffect
は「フリップ方式」を使うのが現代的。効率が良い。
・ストレッチ指定で、ウィンドウサイズが変わっても描画結果を伸縮表示できます。(swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
)
・フルスクリーンとウィンドウモードを切り替えることを許可します。
(swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
)
ThrowIfFailed(
_dxgiFactory->CreateSwapChainForHwnd(
_commandQueue.Get(), // コマンドを投げるキュー
hWnd, // 紐づけるウィンドウ
&swapChainDesc, // 設定情報
nullptr, // フルスクリーン用の設定(今回はなし)
nullptr, // 出力先モニタ(今回はデフォルト)
(IDXGISwapChain1**)_swapChain.ReleaseAndGetAddressOf()
)
);
ここで スワップチェイン本体が作成 されます。
作成したスワップチェインは _swapChain
に格納され、以降の描画ループで
・バックバッファを取得
・描画命令を発行
・Present()
でフロントバッファと入れ替え
CreateDescriptorHeap — RTV ヒープ作成
void D3D12App::CreateDescriptorHeap()
{
D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
heapDesc.NodeMask = 0;
heapDesc.NumDescriptors = 2;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(_device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(_rtvHeaps.ReleaseAndGetAddressOf())));
}
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
・D3D12_DESCRIPTOR_HEAP_TYPE_RTV
= レンダーターゲットビュー用 のディスクリプタヒープ。
ここに「スワップチェインのバックバッファへの RTV」を格納します。
※ 他にも CBV_SRV_UAV
(定数バッファやシェーダリソースビュー用)、DSV
(深度ステンシル用)などの種類があります。
heapDesc.NodeMask = 0;
・マルチ GPU 環境でどの GPU を使うかを指定するマスク。
・0 なら「全 GPU に可視」という意味。普通のシングル GPU 環境なら 0 で問題なし。
heapDesc.NumDescriptors = 2;
・必要なディスクリプタの数。
・今回は ダブルバッファ (2枚) を使っているので RTV も 2 枚分必要。
・それぞれのバックバッファに RTV を作成してヒープに入れるイメージです。
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
・フラグで「シェーダからアクセス可能にするかどうか」などを制御できます。
・RTV はシェーダから直接参照する必要はないので NONE。
ThrowIfFailed(
_device->CreateDescriptorHeap(
&heapDesc,
IID_PPV_ARGS(_rtvHeaps.ReleaseAndGetAddressOf())
)
);
・CreateDescriptorHeap
で RTV 用のヒープ本体を作成。
・結果は _rtvHeaps
(ComPtr) に格納される。
このヒープには後で CreateRenderTargetView
を呼んでバックバッファ用の RTV を関連付けていきます。
LinkSwapChaninAndDescriptor — バッファ → RTV の紐付け
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < FRAME_COUNT; i++)
{
ThrowIfFailed(_swapChain->GetBuffer(i, IID_PPV_ARGS(_renderTargets[i].ReleaseAndGetAddressOf())));
_device->CreateRenderTargetView(_renderTargets[i].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
}
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart());
・GetCPUDescriptorHandleForHeapStart()
で ディスクリプタヒープの先頭アドレス を取得。
・CD3DX12_CPU_DESCRIPTOR_HANDLE
は d3dx12
ヘルパーで、ディスクリプタの「ポインタ操作」をわかりやすくしてくれるクラス。
ここで得た rtvHandle
は「RTV を作成するときの書き込み先アドレス」を指します。
for (UINT i = 0; i < FRAME_COUNT; i++)
・FRAME_COUNT
は スワップチェインのバッファ数 (ここでは 2)。
・ダブルバッファリングをしているので、2枚分の RTV を作成します。
ThrowIfFailed(_swapChain->GetBuffer(i, IID_PPV_ARGS(_renderTargets[i].ReleaseAndGetAddressOf())));
・スワップチェインから i番目のバックバッファ (ID3D12Resource) を取得。
・_renderTargets[i]
に格納。
これで実際の「描画先テクスチャ」を操作できるようになります。
_device->CreateRenderTargetView(_renderTargets[i].Get(), nullptr, rtvHandle);
・バックバッファ用に RTV (Render Target View) を作成。
・引数
・・第1引数 : 対象リソース (ここではバックバッファ)
・・第2引数 : RTV の詳細設定 (nullptr ならデフォルト)
・・第3引数 : 登録先 (ディスクリプタヒープ内の位置)
これで「バックバッファ → RTV」という対応付けが作られます。
rtvHandle.Offset(1, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
・ディスクリプタヒープは「配列」のように並んでいるので、次の位置に移動する必要があります。
・GetDescriptorHandleIncrementSize
で 1つ分のディスクリプタのサイズ を取得して加算。
・これで次の RTV を正しい場所に作成できます。
CreateFence — フェンス作成
ThrowIfFailed(_device->CreateFence(_fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(_fence.ReleaseAndGetAddressOf())));
_fenceEvent = CreateEvent(nullptr, false, false, nullptr);
ThrowIfFailed(_device->CreateFence(
_fenceValue,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(_fence.ReleaseAndGetAddressOf())
));
・CreateFence
は GPU と CPU の同期用オブジェクト を作成する関数。
・引数の意味:
・・_fenceValue
: 初期値(ここでは 0)。「どのフレームまで処理が終わったか」を示すカウンタ。
・・D3D12_FENCE_FLAG_NONE
: 特別なオプションはなし。
・・_fence
: 作成されたフェンスオブジェクトを格納する。
_fenceEvent = CreateEvent(nullptr, false, false, nullptr);
・Windows API のイベントオブジェクト を作成。
・引数の意味:
・・nullptr
: セキュリティ属性 (デフォルト)。
・・false
: 手動リセットではなく自動リセット。
・・false
: 最初は非シグナル状態。
・・nullptr
: 名前なしイベント。
描画処理
void D3D12App::OnRender()
{
// コマンドリストとコマンドアロケーターのクリア
{
ThrowIfFailed(_commandAllocator->Reset());
ThrowIfFailed(_commandList->Reset(_commandAllocator.Get(), nullptr));
}
// レンダーターゲットの設定
{
// 現在のバックバッファインデックスを取得
auto backBufferIdx = _swapChain->GetCurrentBackBufferIndex();
// リソースバリアの設定
auto startResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[backBufferIdx].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
_commandList->ResourceBarrier(1, &startResourceBarrier);
//レンダーターゲットの設定
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart(), backBufferIdx, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
_commandList->OMSetRenderTargets(1, &rtvHandle, true, nullptr);
// 画面クリア
float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
// リソースバリアの設定
auto endResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[backBufferIdx].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
_commandList->ResourceBarrier(1, &endResourceBarrier);
_commandList->Close();
}
// 命令を実行
{
ID3D12CommandList* cmdLists[] = { _commandList.Get() };
_commandQueue->ExecuteCommandLists(1, cmdLists);
// 画面のスワップ
_swapChain->Present(1, 0);
}
// 処理終了の待機
{
ThrowIfFailed(_commandQueue->Signal(_fence.Get(), ++_fenceValue));
if (_fence->GetCompletedValue() != _fenceValue)
{
ThrowIfFailed(_fence->SetEventOnCompletion(_fenceValue, _fenceEvent));
// イベントが発生するまで待ち続ける
WaitForSingleObject(_fenceEvent, INFINITE);
}
}
}
① コマンドリストとコマンドアロケーターのリセット
ThrowIfFailed(_commandAllocator->Reset());
ThrowIfFailed(_commandList->Reset(_commandAllocator.Get(), nullptr));
・CommandAllocator
… コマンドの「バッファ」。前回のフレームの命令を一旦リセット。
・CommandList
… 実際の GPU 命令を積むリスト。これも新しく使えるようにリセット。
👉 「前のフレームのゴミを消して、新しいフレームの描画命令を積み始める準備」。
② レンダーターゲットの設定
auto backBufferIdx = _swapChain->GetCurrentBackBufferIndex();
・スワップチェインには複数のバックバッファがあり(ここでは 2 枚)、そのうち「今描画すべきバッファ」を取得。
auto startResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(
_renderTargets[backBufferIdx].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET
);
_commandList->ResourceBarrier(1, &startResourceBarrier);
・GPU のリソースは「状態 (State)」を持つ。
・表示用 (Present) の状態から、描画先 (RenderTarget) の状態へ切り替え。
・リソースバリアを使うことで「今はこのバッファに描画していいよ」と宣言する。
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
_rtvHeaps->GetCPUDescriptorHandleForHeapStart(),
backBufferIdx,
_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)
);
_commandList->OMSetRenderTargets(1, &rtvHandle, true, nullptr);
・rtvHandle
… 「どのバックバッファを描画対象にするか」を示すハンドル。
・OMSetRenderTargets
… 出力マージャーステージ (Output Merger) にターゲットをセット。
ここで「このバックバッファに描け!」と GPU に指示する。
float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
・背景色で画面を塗りつぶす。
・ここでは黒 (R=0, G=0, B=0, A=1) でクリア。
auto endResourceBarrier =
CD3DX12_RESOURCE_BARRIER::Transition(
_renderTargets[backBufferIdx].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT
);
_commandList->ResourceBarrier(1, &endResourceBarrier);
・描画が終わったので、バッファを再び 表示用 (Present) 状態に戻す。
・これでスワップチェインが画面に表示できる。
_commandList->Close();
・命令の記録が終わったので、これ以上追記できないようにクローズ。
③ 命令を実行
ID3D12CommandList* cmdLists[] = { _commandList.Get() };
_commandQueue->ExecuteCommandLists(1, cmdLists);
・コマンドリストをコマンドキューに積み、GPU に実行させる。
_swapChain->Present(1, 0);
・バックバッファとフロントバッファを入れ替える。
・第1引数 1
は 垂直同期 (VSync) を待つ設定。
👉 これで画面に描画結果が表示される。
④ GPU の完了を待機
ThrowIfFailed(_commandQueue->Signal(_fence.Get(), ++_fenceValue));
if (_fence->GetCompletedValue() != _fenceValue)
{
ThrowIfFailed(_fence->SetEventOnCompletion(_fenceValue, _fenceEvent));
WaitForSingleObject(_fenceEvent, INFINITE);
}
1, Signal
… 「このフレームの処理が終わったらフェンス値を増やしてね」と GPU に伝える。
2, GetCompletedValue
… GPU がどこまで処理したか確認。
3, まだ終わってなければ SetEventOnCompletion
でイベントを設定しWaitForSingleObject
で待機。
👉 こうすることで GPU の描画完了を保証 し、次のフレームに進める。
描画処理のまとめ
・リセット : 前フレームのコマンドを消して準備。
・リソースバリア : バッファ状態を Present → RenderTarget → Present
に切り替え。
・レンダーターゲットセット & クリア : 描画先を決めて画面クリア。
・実行 & スワップ : 命令を GPU に渡し、画面に表示。
・フェンス待機 : GPU の処理が終わるまで CPU を待たせる。
完成だ!!
ということで、実行!!
完璧すぎる!!!
最後に
最後まで閲覧いただきありがとうございます。今回はDirectX12の画面クリア処理でかなり膨大なコードを書きました。記事の方もかなり長くなってると思います。投稿は不定期になりますが、次回は、ポリゴン描画かなと思います。
Discussion