いまさらDirect3D11入門

初めてグラフィックスAPIを触る人に向けて

タイルリソース

今パートではDX11.2から導入されたタイルリソース(英訳:Tiled Resource)についてみていきます。

タイルリソースはGPU版仮想メモリみたいなもので、GPUのメモリに収まらないほど巨大なデータを扱うときなどに使われます。 もとはメガテクスチャと呼ばれていたりとか、今流行のオープンワールド系のゲームや自動地形生成などで使われているそうです。 が、書いている人自身あまり詳しくないので上の内容が間違っているかもしれません。 ここでは使用法だけについて見ていきます。

タイルリソースはバッファとテクスチャ両方に対応していますので目的にあった方を使用してください。

概要

対応しているプロジェクトはPart17_TiledResourceになります。 タイルリソースはDX11.2に対応しているGPU上でしか利用できません。 そのため一部のGPUではサンプルを実行できない場合がありますがご容赦ください。

タイルリソースの作成

タイルリソースを使う場合に必要となるものはタイルリソースとタイププールの2つになります。 タイルプールがメモリの実態を表し、タイルリソースを介してタイルプールの内容にアクセスします。

まず、タイルリソースの作成について見ていきましょう。 といっても他のリソースとほとんど変わりません。 作成時にMiscFlagsD3D11_RESOURCE_MISC_TILEDを設定するだけです。 リソースを作成した後は必要に応じてビューを作成することも変わりません。

// Scene::onInit関数の一部
//タイルリソースの作成
auto desc = makeTex2DDesc(this->width(), this->height(), DXGI_FORMAT_R8G8B8A8_UNORM);
//タイルリソースとして扱うときは必ずD3D11_RESOURCE_MISC_TILEDを指定すること
desc.MiscFlags = D3D11_RESOURCE_MISC_TILED;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;
//desc.MipLevels = calMaxMipLevel(desc.Width, desc.Height);
//複数のタイルリソースを作成するので、作成処理を1つにまとめた
auto create = [&](TileTexture* pOut) {
  auto hr = this->mpDevice->CreateTexture2D(&desc, nullptr, pOut->mpResource.GetAddressOf());
  if (FAILED(hr)) {
    throw std::runtime_error("タイルリソースの作成に失敗");
  }
  //必要となるビューの作成
  hr = this->mpDevice->CreateShaderResourceView(pOut->mpResource.Get(), nullptr, pOut->mpSRV.GetAddressOf());
  if (FAILED(hr)) {
    throw std::runtime_error("タイルリソースのシェーダリソースビューの作成に失敗");
  }
  hr = this->mpDevice->CreateUnorderedAccessView(pOut->mpResource.Get(), nullptr, pOut->mpUAV.GetAddressOf());
  if (FAILED(hr)) {
    throw std::runtime_error("タイルリソースのアンオーダードアクセスビューの作成に失敗");
  }
};
this->mTextures.resize(6);
for (auto& tex : this->mTextures) {
  create(&tex);
}
create(&this->mTargetTex);

タイルプールの作成

次にタイルプールの作成について見ていきます。

タイルプールはID3D11Bufferとして作成します。 作成の際はMiscFlagsD3D11_RESOURCE_MISC_TILE_POOLを指定した上で、サイズが必ず64KBの倍数になるようにしなければなりません。

タイルプールはタイルと呼ばれるものの集合になり、1タイル当たり64KBのサイズとなります。 タイルリソースを扱う上で、タイルの集合体を操作している事を念頭に置くことが大事です。

// Scene::onInitの一部
//タイルプールの作成
D3D11_BUFFER_DESC desc = {};
desc.ByteWidth = 64 * 1024;//64KBの倍数でないといけない
desc.Usage = D3D11_USAGE_DEFAULT;
desc.MiscFlags = D3D11_RESOURCE_MISC_TILE_POOL;
hr = this->mpDevice->CreateBuffer(&desc, nullptr, this->mpTilePool.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("タイルプールの作成に失敗");
}

//タイルリソースの情報を取得する
//タイルリソースに関係する操作はID3D11Device2やID3D11DeviceContext2を使う必要がある
Microsoft::WRL::ComPtr<ID3D11Device2> pDevice2;
auto hr = this->mpDevice.Get()->QueryInterface(IID_PPV_ARGS(&pDevice2));
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11Device2が使えません。");
}
TileInfo tileInfo;
tileInfo.subresourceTileCount = 1;
tileInfo.firstSubresourceTile = 0;
pDevice2->GetResourceTiling(this->mTargetTex.mpResource.Get(), &tileInfo.tileCount, &tileInfo.packedMipDesc, &tileInfo.tileShape, &tileInfo.subresourceTileCount, tileInfo.firstSubresourceTile, &tileInfo.subresourceTiling);
for (auto& tex : this->mTextures) {
  tex.mInfo = tileInfo;
}
this->mTargetTex.mInfo = tileInfo;
//タイルプールのサイズを必要となるサイズに変更している
UINT64 unitSize = tileInfo.tileCount * (64 * 1024);//タイルリソース1枚あたりのサイズ
hr = this->mpContext2->ResizeTilePool(this->mpTilePool.Get(), unitSize * this->mTextures.size());
if (FAILED(hr)) {
  throw std::runtime_error("タイルプールのリサイズに失敗");
}

タイルプールのリサイズ

タイルプールは作成後ID3D11DeviceContext2::ResizeTilePoolでそのサイズを変更することが出来ます。 変更する場合もそのサイズは64KBの倍数か0でなければなりませんの注意してください。

ちなみにID3D11DeviceContext2ID3D11DeviceContext::QueryInterfaceで取得できます。 詳しい使い方は下のコードを参考にしてください。

ドキュメント:ID3D11DeviceContext2::ResizeTilePool (英語)

//ID3D11DeviceContext2の取得
auto hr = this->mpImmediateContext.Get()->QueryInterface(IID_PPV_ARGS(&mpContext2));
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11DeviceContext2が使えません");
}
//タイルプールのサイズを必要となるサイズに変更している
UINT64 unitSize = tileInfo.tileCount * (64 * 1024);//タイルリソース1枚あたりのサイズ
hr = this->mpContext2->ResizeTilePool(this->mpTilePool.Get(), unitSize * this->mTextures.size());
if (FAILED(hr)) {
  throw std::runtime_error("タイルプールのリサイズに失敗");
}

タイルリソースが使用するタイル数の調べ方

タイルプールのサイズは自由に変更できますが、実際に必要となるサイズがどの程度か把握できればそれに越したことはありません。 そのような場合はタイルリソースの情報を調べるID3D11Device2::GetResourceTilingを使うと便利でしょう。

ID3D11Device2::GetResourceTilingを使うとタイルリソースに必要となるタイルの数や横と縦と奥行きのタイル数、ミップマップに関係する情報を取得できます。 サンプルでは1つのタイルリソースが必要となるタイル数を調べ、使用するタイルリソース分のタイルを確保しています。

ID3D11Device2ID3D11DeviceContext2と同じくID3D11Device::QueryInterfaceで取得できます。

ドキュメント:ID3D11Device2::GetResourceTiling (英語)

//タイルリソースの情報を取得する
//タイルリソースに関係する操作はID3D11Device2やID3D11DeviceContext2を使う必要がある
Microsoft::WRL::ComPtr<ID3D11Device2> pDevice2;
auto hr = this->mpDevice.Get()->QueryInterface(IID_PPV_ARGS(&pDevice2));
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11Device2が使えません。");
}
TileInfo tileInfo;
tileInfo.subresourceTileCount = 1;
tileInfo.firstSubresourceTile = 0;
pDevice2->GetResourceTiling(this->mTargetTex.mpResource.Get(), &tileInfo.tileCount, &tileInfo.packedMipDesc, &tileInfo.tileShape, &tileInfo.subresourceTileCount, tileInfo.firstSubresourceTile, &tileInfo.subresourceTiling);

タイルリソースへタイルプールを紐付け

タイルリソースとタイルプールの両方作成した後はタイルリソースへタイルプールを紐付けます。 紐付け方には直接タイルプールの場所を指定する方法と他のタイルリソースに設定されているものをコピーする方法があります。

直接タイルプールの場所を指定する方法

直接、タイルリソースへタイルプールの場所を指定するにはID3D11DeviceContext2::UpdateTileMappingsを使用します。

ドキュメント:ID3D11DeviceContext2::UpdateTileMappings (英語)

// Scene::onInit関数の一部
D3D11_TEXTURE2D_DESC desc;
this->mTargetTex.mpResource->GetDesc(&desc);
auto& tileInfo = this->mTargetTex.mInfo;
//設定するタイルリソース内の範囲
std::array<D3D11_TILE_REGION_SIZE, 1 > regionSizesInResource;
regionSizesInResource[0].Width = tileInfo.subresourceTiling.WidthInTiles;
regionSizesInResource[0].Height = tileInfo.subresourceTiling.HeightInTiles;
regionSizesInResource[0].Depth = tileInfo.subresourceTiling.DepthInTiles;
regionSizesInResource[0].bUseBox = true;
regionSizesInResource[0].NumTiles = regionSizesInResource[0].Width * regionSizesInResource[0].Height * regionSizesInResource[0].Depth;
//設定するタイルリソース内の開始座標
std::array<D3D11_TILED_RESOURCE_COORDINATE, 1> coordinatesInResource;
coordinatesInResource[0].Subresource = 0;
coordinatesInResource[0].X = 0;
coordinatesInResource[0].Y = 0;
coordinatesInResource[0].Z = 0;

//各タイルリソースにタイププールを割り当てる
for (UINT i = 0; i < this->mTextures.size(); ++i) {
  auto& tex = this->mTextures[i];
  //割り当てるタイルプールの場所の指定
  std::array<UINT, 1> rangeFlags = { { 0 } };
  std::array<UINT, 1> offsets = { { i * tileInfo.tileCount } };
  std::array<UINT, 1> rangeTileCounts = { { tileInfo.tileCount } };

  UINT flags = 0;
  //タイルリソースにタイルプールを設定する
  hr = this->mpContext2->UpdateTileMappings(
    tex.mpResource.Get(), static_cast<UINT>(coordinatesInResource.size()), coordinatesInResource.data(), regionSizesInResource.data(),
    this->mpTilePool.Get(), static_cast<UINT>(rangeFlags.size()), rangeFlags.data(), offsets.data(), rangeTileCounts.data(), flags);
  if (FAILED(hr)) {
    throw std::runtime_error("タイルリソースがさすタイルプールの場所の設定に失敗");
  }
}

//他のタイルリソースが設定されているタイルプールの場所もマップすることもできる
std::array<UINT, 1> rangeFlags = { { 0 } };
std::array<UINT, 1> offsets = { { 0 } };
std::array<UINT, 1> rangeTileCounts = { { tileInfo.tileCount } };

UINT flags = 0;
hr = this->mpContext2->UpdateTileMappings(
  this->mTargetTex.mpResource.Get(), static_cast<UINT>(coordinatesInResource.size()), coordinatesInResource.data(), regionSizesInResource.data(),
  this->mpTilePool.Get(), static_cast<UINT>(rangeFlags.size()), rangeFlags.data(), offsets.data(), rangeTileCounts.data(), flags);
if (FAILED(hr)) {
  throw std::runtime_error("タイルリソースがさすタイルプールの場所の設定に失敗");
}

ID3D11DeviceContext2::UpdateTileMappings

ID3D11DeviceContext2::UpdateTileMappingsの引数は大まかに分けてタイルリソース側とタイルプール側の情報に別れます。 また引数の大半は各々の使用する範囲を指定するものになり、1回の呼び出しで複数の範囲を指定することが可能となっています。 ドキュメントにはより詳細な解説がありますので参考にしてください。

タイルリソース側

  • 第1引数:pTiledResource

    設定するタイルリソース

  • 第2引数:NumTiledResourceRegions

    タイルプールのタイルをタイルリソース内のどの範囲に紐付けるかを表すパラメータの個数。 範囲は第3,4引数で指定し、それぞれ開始座標とサイズを指定します。 第3,4引数はここで指定した数の要素を持つ配列を指定する必要があります。

  • 第3引数:pTiledResourceRegionStartCoordinates

    タイルリソース内の紐付ける場所の開始座標の配列。 D3D11_TILED_RESOURCE_COORDINATEで指定します。 タイルリソースがテクスチャの場合は紐付ける対象となるミップレベルまたは配列の添字も指定する必要があります。

    ドキュメント:D3D11_TILED_RESOURCE_COORDINATE:(英語)

  • 第4引数:pTiledResourceRegionSizes

    タイルリソース内の紐付ける場所の範囲の配列。 D3D11_TILE_REGION_SIZEで指定します。 範囲はバッファなどではタイルの数だけを指定します。 テクスチャの場合はbUseBoxtrueに指定した上で横、縦、奥行きを指定します。

    ドキュメント:D3D11_TILE_REGION_SIZE:(英語)

タイルプール側

  • 第5引数:pTilePool

    紐付け元となるタイルプールを指定します。

  • 第6引数:NumRanges

    紐付けを行うタイルプール内の範囲を表すパラメータの個数。 第7,8,9引数には個々で指定した数分の要素を持つ配列を指定する必要があります。

  • 第7引数:pRangeFlags

    紐付けを行う範囲のフラグを指定します。 D3D11_TILE_RANGE_FLAGで指定します。

    ドキュメント:D3D11_TILE_RANGE_FLAG:(英語)

    1. 何も指定しない(0を指定した時)時:

      pRangeTileCountsで指定した数分のタイルを使用します。

    2. D3D11_TILE_RANGE_REUSE_SINGLE_TILEの時:

      1タイルのみ紐付けられます。 その際、pRangeTileCountsには紐付け先となるタイルリソース側のタイルの個数を指定します。

    3. D3D11_TILE_RANGE_NULLの時:

      紐付け先をNULLにします。

    4. D3D11_TILE_RANGE_SKIPの時:

      pRangeTileCountsで指定した数分、紐付け先のタイルリソースのタイルを飛ばして、紐付け先の状態をそのままに保ちます。 これを指定した時はpTilePoolStartOffsetsの値は無視されます。

  • 第8引数:pTilePoolStartOffsets

    紐付けを行う範囲の開始場所を指定します。 UINTを使いタイル単位で指定します。

  • 第9引数:pRangeTileCounts

    紐付けを行う範囲のサイズを指定します。 UINTを使いタイル単位で指定します。

    pRangeFlagsD3D11_TILE_RANGE_SKIP以外でかつNumRangesが1の場合はnullptrを指定できます。 その際はタイルリソース側で指定した個数分が紐付けられます。

その他

  • 第10引数:Flags

    紐付けを行う際のフラグを指定します。 指定できるのものはD3D11_TILE_MAPPING_FLAGで定義されています。

    ドキュメント:D3D11_TILE_MAPPING_FLAG:(英語)

他のタイルリソースに設定されているものをコピーする方法

他のタイルリソースに設定されているものをコピーするにはID3D11DeviceContext2::CopyTileMappingsを使用します。

ドキュメント:ID3D11DeviceContext2::CopyTileMappings (英語)

// Scene::onKeyUp関数の一部
auto& tileInfo = this->mTargetTex.mInfo;
//設定するタイルリソース内の範囲
std::array<D3D11_TILED_RESOURCE_COORDINATE, 1> coordinates;
coordinates[0].Subresource = 0;
coordinates[0].X = 0;
coordinates[0].Y = 0;
coordinates[0].Z = 0;
//設定するタイルリソース内の開始座標
std::array<D3D11_TILE_REGION_SIZE, 1 > regionSizes;
regionSizes[0].Width = tileInfo.subresourceTiling.WidthInTiles;
regionSizes[0].Height = tileInfo.subresourceTiling.HeightInTiles;
regionSizes[0].Depth = tileInfo.subresourceTiling.DepthInTiles;
regionSizes[0].bUseBox = true;
regionSizes[0].NumTiles = regionSizes[0].Width * regionSizes[0].Height * regionSizes[0].Depth;

UINT flags = 0;
//マップ情報をコピーする
auto hr = this->mpContext2->CopyTileMappings(
  this->mTargetTex.mpResource.Get(), coordinates.data(),
  this->mTextures[this->mTargetIndex].mpResource.Get(), coordinates.data(), regionSizes.data(), flags);
if (FAILED(hr)) {
  throw std::runtime_error("タイルリソースのマッピング情報のコピーに失敗");
}

ID3D11DeviceContext2::CopyTileMappings

  • 第1引数:pDestTiledResource

    コピー先となるタイルリソースを指定します。

  • 第2引数:pDestRegionStartCoordinate

    コピー先の開始座標を指定します。

  • 第3引数:pSourceTiledResource

    コピー元となるタイルリソースを指定します。

  • 第4引数:pSourceRegionStartCoordinate

    コピー元のタイルリソースからコピーする範囲の開始座標を指定します。

  • 第5引数:pTileRegionSize

    コピー元のタイルリソースからコピーする範囲の範囲を指定します。

  • 第6引数:Flags

    紐付けを行う際のフラグを指定します。 指定できるのものはD3D11_TILE_MAPPING_FLAGで定義されています。

    ドキュメント:D3D11_TILE_MAPPING_FLAG:(英語)

このサンプルではタイルリソースの範囲や開始位置を指定する意味がほとんどありません。 なので、タイルリソースは面倒な設定が必要なものだと感じられるかもしれません。 が、サイズが縦横一万を超えるといった巨大なテクスチャを扱う際には必要不可欠となります。 そのような巨大なテクスチャだと全データがGPUのメモリに収まらないので、現在使用している部分のみをGPUのメモリに読み込ませることになります。 タイルリソースはまさにそういったケースに対応するために追加された機能で、範囲や開始位置を指定しているのは当然のことだと言えます。

内容の更新

タイルリソースにタイププールを紐付けが出来ましたら、今までのようにビューを通してシェーダでその内容へアクセスできます。 またID3D11DeviceContext2::UpdateTilesを使用することで、ID3D11DeviceContext::UpdateSubresourceのようにCPUからデータを転送することも出来ます。

ドキュメント:ID3D11DeviceContext2::UpdateTiles (英語)

ID3D11DeviceContext2::UpdateTilesの引数は上で使った関数と似ています。 注意点として、転送するデータはタイル単位に生成することを忘れないで下さい。

また、タイルプールの内容を異なるタイルリソース間で共有しているときはID3D11DeviceContext2::TiledResourceBarrierを使用しアクセスの順序を指定する必要があります。

ドキュメント:ID3D11DeviceContext2::TiledResourceBarrier (英語)

//Scene::onInitの一部
auto& last = * this->mTextures.rbegin();
auto& tileInfo = last.mInfo;
std::vector<uint32_t> src;
src.resize(tileInfo.tileCount * (64 * 1024 / sizeof(uint32_t)));//必ず64KBの倍数のデータ長になるようにする

// ... 転送するデータの生成

std::array<D3D11_TILED_RESOURCE_COORDINATE, 1> coordinates;
coordinates[0].Subresource = 0;
coordinates[0].X = 0;
coordinates[0].Y = 0;
coordinates[0].Z = 0;
std::array<D3D11_TILE_REGION_SIZE, 1 > regionSizes;
regionSizes[0].Width = tileInfo.subresourceTiling.WidthInTiles;
regionSizes[0].Height = tileInfo.subresourceTiling.HeightInTiles;
regionSizes[0].Depth = tileInfo.subresourceTiling.DepthInTiles;
regionSizes[0].bUseBox = true;
regionSizes[0].NumTiles = regionSizes[0].Width * regionSizes[0].Height * regionSizes[0].Depth;
this->mpContext2->UpdateTiles(last.mpResource.Get(), coordinates.data(), regionSizes.data(), src.data(), 0);
//CPUからデータを送り終えたことをGPUに伝える
this->mpContext2->TiledResourceBarrier(last.mpResource.Get(), last.mpResource.Get());

まとめ

このパートではタイルリソースについて見ていきました。 GPUメモリに乗りきらないほど膨大なデータを扱う際に効果を発揮するものになるでしょう。

サンプルコードで正しく使用できているか自信が無いのでMicrosoftが公開しているデモコードを見ることを強く推奨します。 また、サンプルコード作成にはこちらのサイトも参考にしました。そちらも合わせてご覧ください。

補足

タイルリソースに対応しているかの確認の仕方

タイルリソースを使用できるかを確認するにはID3D11Device::CheckFeatureSupportを使用します。 この関数で取得できるものにはいくつかありますが、タイルリソースの場合はD3D11_FEATURE_DATA_D3D11_OPTIONS1を使用します。
ドキュメント:ID3D11Device::CheckFeatureSupport (日本語) (英語)

// Scene::onInitの一部
//タイルリソースに対応しているかチェック
D3D11_FEATURE_DATA_D3D11_OPTIONS1 featureData;
this->mpDevice->CheckFeatureSupport(D3D11_FEATURE_D3D11_OPTIONS1, &featureData, sizeof(featureData));
if (D3D11_TILED_RESOURCES_NOT_SUPPORTED == featureData.TiledResourcesTier) {
  throw std::runtime_error("使用しているデバイスはタイルリソースに対応していません。");
}
    

<前 トップ おわり