Browsed by
Category: DirectX

[DirectX] PIX と Graphics Debugger

[DirectX] PIX と Graphics Debugger

PIX は Microsoft から提供されている、DirectX アプリケーションの Graphics Debugger ツールです。フレームキャプチャやパフォーマンスチューニングの機能を持っています。

DirectX 11(Windows 7)の頃までは DirectX SDK に付属する形で「PIX for Windows(PIXWin.exe)」というスタンドアロンのツールが提供されていましたが、Winodws 8 の時代になると DirectX SDK は Windows SDK に統合される形になり、そのタイミングで PIX for Windows は機能の開発や提供は一時的に終了となりました。

代わりに出てきたのが、Visual Studio に統合する形で提供された Graphics Debugger(Graphics Diagnostics)というツールでした。これは Visual Studio 2012 から利用できるようになり、Visual Studio 上でコーディングから DIrectX アプリケーションのデバッグまでを行うことが可能となりました。当初は DirectX 10/11 の対応でしたが、2015年にリリースされた Windows 10 と Visual Studio 2015 からは DirectX 12 にも対応しています。

DirectX Tools for Windows 10 in Visual Studio 2015
https://learn.microsoft.com/en-us/shows/connecton-demand/212

動画を見てもらえれば分かるように Visual Studio 上で DirectX のパフォーマンス解析や Frame Capture などが行えています。

新規 PIX の提供

https://devblogs.microsoft.com/pix/introducing-pix-on-windows-beta/

暫くの間、Windows のグラフィックスデバッガは Visual Studio 上のツールとして提供されていましたが、2017年1月には別の動きがありました。DirectX SDK での更新を最後に止まっていた Windows 向けの PIX が新規ツールとして再び提供されるようになりました。

こちらは DirectX 12 のみに対応しており(後にD3D11on12には対応)UI なども一新され、名称も「PIX for Windows」から「PIX on Windows」と僅かに変更がありました。

そして、新規 PIX がリリースされた約1年後、2018年にリリースされた Windows10 1809 からは、以前まで利用できていた Visual Studio 上の Graphics Debugging のサポートも打ち切りとなりました。

DirectX12 debugging in Visual Studio is no longer supported by the Windows team in 1809 and the recommended path forward is to use their PIX for Windows app.

https://developercommunity.visualstudio.com/t/visual-studio-directx-graphics-debugging-tool-can/417292

そのため、Microsoft から提供される DirectX アプリケーションの Graphics Debugger は再び PIX に一本化されることとなりました。

PIX の更新

2024年現在 PIX は年に数回と、かなりの高頻度でアップデートが行われています。

特に Agility SDK の更新に追随する形でデバッグ機能もサポートした PIX も同時にリリースしてくれているので、手探りの状態で進めないといけない新規機能の対応などでは PIX が非常に活躍してくれます。

参考

https://devblogs.microsoft.com/pix/download/

[DirectX12] WinPixEventRuntimeを使ってPIX向けにEvent名を仕込む

[DirectX12] WinPixEventRuntimeを使ってPIX向けにEvent名を仕込む

PIX は DirectX で作成されたアプリケーションのパフォーマンスチューニングや描画の解析を行うためのツールです。1フレームの中でどのような順番で何が描かれたのかを観測できたり、1パスの中で描画に掛かった時間を計測できたりするので、意図しない描画結果が生成された場合や想像以上にアプリケーションが重い場合などのデバッグに役立ちます。

DirectX 12で作成されたアプリケーションであれば、そのままでも PIX 経由で Launch すれば GPU Capture などを取ることが可能ですが、ここで紹介する WinPixEventRuntime を使用することで、デバッグに役立つラベルが付いた状態でキャプチャーを取ることが可能です。

なぜラベル付けが必要か?

DirectX のアプリケーションは、非常に膨大なグラフィックスコマンドの呼び出し(記録)によって処理が行われます。

適宜、これが Shadow pass なのか G-buffer pass なのか Lighting pass なのか、ラベルが付いていると視認しやすくなります。

WinPixEventRuntime の使い方

インストール方法はこのページに記載があります。WinPixEventRuntime は NuGet パッケージ経由でインストールを行います。

PIXBeginEvent/PIXEndEvent の内部は?

pix3.h の関数はヘッダー内で実装が完結しているものが多いため、ブレークポイントで止めて Stepin して中身の実装を確認することが可能です。実は commandlist::beginevent をラップしたようなものになっています。

そのため、PIXBeginEvent はどこか特殊なバッファに情報を記録しているという訳ではなく、実際はアプリケーション側で作成した DirectX12 の commandlist/commandqueue に対して、計測用の情報を記録している形になっています。

https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-beginevent

PIXBeginEvent で仕込んだイベント名は、PIX でしか使えないと思われるかもしれませんが、NSight や RenderDoc でキャプチャーした場合でもイベント名を取得することが可能です。PIXBeginEvent で設定さえすれば他のグラフィックスデバッガーでも同様に使えるので非常に便利です。

[DirectX12] Compute Shader の Thread とシステム値

[DirectX12] Compute Shader の Thread とシステム値

Compute Shader で起動するスレッド数は、コマンドバッファに記録する Dispatch() の引数と、シェーダー内に記述する numthreads の Thread Group 数によって決まる。Dispatch() の引数は、アプリ実行時に動的に変更することが可能だが、numthreads への指定はシェーダーコンパイル時に決まるため、こちらは固定の数になる。

Compute Shader で起動したスレッドは、後述するシステムセマンティックの ID によって、シェーダー内で一意の ID を取得することが出来る。その ID によって処理やデータを分岐して処理する。

Dispatch

実行する Thread Group の数を X, Y, Z で指定する。例えば 1,1,1 で Dispatch した場合は、1つの Thread Group が起動する。

numthreads

コンピュートシェーダーが Dispatch された(起動した)ときの、一つの Thread Group 内で起動するスレッド数を numthreads で指定する。シェーダーコンパイル時に値が決まるため、動的な変更はできない。

System Value

Compute Shader で取得できるシステムセマンティックは4つある。

uint3 gidSV_GroupID
uint3 dtidSV_DispatchThreadID
uint3 gtidSV_GroupThreadID
uint giSV_GroupIndex

SV_GroupID (uint3)

スレッドが、どこの Thread Group で起動しているかを取得できる。Dispatch に指定した値によって変化する。
例えば、Dispatch(2,1,1) を呼び出すと、2*1*1 = 2 Thread Group が起動して、SV_GroupID の値としては(0,0,0)と(1,0,0)という値がスレッド内で得られる。

SV_GroupThreadID (uint3)

スレッドが、Thread Group 内のどの Thread かを表す。numthread に指定した値によって変化する。
例えば、numthreads(3,2,1) が指定された場合は 3*2*1 = 6 Thread 起動する。SV_GroupThreadID としては (0-2, 0-1, 0) 範囲の値が取得できる。

SV_DispatchThreadID (uint3)

Dispatch と numthread の組み合わせによって変化する。

SV_GroupIndex (uint)

起動例 1

Distach(4, 4, 4) / numthread[1,1,1]

SV_GroupID には (0,0,0)~(3,3,3) の値が来る。
SV_GroupID から、一意な Index を取得したい場合は、下記で変換できる。

// index = (gid.z * dispatch_y * dispatch_x) + (gid.y * dispatch_x) + gid.z
uint index = (gid.z * 4 * 4) + (gid.y * 4) + gid.z;

index は 0~63 までの値になる。

[DirectX12] View Matrix での変換は何をしているか

[DirectX12] View Matrix での変換は何をしているか

View 行列を掛けることによって、カメラを基準にした相対座標に変換される。

# ローカルの頂点座標
LocalPos = [0.0,0.0,0.0])

# Model のワールド座標
WorldPos = [1.0,0.0,3.0]

# カメラポジション
eye = [1.0, 0.0,-3.0]
at  = [1.0,0.0,0.0]
up  = [0.0,1.0,0.0]

この座標を使い View 変換行った結果は下記になる。

[0.0, 0.0, 6.0]

Model が(1.0, 0.0, 3.0)、Eye が(1.0, 0.0, -3.0)のところにあるため、カメラから見た相対座標は Z 軸が 6.0 だけ離れているという事。

カメラの向いている方向

View 行列から、カメラの視線の Direction を取得できる。まず、View 行列の計算式は下記の通り。

zaxis = normal(At - Eye)
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)

 xaxis.x           yaxis.x           zaxis.x          0
 xaxis.y           yaxis.y           zaxis.y          0
 xaxis.z           yaxis.z           zaxis.z          0
-dot(xaxis, eye)  -dot(yaxis, eye)  -dot(zaxis, eye)  1

式を見れば分かるが、zaxisは Eye から見た注視点へのベクトルになる(正規化済み)。

eye = [1.0, 0.0,-3.0]
at  = [1.0,0.0,0.0]

この場合の View Matrix の zaxis は (0.0, 0.0, 1.0) になる。

eye = [1.0, 0.0,3.0]
at  = [1.0,0.0,0.0]

この場合はマイナス軸方向を向くことになるので、zaxis は (0.0, 0.0, -1.0) の値になる。

View Matrix

例1

XMVECTOR Eye = XMVectorSet( 0.0f, 0.0f, 3.0f, 0.0f );
XMVECTOR At = XMVectorSet( 0.0f, 0.0f, 0.0f, 0.0f );
XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
g_View = XMMatrixLookAtLH( Eye, At, Up );

こちらで定義した場合は、下記のような Matrix が作られる。

{-1.0, 0.0, 0.0, 0.0}
{0.0, 1.0, 0.0, 0.0}
{0.0, 0.0, -1.0, 0.0}
{0.0, -0.0, 3.0, 1.0}

例2

XMVECTOR Eye = XMVectorSet( 1.0f, 0.0f, 3.0f, 0.0f );
XMVECTOR At = XMVectorSet( 0.0f, 0.0f, 0.0f, 0.0f );
XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
g_View = XMMatrixLookAtLH( Eye, At, Up );

Eye の X 位置が 1.0f にした状態。

{-0.948, 0.0, -0.316, 0.0}
{0.0, 1.0, 0.0, 0.0}
{0.316, 0.0, -0.948, 0.0}
{-5.96046448e-08, -0.0, 3.16, 1.0}
[DirectX11] Visual Studio 2019 で DirectX11 Sample をビルド

[DirectX11] Visual Studio 2019 で DirectX11 Sample をビルド

LNK2019 のリンクエラー

最新の Visual Studio で DirectX 9 や DirectX 11 の一部サンプルプロジェクトをビルドしようとすると 下記のような LNK2019 のエラーが発生する。

1>dxerr.lib(dxerrw.obj) : error LNK2019: unresolved external symbol __vsnwprintf referenced in function "long __stdcall StringVPrintfWorkerW(unsigned short *,unsigned int,unsigned int *,unsigned short const *,char *)" (?StringVPrintfWorkerW@@YGJPAGIPAIPBGPAD@Z)
1>C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Direct3D11\VarianceShadows11\Debug\VarianceShadows11.exe : fatal error LNK1120: 1 unresolved externals

これを解決するためには、プロジェクト設定から静的ライブラリの追加が必要。

プロジェクトの設定変更

Project を右クリックして「Properties」を選択。

Linker -> Input の Additional Dependencies に下記の lib ファイルを追加する。

legacy_stdio_definitions.lib

これでビルドが通るはず。

[DirectX12] Matrix の Row-major, Culumn-major

[DirectX12] Matrix の Row-major, Culumn-major

Row-major と Culumn-major は、行列のような多次元配列を、どのような順番でメモリーに格納しているかを表す概念である。例えば、下記のような 4×4 行列がある。

a11 a12 a13 a14
a21 a22 a23 a24
a31 a32 a33 a34
a41 a42 a43 a44

Row-major(行順)

メモリー上では a11 a12 a13 a14 a21 a22 … の順番で並んでいる。

C/C++ や Numpy では Row-major でメモリーに格納される。

DirectX などで、Row-major の変換行列(World, View, Projection)をベクトルに掛ける場合は下記のように右に置く。(逆行列も右側に置く)

mul(vector, matrix)

Column-major(列順)

メモリー上では a11 a21 a31 a41 a12 a22 … の順番で並んでいく。

Reference

https://pr0g.github.io/mathematics/matrix/2022/12/26/column-row-major.html

[DirectX12] World, View, Projection マトリックス

[DirectX12] World, View, Projection マトリックス

World 行列

View 行列

XMVECTOR Eye = XMVectorSet( 0.0f, 0.0f, -5.0f, 0.0f );
XMVECTOR At = XMVectorSet( 0.0f, 0.0f, 0.0f, 0.0f );
XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
g_View = XMMatrixLookAtLH( Eye, At, Up );

カメラ位置(座標)と、カメラの見ている場所(注視点)、そしてカメラの上方向のベクトルをXMMatrixLookAtLH に指定することで View 行列が作られる。この行列によって、World 座標から定義した View 座標への変換が行える。

上記の Eye, At, Up から作られる View 行列はこちら。

{1.00, 0.00, 0.00, 0.00}
{0.00, 1.00, 0.00, 0.00}
{0.00, 0.00, 1.00, 0.00}
{0.00, 0.00, 5.00, 1.00} 

計算式は以下。

zaxis = normal(At - Eye)
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)

 xaxis.x           yaxis.x           zaxis.x          0
 xaxis.y           yaxis.y           zaxis.y          0
 xaxis.z           yaxis.z           zaxis.z          0
-dot(xaxis, eye)  -dot(yaxis, eye)  -dot(zaxis, eye)  1
https://learn.microsoft.com/ja-jp/windows/win32/direct3d9/d3dxmatrixlookatlh

計算式を見れば分かるが up ベクトルが必要なのは、カメラからの視線のベクトルと up 方向のベクトルの外積を取って、横方向(xaxis)のベクトルを計算するために使用している。つまり、カメラの3次元での軸を決定するために、上方向がどちらかという情報が必要になる。

Projection 行列

g_Projection = XMMatrixPerspectiveFovLH( XM_PIDIV4, width / (FLOAT)height, 0.01f, 100.0f );
  • XM_PIDIV4 は 0.785398163f(3.14/4)
  • 2つ目はビュースペースのアスペクト比。(640/480=1.333333)
  • 3つ目と、4つ目は NearPlane と FarPlane。(0.01~100.0)

これによって、下記の Projection 行列が得られる。

{1.81066, 0.00, 0.00, 0.00}
{0.00, 2.41421, 0.00, 0.00}
{0.00, 0.00, 1.0001, 1.00}
{0.00, 0.00, -0.01, 0.00}

計算式としては下記。

xScale     0          0               0
0        yScale       0               0
0          0       zf/(zf-zn)         1
0          0       -zn*zf/(zf-zn)     0
where:
yScale = cot(fovY/2)

xScale = yScale / aspect ratio

https://learn.microsoft.com/ja-jp/windows/win32/direct3d9/d3dxmatrixperspectivefovlh

Viewport 行列

640×480 のレンダーターゲットに描画する場合は、Viewport も Width 640, Height 480 に設定する。他は通常はデフォルト値の X, Y 0で、Min 0.0, Max 1.0 になるのが普通。

    dwWidth/2,              0,             0, 0,
            0,    -dwHeight/2,             0, 0,
            0,              0, dwMaxZ-dwMinZ, 0,
dwX+DwWidth/2, dwHeight/2+dwY,        dwMinZ, 1,

https://learn.microsoft.com/ja-jp/windows/win32/direct3d9/viewports-and-clipping

実際は width, height ぐらいしかパラメータは設定しないので、

w = width / 2
h = height / 2

w  0 0 0
0 -h 0 0
0  0 1 0
w  h 0 1

ぐらいの単純な行列になる。

SV_Position の値

頂点シェーダーを使用して、上記のような View, Projection 行列で、ローカル座標 [ -1.0f, 1.0f, -1.0f] を変換した場合はこのような結果になる。

 [-1.81066, 2.41421, 3.9904, 4.00]

頂点シェーダーから SV_Position へ渡している値はこれ。
ラスタライザを通り、ピクセルシェーダーの入力として渡ってくる SV_Position は下記のようになる。

[175.50, 95.50, 0.9976, 4.00]

X, Y はピクセルのスクリーン座標。
Z は Depth Buffer に書かれる値と一致する。頂点シェーダーからの出力 3.9904 / 4.0 = 0.9976 になる。
W は頂点シェーダーでの World, View, Projection の計算結果がそのまま来る。

Reference

https://shikihuiku.github.io/post/projection_matrix/
[DXGI] DirectX グラフィックス インフラストラクチャ

[DXGI] DirectX グラフィックス インフラストラクチャ

レンダリングを行う機能と、レンダリング結果(フレームバッファの内容)を画面に表示する機能は別に分かれている。前者は DirectX と呼ばれ、後者は DXGI (DirectX Graphic Infrastructure) と呼ばれる仕組みが担ってる。

このような機能の分割は、他のグラフィックス API でも同様で、例えば OpenGL であれば、ネイティブの描画部分は EGL という仕組みで独立しており、Vulkan であれば WSI という仕組みが提供されている。

DXGIの呼び出し方

DXGI は DirectX よりも前に初期化が必要。DXGI の生成順序としては、まず Factory を生成し、Factory から PC の Adapter (GPU) を取得。ここで取得した GPU を初期化して、表示用のフレームバッファ (SwapChain) の領域を確保。このフレームバッファに描画したものを Present することによって、SwapChain が画面表示の管理を行ってくれる。

Factory と Adapter の生成

Microsoft::WRL::ComPtr<IDXGIFactory6> dxgiFactory;
CreateDXGIFactory2(dxgiFactoryFlags, MY_IID_PPV_ARGS(&dxgiFactory));

IDXGIFactory を CreateDXGIFactory に渡すことで Factory が作られる。インターフェースや関数名に数字がついているが、これは過去に拡張された事を表している。現在のバージョン番号である。

Factory が作られれば Factory が持つ EnumAdapter などを呼び出して、IDXGIAdapter が取得できる。

取得した IDXGIAdapter を D3D12CreateDevice に渡せば、ここで初めて DirectX が初期化できる。ちなみに Adapter は無くても D3D12CreateDevice は初期化できるが、GPU の情報にアクセス出来るので作っておいた方が無難。

Swapchain

Factory から作られる、もう一つ重要なインターフェースが IDXGISwapChain である。これが無いと、レンダリング結果の表示が行えない。

ComPtr<IDXGISwapChain1> swapChain;
factory->CreateSwapChainForHwnd(
        m_commandQueue.Get(),        // Swap chain needs the queue so that it can force a flush on it.
        Win32Application::GetHwnd(),
        &swapChainDesc,
        nullptr,
        nullptr,
        &swapChain
);

CreateSwapChain の引数には SwapChain Desc を渡す。この内容でフレームバッファが作られる。

DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.Width = m_width;
swapChainDesc.Height = m_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
pChainDesc.SampleDesc.Count = 1;

CreateSwapChain を行ったときに VRAM 上にフレームバッファの領域が確保され、ここで確保したバッファを Render Target として使用することによってピクセルを更新する。

最後に SwapChain の Present メソッドを呼び出すことによって、バッファの内容がモニター等に出力される。

全画面モードや描画サイズの変更

ゲームの全画面表示や、描画解像度の変更などは DXGI を経由して行われる。例えば IDXGIFactory::MakeWindowAssociation を呼び出すことによって、Alt + Enter で全画面表示に移行できる。全画面遷移など、ウインドウが変更された場合 WndProc で WM_SIZE メッセージが来る。このメッセージによって、SwapChain->ResizeBuffers を呼び出せばフレームバッファのサイズ変更が行える。

フレームバッファが変わったら、他の RenderTarget の再生成も行う必要があるので、同じように WM_SIZE メッセージで処理することになる。

その他重要なインターフェース

IDXGIOutput

IDXGISwapChain から取得できる。GetDesc() を呼ぶ出すことによってディスプレイの表示解像度などの情報を取得することが出来る。

ComPtr<IDXGIOutput> pOutput;
ThrowIfFailed(pSwapChain->GetContainingOutput(&pOutput));
DXGI_OUTPUT_DESC Desc;
ThrowIfFailed(pOutput->GetDesc(&Desc));

2022年11月現在、IDXGIOutput6 が最新のインターフェースになっていて、GetDesc1() を呼び出すことで、ディスプレイのカラースペースや表示輝度などの情報をとることが出来る。

ComPtr<IDXGIOutput6> output6;
ThrowIfFailed(pOutput.As(&output6));

DXGI_OUTPUT_DESC1 desc1;
ThrowIfFailed(output6->GetDesc1(&desc1));

HDR に関してはこちらのドキュメントを参考。

過去の DirectX

昔の DirectX では、IDirect3DDevice が Present メソッドを持っていたみたい。DirectX9 までは DirectX API と低レベルの描画機能が一緒になっていたが、DirectX10 から DXGI として分離された。

Reference

https://learn.microsoft.com/ja-jp/windows/win32/direct3ddxgi/dx-graphics-dxgi
https://learn.microsoft.com/ja-jp/windows/uwp/gaming/moving-from-egl-to-dxgi