Chapter 12

ディスクリプタセットの作成

この章ではディスクリプタセットを作成します。ディスクリプタ自体はレイトレ特有のものではないのですが、簡単に整理します。

ディスクリプタについて

ディスクリプタは様々なデータをシェーダから使用できるようにするための概念です。

ディスクリプタという単語はパイプラインの作成時に既に出てきています。詳しく説明しませんでしたが、パイプラインの作成時に出てきた構造体はDescriptorSetLayoutPipelineLayoutなど、Layoutという名前がついていました。Layoutはその名の通り、データのレイアウトだけを指定するもので、データの中身については参照しないものでした。これだけではデータを使えません。

つまり、シェーダからユニフォーム変数を使うためには

  • データ自体
  • データがどのように並んでいるかを示すレイアウト

この2つが必要です。これらは次の図のように対応しています。

図を見れば明らかなように、パイプラインの作成時には右側のレイアウトだけを先に作成したということです。ということで、この章では左側を作成していきます。

大まかな流れはこのようになります。

  1. ディスクリプタプールを作成
  2. ディスクリプタセットを確保
  3. トップレベルアクセラレーション構造のディスクリプタを作成
  4. ストレージイメージのディスクリプタを作成
  5. ディスクリプタセットを更新

ディスクリプタプールを作成

まずはディスクリプタプールとディスクリプタセットをメンバ変数に追加します。

vk::UniqueDescriptorPool descriptorPool;
vk::UniqueDescriptorSet descriptorSet;

いつものように新しい関数を作成して呼び出します。

void initVulkan()
{
    ...

    createDescriptorSets();
}

void createDescriptorSets()
{
    
}

ディスクリプタプールはディスクリプタセットを割り当てるためのメモリ領域のようなものです。ですので、まずはどの程度の大きさが必要なのかを決める必要があります。

std::vector<vk::DescriptorPoolSize> poolSizes = {
    {vk::DescriptorType::eAccelerationStructureKHR, 1},
    {vk::DescriptorType::eStorageImage, 1}
};

今回必要なのは1つのトップレベルアクセラレーション構造と1つのストレージイメージです。

次に実際にディスクリプタプールを作成します。FreeDescriptorSetを指定すると、このプールから作成したディスクリプタセットはプールが破棄される際に自動で解放されるようになります。

descriptorPool = device->createDescriptorPoolUnique(
    vk::DescriptorPoolCreateInfo{}
    .setPoolSizes(poolSizes)
    .setMaxSets(1)
    .setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet)
);

ディスクリプタセットを確保

ディスクリプタセットは複数作ることができますが、今回は1つだけでOKです。allocateDescriptorSetsUnique()には既に作成したディスクリプタセットレイアウトを渡しています。このレイアウトと同じようにディスクリプタセットを作成してくれます。

auto descriptorSets = device->allocateDescriptorSetsUnique(
    vk::DescriptorSetAllocateInfo{}
    .setDescriptorPool(descriptorPool.get())
    .setSetLayouts(descriptorSetLayout.get())
);
descriptorSet = std::move(descriptorSets.front());

この関数は作成するディスクリプタセットが1つだけでも配列の形で結果を返してきます。ですので、配列の先頭要素をstd::moveして取得します。

トップレベルアクセラレーション構造のディスクリプタを作成

ディスクリプタを作成と言っていますが、実際にvk::Descriptorなどという構造体があるわけではないので注意です。コマンドなどで操作する場合はディスクリプタセットが1つの単位となっています。

ここではvk::WriteDescriptorSetという構造体を使います。

vk::WriteDescriptorSetAccelerationStructureKHR descriptorAccelerationStructureInfo{};
descriptorAccelerationStructureInfo
    .setAccelerationStructures(tlas.handle.get());

vk::WriteDescriptorSet accelerationStructureWrite{};
accelerationStructureWrite
    .setDstSet(descriptorSet.get())
    .setDstBinding(0)
    .setDescriptorCount(1)
    .setDescriptorType(vk::DescriptorType::eAccelerationStructureKHR)
    .setPNext(&descriptorAccelerationStructureInfo);

アクセラレーション構造は拡張機能で提供される構造体なので、特別なvk::WriteDescriptorSetAccelerationStructureKHRを先に作成してsetPNext()vk::WriteDescriptorSetの後ろにくっつけています。

ストレージイメージのディスクリプタを作成

レイトレーシングの結果を保存するストレージイメージについてもvk::WriteDescriptorSetを作成します。

vk::DescriptorImageInfo imageDescriptor{};
imageDescriptor
    .setImageView(storageImage.view.get())
    .setImageLayout(vk::ImageLayout::eGeneral);

vk::WriteDescriptorSet resultImageWrite{};
resultImageWrite
    .setDstSet(descriptorSet.get())
    .setDescriptorType(vk::DescriptorType::eStorageImage)
    .setDstBinding(1)
    .setImageInfo(imageDescriptor);

ディスクリプタセットを更新

最後に作成した2つの構造体を渡して、ディスクリプタを更新します。

device->updateDescriptorSets({ accelerationStructureWrite, resultImageWrite }, nullptr);

以上でディスクリプタセットが作成できました。

この記事もそろそろ終盤です。次の章ではコマンドバッファを構築していきます。

ここまでのC++コード(09_create_descriptor_sets.cpp)