📘

DirectX12を勉強してみる 第2回目 画面のクリア

に公開

はじめに

前回はウインドウ表示までやりました。今回は、ウインドウの画面をDirectX12を使って黒色にクリアしていきます。
注意: 今回の記事は長すぎるので、説明を簡単に済ませている場所があります。ご了承ください。

開発環境

エディタ: Visual Studio 2022 Pro
言語: C++

クラスの作成

D3D12App.cppD3D12App.hを作成しました。

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 アダプタを選択
};

インクルードとライブラリ指定

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;

#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.h
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 が完了するまで待ってハンドルを閉じるなど。

メンバ変数の解説(重要)

D3D12App.h
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 オブジェクト

D3D12App.h
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)

D3D12App.h
void ThrowIfFailed(HRESULT hr);
void EnableDebugLayer();
void CreateDevice();
void CreateCommand();
void CreateSwapChain(HWND hWnd);
void CreateDescriptorHeap();
void LinkSwapChaninAndDescriptor();
void CreateFence();
ComPtr<IDXGIAdapter> FindAdapter();

ThrowIfFailedHRESULT をチェックして失敗時に例外を投げる。実装例は一般的です(if(FAILED(hr)) throw std::runtime_error("...");)。

EnableDebugLayer:デバッグビルド時に D3D12 のデバッグレイヤーを有効にする。これを呼ぶと InfoQueue にエラー/warning が流れてデバッグが楽になります。

CreateDeviceFindAdapter() で選択したアダプタ(もしくは 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 を関連づける処理です。

CreateFenceCreateFence(...)CreateEvent(...) を行い、CPU 側で GPU の完了を待てるようにします。

FindAdapter:GPU アダプタ選択。

Windowクラスとmain処理の追加

前回作成したmain.cpp,Window.cpp,Window.hにDirectX用の処理を追加します。

Window.h
#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);
};
Window.cpp
#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);

}

main.cpp
#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

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 — 初期化の起点

D3D12App.cpp
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

D3D12.cpp
void D3D12App::ThrowIfFailed(HRESULT hr)
{
	if (FAILED(hr))
	{
		throw std::runtime_error("HRESULT failed");
	}
}

単純なエラーチェック。実際は HRESULT の値をログに入れる、std::system_error と組み合わせるなどの拡張が便利です。

D3D12App.cpp
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 — アダプタ列挙と選択

D3D12App.cpp
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 — デバイス作成

D3D12App.cpp
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 — コマンド系の作成

D3D12.cpp
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() して再利用する)。

D3D12App.cpp
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 — スワップチェイン作成

D3D12App.cpp
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;)

D3D12App.cpp
ThrowIfFailed(
    _dxgiFactory->CreateSwapChainForHwnd(
        _commandQueue.Get(),   // コマンドを投げるキュー
        hWnd,                  // 紐づけるウィンドウ
        &swapChainDesc,        // 設定情報
        nullptr,               // フルスクリーン用の設定(今回はなし)
        nullptr,               // 出力先モニタ(今回はデフォルト)
        (IDXGISwapChain1**)_swapChain.ReleaseAndGetAddressOf()
    )
);

ここで スワップチェイン本体が作成 されます。
作成したスワップチェインは _swapChain に格納され、以降の描画ループで
・バックバッファを取得
・描画命令を発行
Present() でフロントバッファと入れ替え

CreateDescriptorHeap — RTV ヒープ作成

D3D12App.cpp
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())));
}
D3D12App.cpp
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;

D3D12_DESCRIPTOR_HEAP_TYPE_RTV = レンダーターゲットビュー用 のディスクリプタヒープ。
ここに「スワップチェインのバックバッファへの RTV」を格納します。
※ 他にも CBV_SRV_UAV(定数バッファやシェーダリソースビュー用)、DSV(深度ステンシル用)などの種類があります。

D3D12App.cpp
heapDesc.NodeMask = 0;

・マルチ GPU 環境でどの GPU を使うかを指定するマスク。
・0 なら「全 GPU に可視」という意味。普通のシングル GPU 環境なら 0 で問題なし。

D3D12App.cpp
heapDesc.NumDescriptors = 2;

・必要なディスクリプタの数。
・今回は ダブルバッファ (2枚) を使っているので RTV も 2 枚分必要。
・それぞれのバックバッファに RTV を作成してヒープに入れるイメージです。

D3D12App.cpp
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

・フラグで「シェーダからアクセス可能にするかどうか」などを制御できます。
・RTV はシェーダから直接参照する必要はないので NONE。

D3D12App.cpp
ThrowIfFailed(
    _device->CreateDescriptorHeap(
        &heapDesc, 
        IID_PPV_ARGS(_rtvHeaps.ReleaseAndGetAddressOf())
    )
);

CreateDescriptorHeap で RTV 用のヒープ本体を作成。
・結果は _rtvHeaps (ComPtr) に格納される。
このヒープには後で CreateRenderTargetView を呼んでバックバッファ用の RTV を関連付けていきます。

LinkSwapChaninAndDescriptor — バッファ → RTV の紐付け

D3D12.cpp
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));
}
D3D12.cpp
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeaps->GetCPUDescriptorHandleForHeapStart());

GetCPUDescriptorHandleForHeapStart() で ディスクリプタヒープの先頭アドレス を取得。
CD3DX12_CPU_DESCRIPTOR_HANDLEd3dx12 ヘルパーで、ディスクリプタの「ポインタ操作」をわかりやすくしてくれるクラス。
ここで得た rtvHandle は「RTV を作成するときの書き込み先アドレス」を指します。

D3D12.cpp
for (UINT i = 0; i < FRAME_COUNT; i++)

FRAME_COUNT は スワップチェインのバッファ数 (ここでは 2)。
・ダブルバッファリングをしているので、2枚分の RTV を作成します。

D3D12.cpp
ThrowIfFailed(_swapChain->GetBuffer(i, IID_PPV_ARGS(_renderTargets[i].ReleaseAndGetAddressOf())));

・スワップチェインから i番目のバックバッファ (ID3D12Resource) を取得。
_renderTargets[i] に格納。
これで実際の「描画先テクスチャ」を操作できるようになります。

D3D12.cpp
_device->CreateRenderTargetView(_renderTargets[i].Get(), nullptr, rtvHandle);

・バックバッファ用に RTV (Render Target View) を作成。
・引数
・・第1引数 : 対象リソース (ここではバックバッファ)
・・第2引数 : RTV の詳細設定 (nullptr ならデフォルト)
・・第3引数 : 登録先 (ディスクリプタヒープ内の位置)
これで「バックバッファ → RTV」という対応付けが作られます。

D3D12.cpp
rtvHandle.Offset(1, _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));

・ディスクリプタヒープは「配列」のように並んでいるので、次の位置に移動する必要があります。
GetDescriptorHandleIncrementSize で 1つ分のディスクリプタのサイズ を取得して加算。
・これで次の RTV を正しい場所に作成できます。

CreateFence — フェンス作成

D3D12.cpp
ThrowIfFailed(_device->CreateFence(_fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(_fence.ReleaseAndGetAddressOf())));
_fenceEvent = CreateEvent(nullptr, false, false, nullptr);
D3D12.cpp
ThrowIfFailed(_device->CreateFence(
    _fenceValue,
    D3D12_FENCE_FLAG_NONE,
    IID_PPV_ARGS(_fence.ReleaseAndGetAddressOf())
));

CreateFence は GPU と CPU の同期用オブジェクト を作成する関数。
・引数の意味:
・・_fenceValue : 初期値(ここでは 0)。「どのフレームまで処理が終わったか」を示すカウンタ。
・・D3D12_FENCE_FLAG_NONE : 特別なオプションはなし。
・・_fence : 作成されたフェンスオブジェクトを格納する。

D3D12.cpp
_fenceEvent = CreateEvent(nullptr, false, false, nullptr);

・Windows API のイベントオブジェクト を作成。
・引数の意味:
・・nullptr : セキュリティ属性 (デフォルト)。
・・false : 手動リセットではなく自動リセット。
・・false : 最初は非シグナル状態。
・・nullptr : 名前なしイベント。

描画処理

D3D12.cpp
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);
		}
	}
}

① コマンドリストとコマンドアロケーターのリセット

D3D12.cpp
ThrowIfFailed(_commandAllocator->Reset());
ThrowIfFailed(_commandList->Reset(_commandAllocator.Get(), nullptr));

CommandAllocator … コマンドの「バッファ」。前回のフレームの命令を一旦リセット。
CommandList … 実際の GPU 命令を積むリスト。これも新しく使えるようにリセット。
👉 「前のフレームのゴミを消して、新しいフレームの描画命令を積み始める準備」。

② レンダーターゲットの設定

D3D12.cpp
auto backBufferIdx = _swapChain->GetCurrentBackBufferIndex();

・スワップチェインには複数のバックバッファがあり(ここでは 2 枚)、そのうち「今描画すべきバッファ」を取得。

D3D12.cpp
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) の状態へ切り替え。
・リソースバリアを使うことで「今はこのバッファに描画していいよ」と宣言する。

D3D12.cpp
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 に指示する。

D3D12.cpp
float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

・背景色で画面を塗りつぶす。
・ここでは黒 (R=0, G=0, B=0, A=1) でクリア。

D3D12.cpp
auto endResourceBarrier =
    CD3DX12_RESOURCE_BARRIER::Transition(
        _renderTargets[backBufferIdx].Get(),
        D3D12_RESOURCE_STATE_RENDER_TARGET,
        D3D12_RESOURCE_STATE_PRESENT
    );
_commandList->ResourceBarrier(1, &endResourceBarrier);

・描画が終わったので、バッファを再び 表示用 (Present) 状態に戻す。
・これでスワップチェインが画面に表示できる。

D3D12.cpp
_commandList->Close();

・命令の記録が終わったので、これ以上追記できないようにクローズ。

③ 命令を実行

D3D12.cpp
ID3D12CommandList* cmdLists[] = { _commandList.Get() };
_commandQueue->ExecuteCommandLists(1, cmdLists);

・コマンドリストをコマンドキューに積み、GPU に実行させる。

D3D12.cpp
_swapChain->Present(1, 0);

・バックバッファとフロントバッファを入れ替える。
・第1引数 1 は 垂直同期 (VSync) を待つ設定。
👉 これで画面に描画結果が表示される。

④ GPU の完了を待機

D3D12.cpp
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