DirectStorageって・・?
昨今のゲームは高精細化が進み、容量も爆増していますが、
そういった大量のデータを高速にロードするための
Microsoftが提供しているAPIです。
Windows10,11や、Xboxなどでも使えるそうです。
高速ロードは魅力的ではあるものの、他の描画技術に比べると
あまり魅力を感じてなかったのですが、ふとしたことからサンプルを見て
衝撃を受けたので、調べてみました。
なお、個人で調べたものなので、間違いなどが含まれている可能性がありますので
ご容赦ください。コメントいただければ修正いたします。
まずはどんなものか
上記のgitにあるBulkLoadDemoサンプルで試してみました。
適当なgltfファイルを28個、全部で288MBぐらいのデータですが、
これらをランダムで1024個読み込もうとしています。
(ここでは読み込めた数が1008個となっているので1024のうち
メモリが許す範囲でロードしているみたいです)
それらをgpuに展開しています。
また、これらのgltfファイルはサンプルに含まれている
MiniArchiveというツールによって、ひとまとめにされています。
この際、gdeflateという圧縮オプションを使用することで、高速に
gpuにデータを展開しています。
gdeflateはNVIDIAによるGPUに最適化されたデータ圧縮技術だそうです。
https://forest.watch.impress.co.jp/docs/news/1454536.html
DirectStorageの本来の高速化のポイントはシステムメモリにロードしてから
GPUメモリに展開していたものを、直接GPUメモリに展開するため、
無駄が減って早くなるという原理だと思います。
更に、gdeflateで圧縮展開をgpuで行うことで早めてるという事だと
思います。
https://devblogs.microsoft.com/directx/directstorage-1-1-now-available/
HelloDirectStorage
BulkLoadDemoのデモだと、色々とコードが多いので、わかりやすい
サンプルのHelloDirectStorageから読んでみました。
このサンプルは、D3D12Deviceを初期化、DirectStorageの初期化を行って
引数で受け取ったpngのテクスチャファイルを、DirectStorageでロードし、
D3D12Resourceに展開だけするという、一見何も見た目では確認できないですが
最低限のロードのフローを実装してくれているのでとてもわかり易いです。
全体的な流れ
- 初期化
- factoryの初期化を行います
- ファイル情報取得
- ファイルの実際のサイズを取得
- キュー(DStorageQueue)の作成
- リクエストを貯めるためのキューを作成します。
- ロード先のD3D12Resourceの作成
- ロード先となるCommittedResourceを作成します
- リクエスト(DStorageRequest)の作成・エンキュー
- ファイルとサイズ、D3D12Resourceなどをセットしたリクエストを作成します
- 完了待ち用のFenceの作成。キューにFenceをセット
- 描画完了待ちを行うのと同じ、いつものFenceです。
- Submitでキューのリクエストを処理依頼
- キューをサブミットすることでロードが開始されます
- WaitForSingleObjectで完了待ち
- 完了を待ちます
- エラーがなければ完了です。
初期化
com_ptr<ID3D12Device> device;
check_hresult(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&device)));
com_ptr<IDStorageFactory> factory;
check_hresult(DStorageGetFactory(IID_PPV_ARGS(factory.put())));
Device作成は必須です。DirectStorageで使用します。その後に
ここでは、IDStorageFactoryを生成しています。
ファイルの情報取得
com_ptr<IDStorageFile> file;
const wchar_t* fileToLoad = argv[1];
HRESULT hr = factory->OpenFile(fileToLoad, IID_PPV_ARGS(file.put()));
if (FAILED(hr)){...}
BY_HANDLE_FILE_INFORMATION info{};
check_hresult(file->GetFileInformation(&info));
uint32_t fileSize = info.nFileSizeLow;
次に、ファイルサイズの情報をOpenFile()関数で作成した
IDStorageFileから取得します。
ここからがDirectStorageの処理です。
キュー(DStorageQueue)の作成
DSTORAGE_QUEUE_DESC queueDesc{};
queueDesc.Capacity = DSTORAGE_MAX_QUEUE_CAPACITY;
queueDesc.Priority = DSTORAGE_PRIORITY_NORMAL;
queueDesc.SourceType = DSTORAGE_REQUEST_SOURCE_FILE;
queueDesc.Device = device.get();
com_ptr<IDStorageQueue> queue;
check_hresult(factory->CreateQueue(&queueDesc, IID_PPV_ARGS(queue.put())));
最初に作成したデバイスを渡して、
DirectStorageQueueを作成します。
ロード先のD3D12Resourceの作成
D3D12_RESOURCE_DESC bufferDesc = {};
bufferDesc.Width = fileSize;
...
com_ptr<ID3D12Resource> bufferResource;
check_hresult(device->CreateCommittedResource(
&bufferHeapProps,
D3D12_HEAP_FLAG_NONE,
&bufferDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(bufferResource.put())));
このあたりは、いつものリソース作成と変わらないので端折っています。
ただ、初期ステートがD3D12_RESOURCE_STATE_COMMONにする必要があるみたいです。
リクエストの作成
DSTORAGE_REQUEST request = {};
request.Options.SourceType = DSTORAGE_REQUEST_SOURCE_FILE;
request.Options.DestinationType = DSTORAGE_REQUEST_DESTINATION_BUFFER;
request.Source.File.Source = file.get();
request.Source.File.Offset = 0;
request.Source.File.Size = fileSize;
request.UncompressedSize = fileSize;
request.Destination.Buffer.Resource = bufferResource.get();
request.Destination.Buffer.Offset = 0;
request.Destination.Buffer.Size = request.Source.File.Size;
queue->EnqueueRequest(&request);
ファイル、ファイルサイズや、リソースをパラメーターにセットして
キューにリクエストをいれます。
ポイントとしてはDestinationTypeが、ここではDSTORAGE_REQUEST_DESTINATION_BUFFER
となっていますが、これはID3D12Resourceのバッファとしてリクエストするという
意味合いです。他にも、以下があるようです。
詳細は別記事で調査したいと思います。
DSTORAGE_REQUEST_DESTINATION_MEMORY // block of memory
DSTORAGE_REQUEST_DESTINATION_TEXTURE_REGION // texture
DSTORAGE_REQUEST_DESTINATION_MULTIPLE_SUBRESOURCES // subresources
DSTORAGE_REQUEST_DESTINATION_TILES // tile
完了待ち用のFenceを作成
com_ptr<ID3D12Fence> fence;
check_hresult(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence.put())));
ScopedHandle fenceEvent(CreateEvent(nullptr, FALSE, FALSE, nullptr));
constexpr uint64_t fenceValue = 1;
check_hresult(fence->SetEventOnCompletion(fenceValue, fenceEvent.get()));
queue->EnqueueSignal(fence.get(), fenceValue);
ここはいつもの描画待ち用のFenceと変わりないので端折ります。
最後に、キューにフェンスをセットします。
Submitでキューのリクエストを処理
queue->Submit();
この命令で、DirectStorageのロード展開処理が行われます。
WaitForSingleObjectで完了待ち
WaitForSingleObject(fenceEvent.get(), INFINITE);
DSTORAGE_ERROR_RECORD errorRecord{};
queue->RetrieveErrorRecord(&errorRecord);
if (FAILED(errorRecord.FirstFailure.HResult))
{
...
}
最後は完了をまち、結果のエラーがないかを確認すればOKです。
シンプルなサンプルで助かります。
おわりに
サンプルを追ってみただけではありますが、
思ってたより簡単に使えそうです。
次回は、BulkLoadDemoのサンプルを追ったり、パフォーマンス比較など
できればいいかなと思います。
GameEngineDevのアドベントカレンダーに初めて参加してみました。
自作ゲームエンジンに組み込んでみてはいかがでしょうか。
なお、gdeflateの圧縮、展開用のソースも公開されているようなので
他のプラットフォームのgpuでも同じように対応することは可能かと思います。
また、進みは激オソですがYoutubeチャンネルもやっているので
良ければどうぞ。DirectStorage関係も動画を作れたら作ってみたいと
思います。