🦀

現状のwindows-rsから利用できるDirect3D12で三角形を描画したかったメモ

16 min read 2

windows-rs

先日Microsoftからwin32metadataによって、c/cpp以外からの言語でもWin32 APIを手軽に利用できるようにするというプロジェクトが立ち上がったのが記憶に新しいです。

さて、本題ですが
今まではRustからpureなDirect3D12を扱うには自分で環境構築する場合を除き、
手動で運用されているwinapi crateを利用することで可能になっていましたが、
先のプロジェクトによってwindows crateからもDirect3D12を使って描画プログラムを
作成する事ができないか、軽い調査のような気持ちで取り組んでみました。

winapiについては、有名どころだとRust製の描画ライブラリであるgfxのbackend layerとしてDirect3D12を扱う際にも利用されています。

結論(2021/02/18時点)

実行環境はこちらです。

Windows 10 Pro 64bit 19041.804
rustc 1.46.0 (04488afe3 2020-08-24)
cargo 1.46.0 (149022b1d 2020-07-17)
Visual Studio Build Tools 2019 16.8.4
windows-rs 0.3.0

先のプロジェクトですが、ロードマップにある通り現状はまだまだpreviewとして利用できるに過ぎません。
そんな中でDirect3D12でHello World!としてシンプルな三角形を描画したい場合、window classなどは除くと
初期化、そして描画フローに必要なinterfaceとして

IDXGIFactory
IDXGIAdapter (一応)
ID3D12Device
ID3D12CommandQueue
IDXGISwapChain
ID3D12DescriptorHeap
ID3D12CommandAllocator
ID3D12RootSignature
ID3D12PipelineState
ID3D12GraphicsCommandList
ID3D12Fence
ID3D12Resource (vertex shader, pixel shaderなど)

あたりが登場します。(抜けてるのもあるかもです)

この内、Pipeline State Objectを作成する際に
CreateGraphicsPipelineState関数の引数としてD3D12_GRAPHICS_PIPELINE_STATE_DESC 構造体を描画したい設定に基づいて初期化しなければなりませんが、
フィールドの値であるrtv_formatsの型がNOT_YET_SUPPORTED_TYPEとなっています。
こちらの型ですが、

https://twitter.com/frozenlib/status/1358303544596566016?s=20
というような編纂もありながら、名前そのままで、ゆくゆくサポートされる型を表すようです。

そんな型が含まれている構造体を引数にとったCreateGraphicsPipelineState関数を実行すると

Exception thrown at 0x00007FFC2010BECF (d3d12SDKLayers.dll) in hello_triangle.exe: 0xC0000005: Access violation reading location 0x000000000000000C.

Undefined behaviorとなってプログラムが終了してしまいました。
上記の関数は勿論ですが、unsafeです。

スタックトレースを見ると、

rtv_formatsunsigned charが渡されてしまっています。

こちらのフィールドは[DXGI_FORMAT; 8]arrayでなければなりませんが
初期化が出来ないようにNOT_YET_SUPPORTED_TYPEガードされています。

というわけで、ID3D12PipelineStateが初期化出来ない以上CPUとシェーダーでのやりとりが出来ないため、現状のwindows-rsではDirect3D12で三角形を描画することは出来ませんでした。

pipelineを設定出来なくともClearRenderTargetViewぐらいは出来るんじゃないのと
思ったのですが、D3D12_RESOURCE_BARRIERunionであるTransitionにあたるフィールドが
anonymusになっていたりで、駄目でした。

winapi-rsとの違い

ここからはDirect3D12の話はちょっと少なめです。

命名規則

関数、構造体に関してはどちらも同じようでした。
ただ、フィールドの命名に関してwinapiは CamelCaseなのに対して(cppと同じです)
windowsはRustの標準仕様に従っていて、 snake_caseとなっていました。
winapiでTypeという名前だった場合はr#typeとかで書くことになりますね。

利用できるAPI

Direct3D12を使うとなると、お持ちのGPUによってはray tracingによる描画プログラム
を作成することも可能な時代になりましたね。
GPUがDirect3D12のray tracingを利用出来るかどうかはD3D12_FEATURE_DATA_D3D12_OPTIONS5でチェックするようです。
winapiでは手動メンテナンスでの難しさ故に現状ではこちらをサポートしていません

一方でwindowsの方には現状でも既に構造体内に確認できます。
他にもraytracingでドキュメントを検索するとそれっぽいものが沢山出てきて
半自動生成の強みが出ています。

主な書き方

winapicppに提供されているAPIに近い設計になっていて

例えばIDXGIFactoryの初期化に際しては

main.rs
use winapi::{
	shared::{
	    winerror,
	    dxgi1_3,
	    dxgi1_6,
	},
	ctypes,
};

fn main() {
	let mut dxfactory = ptr::null_mut::<dxgi1_6::IDXGIFactory6>();
	let result = unsafe {
		dxgi1_3::CreateDXGIFactory2(
		    dxgi1_3::DXGI_CREATE_FACTORY_DEBUG,
		    &dxgi1_6::IDXGIFactory6::uuidof(),
		    &mut dxfactory as *mut *mut dxgi1_6::IDXGIFactory6 as *mut *mut ctypes::c_void
		)
	};

	if result == winerror::S_OK {
	    ...
	}
}

書いたことのある方は覚えていらっしゃるかもですが、上記のようにcppっぽくnull pointerにCOMオブジェクトを詰め込む感じでした。(IID_PPV_ARGS macroが無いのが辛いですね)

これに対してwindowsはよりRustらしい表現になっていて(build.rsについては割愛)

main.rs
use bindings::{
    windows::win32::dxgi as dxgi,
    windows::win32::direct3d12 as direct3d12,
    windows::Interface,
    windows::ErrorCode,
    windows::Error as WinError,
    windows::Abi,
    windows::Result as WinResult,
};

fn main() {
	let result = unsafe {
		let mut dxfactory: Option<dxgi::IDXGIFactory6> = None;
		dxgi::CreateDXGIFactory2(
		    flags,
		    &dxgi::IDXGIFactory6::IID,
		    dxfactory.set_abi()
		)
		.and_some(dxfactory)
	}

	let dxfactory = result?;
	...
}

ErrorCode::and_some(option: Option<T>)optionNoneで無い場合には
Result<T, windows::Error>として返すようになってます。
ちなみに公式example repositoryの書き方ををそのまま踏襲しているだけです。

構造体にDefault traitが実装されている(2021/02/23加筆)

こちらコメント欄にてLNSEAB様からのご指摘があったとおり、
Cargo.tomlのfeaturesにimpl-defaultを指定することによってwinapiに関してもDefaultが利用出来るようになるようでした。
こちらは完全に知らなかったです、失礼致しました。

winapiではDirect3D12で利用する構造体に関してはDefault traitが実装されておらず
先述のD3D12_GRAPHICS_PIPELINE_STATE_DESCなどのように
フィールドが沢山ある構造体をnon-NULLとして初期化したい場合は

use winapi::{
    um::{
        d3d12,
    },
};
use std::mem;
	
let mut pp_state: d3d12::D3D12_GRAPHICS_PIPELINE_STATE_DESC = unsafe { mem::zeroed() };

というようにzero埋めするかMaybeUninitなどを利用するか
という感じで、いちいちunsafeブロックで囲まないといけないみたいなことがありました。

windowsでは半自動ビルドの効力発揮という感じで
おそらくほぼ全ての構造体にDefault traitが実装されているため、
let struct = Struct::default();
という感じでunsafeブロック無しで気軽に初期化出来るようになっています。

COMオブジェクトの管理

winapiではこちらのIUnknownドキュメントに合わせた作りで
IUnknownの実装Interfaceに要求し、Interface traitが各COMオブジェクトに実装されることで
GUIDにアクセスしたり、IUnknownが持つ関数を利用できたりします。
そして気になるCOMオブジェクトのlifetime管理ですが、自分で逐一IUnknown::Release()を呼ぶか
IUnknown::Release()をラップしたDropを実装しておくか
と、cppでraw pointerを扱うような感じになります。
何も管理しない場合にID3D12Debug::EnableDebugLayer()でdebug layerを有効にした状態で描画プログラムを開始し
ウィンドウを終了させた際、以下のようにdebug consoleにリークしているCOMオブジェクトが報告されます。
(d3d12sdklayers::ID3D12DebugDevice::ReportLiveDeviceObjects()を有効にしています)

D3D12 WARNING: Live ID3D12Device at 0x000001E05633E168, Refcount: 32 [ STATE_CREATION WARNING #274: LIVE_DEVICE]
D3D12 WARNING: 	Live ID3D12CommandAllocator at 0x000001E05CF31480, Refcount: 1, IntRef: 2 [ STATE_CREATION WARNING #571: LIVE_COMMANDALLOCATOR]
D3D12 WARNING: 	Live ID3D12GraphicsCommandList at 0x000001E05CF32210, Refcount: 1, IntRef: 0 [ STATE_CREATION WARNING #573: LIVE_COMMANDLIST12]
...

これに対してwindowsではIUnknownはパブリックなフィールドや関数を持たずvtableをラップし、
lifetime管理をしていてInterface traitに実装を要求しており
GUIDにアクセス出来たり、ドキュメントの通りですがQueryInterfaceのような関数を用いずに
一例として、IDXGISwapChain1として生成したCOMオブジェクトからGetCurrentBackBufferIndexを使いたいときにIDXGISwapChain3への安全な型キャストを提供するというような作りになっていると思われます。
こちらのプログラムで先のdebug layerを有効にし、ウィンドウを終了させた際にはdebug consoleに何かしらのリークしているオブジェクトは表示されませんでした。
プログラマによるlifetime管理を基本的に必要とせず、Rustらしいアプリケーションになっています。

現状で辛かった点

ワイルドカードでビルドしようとすると失敗する

ビルド方法に関しては公式の説明exampleを見ながらすすめれば良いかとは思います。
Direct3D12は三角形1個描画しようにも沢山のinterfaceや構造体を利用しなければなりません。
ですので

build.rs
fn main() {
    windows::build!(
        windows::win32::direct3d12::*,
	...
    );
}

のようにワイルドカードでbuildしたかったのですが、現状はまだ完全でなく自分の実行環境では失敗してしまいました。
諦めて必要なものを1個ずつ記述していったところ、成功しました。

VSCodeのrust-analyzerで色々対応出来ていない(2021/02/23加筆)

こちらコメント欄にてLNSEAB様からのご指摘があって確認してみたところ、筆者も公式example通りにbuild.rsに関しては
sub crateとして分けているものの、自分の環境では補完が効かずでした。
rust-analyzerのバージョンも関係しそうな気はしますが、現状ちょっと不安定かもという認識です。

rust-analyzer、挙動自体が怪しい時はあるものの便利なのでRust開発には手放せません。
rust-analyzer v0.2.481において、windowsをビルド後のcreateをuseで利用したものについては
フィールドの値や関数へのアクセスの補完が効きませんでした。
Interface::set_abi()としてApplication Binary Interfaceにアクセスしたいような場面でも
とりあえずタイプして保存しないと関数が存在するのかどうか分かりません。(保存後のチェックは走ります)
anyなJavaScriptに帰ってきた気分です。

型キャストがつらい(2021/02/23加筆)

こちらコメント欄にてLNSEAB様からのご指摘があったとおり、
New Typeとして値にアクセス出来、asでのキャストも可能でした。
ビルドしたcrateを見ても下記のようになっていました。
普通にこの部分も見ていたはずなのに何故気付けなかった、失礼致しました。

impl D3D12_COMMAND_QUEUE_PRIORITY {
    #![allow(non_upper_case_globals)]
    pub const D3D12_COMMAND_QUEUE_PRIORITY_NORMAL: Self = Self(0i32);
    pub const D3D12_COMMAND_QUEUE_PRIORITY_HIGH: Self = Self(100i32);
    pub const D3D12_COMMAND_QUEUE_PRIORITY_GLOBAL_REALTIME: Self = Self(10000i32);
}

全部の構造体がこうなっている訳ではないですが、例えばD3D12_COMMAND_QUEUE_DESCのpriorityというフィールドはi32を要求しつつ実際にはD3D12_COMMAND_QUEUE_PRIORITYのフィールドの値を利用するのが良いと思いますが
i32からinto()asによる型キャストが出来ず

let priority = unsafe { std::mem::transmute::<direct3d12::D3D12_COMMAND_QUEUE_PRIORITY, i32>(direct3d12::D3D12_COMMAND_QUEUE_PRIORITY::D3D12_COMMAND_QUEUE_PRIORITY_NORMAL) };

のように危険な賭けに出るしか無いように思われます。

定数へのアクセスがつらい

先ほどの型キャストの話でも出ていますが
direct3d12::D3D12_COMMAND_QUEUE_PRIORITY::D3D12_COMMAND_QUEUE_PRIORITY_NORMAL
長いです。
これに関しては構造体よりTypeとして型を持たせて
direct3d12::D3D12_COMMAND_QUEUE_PRIORITY_NORMAL
のようにグローバルアクセスしたいです。winapiはこういう表現でした。
名前に関してはユニークに定義されているはずなので、グローバルに定義しても問題ないとは思います。

さいごに

内容の割に長くなってしまいましたが、ざっとこんな感じでした。
先日、Rust Foundationが設立され、メンバーにはMicrosoftも含まれています。
Direct3Dに始まるグラフィックスAPIを長年開発していて、さらにそれを発展させたゲーム事業も抱える同企業は緩やかにc/cppからRustへ移行したいと考えているのかもしれません。
筆者がRust.Tokyo 2019に傍聴者として参加した際は、
組み込み、ブロックチェーン技術などで採用されているなと感じつつ、ゲーム分野で
Rustが採用されるにはライブラリ資産など含めてまだまだ遠いなと思っていましたが、
それももしかしたらそんなに遠くなくなるかもしれませんね。

申し訳無いのですが、筆者はDirect3D12もRustもCGもちょっと独学したぐらいで
普段はWebフロントエンドの端の方でウロウロしているだけなので
的を得てない、もしくは誤りのある記述があるかもしれません。
コメントにてご指摘いただけると幸いです。

Discussion

はじめましてー
rust-analyzerと型キャストがつらい部分について書かせていただきます。

rust-analyzerに関して、windows-rsのGetting startedのままで使おうとすると何かするたびbuild.rsが走ってしまい補完が効かない感じになってしまうようなので、windows-rsのexamplesにあるようにbindingsを分けるとキャッシュされて補完されやすくなると思います。

型キャストがつらい部分に関して、D3D12_COMMAND_QUEUE_PRIORITYのように元々Cのenumで定義されている定数はニュータイプパターンになっていて、D3D12_COMMAND_QUEUE_PRIORITY::D3D12_COMMAND_QUEUE_PRIORITY_NORMAL.0のように.0でフィールドにアクセスできます。.0の型はi32なのでasも使えて楽になるはずです。

余談ですが、winapi-rsのfeaturesに"impl-default"を入れると構造体にDefaultが実装されてDefaultでゼロクリアができるようになります。

はじめまして、ご指摘ありがとうございます!

まだまだ浅いところが露呈してしまいました。。

検証の後、記事に反映させていただきます!

ログインするとコメントできます