DirectStorage1.1を試してみる

# DirectStorageって・・？

昨今のゲームは高精細化が進み、容量も爆増していますが、  
そういった大量のデータを高速にロードするための
Microsoftが提供しているAPIです。  
Windows10,11や、Xboxなどでも使えるそうです。
高速ロードは魅力的ではあるものの、他の描画技術に比べると  
あまり魅力を感じてなかったのですが、ふとしたことからサンプルを見て  
衝撃を受けたので、調べてみました。  
なお、個人で調べたものなので、間違いなどが含まれている可能性がありますので
ご容赦ください。コメントいただければ修正いたします。

https://github.com/microsoft/DirectStorage

## まずはどんなものか
上記のgitにあるBulkLoadDemoサンプルで試してみました。

https://twitter.com/UdonBeats/status/1604849929423241216

適当な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に展開だけするという、一見何も見た目では確認できないですが
最低限のロードのフローを実装してくれているのでとてもわかり易いです。

### 全体的な流れ

<dl>
  <dt>初期化</dt>
  <dd>factoryの初期化を行います</dd>
  <dt>ファイル情報取得</dt>
  <dd>ファイルの実際のサイズを取得</dd>
  <dt>キュー(DStorageQueue)の作成</dt>
  <dd>リクエストを貯めるためのキューを作成します。</dd>
  <dt>ロード先のD3D12Resourceの作成</dt>
  <dd>ロード先となるCommittedResourceを作成します</dd>
  <dt>リクエスト(DStorageRequest)の作成・エンキュー</dt>
  <dd>ファイルとサイズ、D3D12Resourceなどをセットしたリクエストを作成します</dd>
  <dt>完了待ち用のFenceの作成。キューにFenceをセット</dt>
  <dd>描画完了待ちを行うのと同じ、いつものFenceです。</dd>
  <dt>Submitでキューのリクエストを処理依頼</dt>
  <dd>キューをサブミットすることでロードが開始されます</dd>
  <dt>WaitForSingleObjectで完了待ち</dt>
  <dd>完了を待ちます</dd>
  <dd>エラーがなければ完了です。</dd>
</dl>


### 初期化

```cpp:initialize
    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を生成しています。

### ファイルの情報取得
```cpp:get file info
    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)の作成
```cpp: create queue
    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の作成
```cpp:create resource
    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にする必要があるみたいです。

### リクエストの作成
```cpp:create request
    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のバッファとしてリクエストするという
意味合いです。他にも、以下があるようです。
詳細は別記事で調査したいと思います。

```cpp
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を作成
```cpp:create 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でキューのリクエストを処理
```cpp:exec 
    queue->Submit();
```
この命令で、DirectStorageのロード展開処理が行われます。


### WaitForSingleObjectで完了待ち
```cpp: wait finish
    WaitForSingleObject(fenceEvent.get(), INFINITE);

    DSTORAGE_ERROR_RECORD errorRecord{};
    queue->RetrieveErrorRecord(&errorRecord);
    if (FAILED(errorRecord.FirstFailure.HResult))
    {
        ...
    }
```

最後は完了をまち、結果のエラーがないかを確認すればOKです。
シンプルなサンプルで助かります。

## おわりに
サンプルを追ってみただけではありますが、
思ってたより簡単に使えそうです。
次回は、BulkLoadDemoのサンプルを追ったり、パフォーマンス比較など
できればいいかなと思います。

GameEngineDevのアドベントカレンダーに初めて参加してみました。
自作ゲームエンジンに組み込んでみてはいかがでしょうか。
なお、gdeflateの圧縮、展開用のソースも公開されているようなので
他のプラットフォームのgpuでも同じように対応することは可能かと思います。

また、進みは激オソですがYoutubeチャンネルもやっているので
良ければどうぞ。DirectStorage関係も動画を作れたら作ってみたいと
思います。

https://www.youtube.com/@udonbeatsgamedevelopmentlo1135