関連記事/リポジトリ
DirectX Graphics samples HelloTexture
https://github.com/microsoft/DirectX-Graphics-Samples/tree/master/Samples/Desktop/D3D12HelloWorld/src/HelloTexture
https://qiita.com/YKVKDX12/items/e09ef7981a8efeb14523
https://qiita.com/YKVKDX12/items/570ffca08b9ff35fdadf
出力される画像
見てほしいところ
・キャンバスの用意(リソースの確保)
・アトリエから美術館へ搬入(GPUへのアップロードと状態遷移)
・346~364行目 リソースアップロードバッチを使って
使用ライブラリ
DirectXTK12 desckop win10(Nuget経由でインストール)
https://github.com/microsoft/directxtk12
D3D12 Memory allocator(ソースコードをコピペして使用)
https://gpuopen.com/d3d12-memory-allocator/
D3DCompiler.h
フルコード
これから注意する点
いきなりコードを改修せず、どこをどう変えるかを決める
どのようなコードにする(なった)のか?を決める
たとえば、今回はDirectXTK12とD3D12MAを使って
DirectXTK12でリソースアップロード、リソースバリアのラッピング
D3D12MAでテクスチャバッファのラッピングを行った
AIとの協働を想定する
なので反省やこのように備忘録として、コードの解説、どんなライブラリを使ったか、ワークフローは?という資料を少しずつまとめる
特に、ローカルAI(LLM/SLM)に提供する資料を想定した、簡潔なものにする
プログラムを一つ書いて終わりではない
むしろ資料まで作って、投資とする。AIに学習させたり、他の方に読んでもらい、スケールすることが目的となる
解説
D3D12MAを使って、DirectXGraphicSampleのHelloTextureをベースとしたサンプルコードを書きました。
簡略化のために、リソース遷移に関してDirectXTK12の
ResoureUploadBatchを使用しています
また、シェーダーコードのコンパイルに変更があります
元のサンプルは頂点シェーダーとピクセルシェーダーを同じファイルに実装
DXC.exeとカスタムビルドをつかった、.csoファイルによるHLSL_6_0プロファイルのシェーダーを読み込む形式ですが
今回のワークフローでは
D3DCompilerを使った、FXC.exeでコンパイルした頂点シェーダーとピクセルシェーダー
(どちらもHLSL5_0プロファイルを使用)
したものとなっています。
今回のワークフロー
テクスチャのフォーマット(今回はRGBAの8ビット)やサイズを指定し、GPU上にメモリを確保
キャンバスの用意(リソースの確保): GPU上に、テクスチャを格納するためのまっさらな領域(メモリ)を確保
絵を描く(データの準備): CPU側で、実際に表示するピクセルデータを作成
アトリエから美術館へ搬入(GPUへのアップロード): CPU側のデータをGPUのメモリへ転送し、書き込み用の状態から「展示用(シェーダー読み取り用)」の状態へと切り替え
キャプションの設置(SRVの作成): GPU(シェーダー)が作品を正しく解釈できるように、データの形式や見方を示す「シェーダーリソースビュー(SRV)」を作成
キャンバスの用意(リソースの確保)
ここではD3D12 Memory Allocator (D3D12MA) を使用しており、煩雑なヒープ管理をライブラリに委譲
状態はデータを受け取るため D3D12_RESOURCE_STATE_COPY_DEST(コピー先)として初期化
CD3DX12_RESOURCE_DESC textureDesc =
CD3DX12_RESOURCE_DESC::Tex2D(
DXGI_FORMAT_R8G8B8A8_UNORM,
TextureWidth,
TextureHeight
);
D3D12MA::ALLOCATION_DESC textureAllocDesc = {};
textureAllocDesc.HeapType = D3D12_HEAP_TYPE_DEFAULT;
DX::ThrowIfFailed(
m_allocator->CreateResource(
&textureAllocDesc,
&textureDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
m_textureAllocation.ReleaseAndGetAddressOf(),
IID_PPV_ARGS(m_texture.ReleaseAndGetAddressOf())
)
);
絵を描く(データの準備)
次に、テクスチャとして流し込む具体的なピクセルデータ(今回であればチェッカーボード模様)をCPU側で生成
DirectX12にデータを渡すため、行ごとのバイト数(RowPitch)や全体サイズ(SlicePitch)を計算し、D3D12_SUBRESOURCE_DATA 構造体にまとめる
// GenerateTextureData() でピクセル配列(std::vector<UINT8>)を生成
std::vector<UINT8> textureData = GenerateTextureData();
D3D12_SUBRESOURCE_DATA subresourceData = {};
subresourceData.pData = textureData.data();
subresourceData.RowPitch =
static_cast<LONG_PTR>(TextureWidth * TexturePixelSize);
subresourceData.SlicePitch =
static_cast<LONG_PTR>(TextureWidth * TextureHeight * TexturePixelSize);
アトリエから美術館へ搬入(GPUへのアップロードと状態遷移)
DirectX 12では、CPUとGPU間のメモリ同期やリソースの状態管理をアプリケーション側で明示的に行う必要がある
ネイティブAPIでテクスチャを転送しようとすると、「運送用トラック(Upload Heap)」を自前で確保し、「搬入指示書(コマンドリスト)」を作成し、さらに手作業で「展示状態への切り替え(リソースバリア)」を申請するなど、非常に煩雑
めんどくせー。
ResourceUploadBatch は、こうした手続きを一手に引き受ける「一括搬入・設営の専門業者」
ちわーっす。リソースアップロードバッチでーす
346~364行目 リソースアップロードバッチを使って
ResourceUploadBatch resourceUpload(device);
// 業者への指示記録を開始
resourceUpload.Begin();
// ① 作品の搬入指示
resourceUpload.Upload(
m_texture.Get(),
0,
&subresourceData,
1
);
// ② 設営(状態遷移)の指示
resourceUpload.Transition(
m_texture.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE
);
// ③ 指示書の提出と、作業完了の待機
auto uploadFinished = resourceUpload.End(DR->GetCommandQueue());
uploadFinished.wait();
Upload()(搬入の自動化): アトリエ(システムメモリ)にあるピクセルデータを業者に渡す
シェーダーリソースビューの作成
GPUのメモリ上にデータが配置されても、シェーダーはそのままではどのデータをどのように読み取ればよいかわからない。
そこで、ディスクリプタヒープ(Descriptor Heap)上に「シェーダーリソースビュー(SRV)」を作成
これは、テクスチャのフォーマットやミップマップの数などをシェーダーに伝える役割がある
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping =
D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = textureDesc.Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
// ディスクリプタヒープの特定の場所にSRVを作成する
device->CreateShaderResourceView(
m_texture.Get(),
&srvDesc,
m_resourceDescriptors->GetCpuHandle(Descriptors::WindowsLogo)
);
DescriptorHeapもDirectXTK12の
https://github.com/microsoft/DirectXTK12/wiki/DescriptorHeap
ラッパーを使用
以上の手順により、テクスチャデータが完全にGPU上で利用可能となり、描画時の commandList->SetGraphicsRootDescriptorTable() を経てピクセルシェーダー内でサンプリングされる準備が整う
シェーダー初期化
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
// 頂点シェーダー(VS)のコンパイル
DX::ThrowIfFailed(
D3DCompileFromFile(
L"VertexShader.hlsl", // ファイルパス
nullptr, nullptr,
"VSMain", // エントリーポイント名
"vs_5_0", // シェーダーモデル(バージョン)
compileFlags, 0,
pVertexShaderData.ReleaseAndGetAddressOf(),
errorBlob.ReleaseAndGetAddressOf()
)
);
// ピクセルシェーダー(PS)のコンパイル
DX::ThrowIfFailed(
D3DCompileFromFile(
L"PixelShader.hlsl",
nullptr, nullptr,
"PSMain",
"ps_5_0",
compileFlags, 0,
pPixelShaderData.ReleaseAndGetAddressOf(),
errorBlob.ReleaseAndGetAddressOf()
)
);
ピクセルシェーダーとルートシグネチャの関係
ピクセルシェーダーでは
・SamplerState
// ピクセルシェーダー (PixelShader.hlsl)
Texture2D g_texture : register(t0); // t0レジスタ(Textureの0番)
SamplerState g_sampler : register(s0); // s0レジスタ(Samplerの0番)
float4 PSMain(PSInput input) : SV_TARGET
{
// t0のテクスチャを、s0のサンプラー(読み取りルール)に従って読み込む
return g_texture.Sample(g_sampler, input.uv);
}
となっている。
このうち
g_texture : register(t0); // t0レジスタ(Textureの0番)が
CD3DX12_DESCRIPTOR_RANGE1 ranges[1];
ranges[0].Init(
D3D12_DESCRIPTOR_RANGE_TYPE_SRV, // SRV (シェーダーリソースビュー) を渡す
1, // ディスクリプタの数は1つ
0, // ベースシェーダーレジスタ: 0 (つまり t0 に紐づく)
0,
D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC
);
以下のルートシグネチャ
// ② ルートパラメータの定義(インデックス0番)
CD3DX12_ROOT_PARAMETER1 rootParameters[1];
rootParameters[0].InitAsDescriptorTable(
1,
&ranges[0],
D3D12_SHADER_VISIBILITY_PIXEL // ピクセルシェーダーからのみ見えるように制限
);
// ③ 静的サンプラーの定義
D3D12_STATIC_SAMPLER_DESC sampler = {};
// (中略: フィルタリングやアドレスモードの設定)
sampler.ShaderRegister = 0; // シェーダーレジスタ: 0 (つまり s0 に紐づく)
sampler.RegisterSpace = 0;
sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
によってCPUとGPUで紐づいている
これを、
// ルートパラメータの [インデックス0番] に、[WindowsLogo] のテクスチャビューを接続する
commandList->SetGraphicsRootDescriptorTable(
0,
m_resourceDescriptors->GetGpuHandle(Descriptors::WindowsLogo)
);
コマンドリストからのSetGraphicsRootDescriptorTable命令によってCPUからGPUに、テクスチャのデータが載せられたディスクリプタヒープ
m_resourceDescriptors
を送信している。ということになる。
これでピクセルシェーダーにテクスチャのデータが送信され、
テクスチャが描画される準備が整う
もっかい作業フローのまとめ
リソースの確保とデータ生成: CPU側でテクスチャの生データ(ピクセル配列)を作成し、GPU上にコピー先の空き領域(リソース)を確保する。
GPUへのアップロードと状態遷移: ResourceUploadBatch 等を用いて、CPUのメモリからGPUのメモリへデータを一括転送(Upload)し、リソースの状態を「データ書き込み用」から「シェーダー読み取り用」へと遷移(Transition / リソースバリア)させる。
インターフェースの定義(ルートシグネチャ): シェーダー(HLSL)側の受け口となるレジスタ(register(t0)など)に対応するルートパラメータをC++側で定義し、グラフィックスパイプライン(PSO)の仕様を固める。
ビューの作成とバインド: 転送したテクスチャのフォーマットを定義するシェーダーリソースビュー(SRV)をディスクリプタヒープ上に作成し、描画の直前にコマンドリスト経由でルートパラメータの指定スロットに接続(SetGraphicsRootDescriptorTable)する。
描画(ドローコール): 頂点バッファをセットして描画命令(DrawInstanced)を発行。頂点シェーダーが位置を計算し、ピクセルシェーダーがバインドされたテクスチャをサンプリングして画面を塗りつぶす。
作業に必要な要素
テクスチャの生データ(CPU側で生成またはロードした画像配列)
VRAM上の領域(D3D12MA::Allocation, ID3D12Resource)
データ転送機構(ResourceUploadBatch とコマンドキュー)
テクスチャの仕様書・カタログ(ディスクリプタヒープとSRV)
CPUとGPUの配線設計図(ルートシグネチャとPSO)
描画ロジック(HLSLの頂点シェーダーとピクセルシェーダー)
