Vulkanの話 第8回

Compute Shaderを使ってみる 16/12/12 up

Compute Shaderを使って見るサンプルを作成してみたので公開と解説を。

今回もすでにGitHubにコミット済みです。

サンプルは Sample005 となります。

まずはシェーダから。

Sample005/data/test.comp

#version 450#extension GL_ARB_separate_shader_objects : enable#extension GL_ARB_shading_language_420pack : enable layout (local_size_x = 16, local_size_y = 16) in; layout (binding = 0, r11f_g11f_b10f) uniform readonly image2D inputImage; layout (binding = 1) uniform writeonly image2D outputImage; vec4 ConvertColor(in vec4 inputColor) { vec3 irgb = vec3(1.0f) - inputColor.rgb; return vec4(irgb, inputColor.a);}void main(){ // 入力イメージのカラーを取得する vec4 inputColor = imageLoad(inputImage, ivec2(gl_GlobalInvocationID.xy)); // 入力カラーを加工 vec4 result = ConvertColor(inputColor); // 加工したカラーを出力 imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), result);}

やっていることは描画されたバッファのカラーを 1.0f から減算するだけです。

いわゆる色の反転で、これは前回のポストプロセスと同じです。

今回のサンプルは前回のサンプルの途中にCompute Shaderを挟んでいるので、最終結果はSample003と同じになります。遠回りしているだけですね。

GLSLではHLSLの numthreads の代わりに layout で local_size_[xyz] を指定します。

指定されない項目は1で固定になるので、このサンプルはHLSLでは [numthreads(16,16,1)] と書かれているのと同じです。

また、これはGLSLの仕様のようなのですが、サンプラを使用しないテクスチャアクセスをする場合は layout でテクスチャフォーマットを指定する必要があるようです

binding の後にカンマ区切りでフォーマットを指定しています。

このフォーマットが正しくないと、正しくないフォーマットとしてサンプリングしてしまうので結果がおかしくなります。

HLSLでは特に指定する必要がなかったのでちょっと面倒です。対象フォーマットが変更された場合はシェーダ作り直しですね。

ただし、writeonly が指定されている、書き込みのみのテクスチャはフォーマットを指定する必要がないようです。

readonly もしくは何も書かれてない(read & write)のテクスチャのみです。

シェーダの読み込みは他のシェーダと同じなので割愛して、デスクリプタ関連の処理を見ていきましょう。

std::array<vk::DescriptorPoolSize, 3> typeCounts; typeCounts[0].type = vk::DescriptorType::eUniformBuffer; typeCounts[0].descriptorCount = 2; typeCounts[1].type = vk::DescriptorType::eCombinedImageSampler; typeCounts[1].descriptorCount = 3; typeCounts[2].type = vk::DescriptorType::eStorageImage; typeCounts[2].descriptorCount = 2;

まずは DescriptorPool です。

Compute Shaderでテクスチャを読み込む際にはサンプラは使用しません。なので、eCombinedImageSampler は使用できません。

この場合は eStrageImage を使用します。readonly, writeonly, readwriteのどれでもこのタイプを使用します。

std::array<vk::DescriptorSetLayoutBinding, 2> layoutBindings; layoutBindings[0].descriptorType = vk::DescriptorType::eStorageImage; layoutBindings[0].descriptorCount = 1; layoutBindings[0].binding = 0; layoutBindings[0].stageFlags = vk::ShaderStageFlagBits::eCompute; layoutBindings[0].pImmutableSamplers = nullptr; layoutBindings[1].descriptorType = vk::DescriptorType::eStorageImage; layoutBindings[1].descriptorCount = 1; layoutBindings[1].binding = 1; layoutBindings[1].stageFlags = vk::ShaderStageFlagBits::eCompute; layoutBindings[1].pImmutableSamplers = nullptr; vk::DescriptorSetLayoutCreateInfo descriptorLayout; descriptorLayout.bindingCount = static_cast<uint32_t>(layoutBindings.size()); descriptorLayout.pBindings = layoutBindings.data(); descLayouts_.push_back(device.GetDevice().createDescriptorSetLayout(descriptorLayout, nullptr));

DescriptorSetLayout を作成する部分です。

descriptorType と stageFlags を正しく設定すればOKです。

vk::DescriptorImageInfo computeInDescInfo( vk::Sampler(), offscreenBuffer_.GetView(), vk::ImageLayout::eGeneral); vk::DescriptorImageInfo computeOutDescInfo( vk::Sampler(), computeBuffer_.GetView(), vk::ImageLayout::eGeneral);// 略std::array<vk::WriteDescriptorSet, 6> descSetInfos{ // 略 vk::WriteDescriptorSet(descSets_[2], 0, 0, 1, vk::DescriptorType::eStorageImage, &computeInDescInfo, nullptr, nullptr), vk::WriteDescriptorSet(descSets_[2], 1, 0, 1, vk::DescriptorType::eStorageImage, &computeOutDescInfo, nullptr, nullptr),}; device.GetDevice().updateDescriptorSets(descSetInfos, nullptr);

DescriptorSet の作成部分です。

サンプラは不要なので、DescriptorImageInfo のサンプラは空のものを渡します。

ここらへんは指定するタイプが違うというくらいで、通常のグラフィクスパイプを使う場合と大きな変化はありません。

次にパイプラインの生成ですが、グラフィクスパイプと違って設定する項目がほとんどないので、かなり短くて済みます。

bool InitializeComputePipeline(vsl::Device& device){ { vk::PipelineLayoutCreateInfo pPipelineLayoutCreateInfo; pPipelineLayoutCreateInfo.setLayoutCount = 1; pPipelineLayoutCreateInfo.pSetLayouts = &descLayouts_[2]; computePipeLayout_ = device.GetDevice().createPipelineLayout(pPipelineLayoutCreateInfo); } // シェーダステージの設定 vk::PipelineShaderStageCreateInfo shaderInfo(vk::PipelineShaderStageCreateFlags(), vk::ShaderStageFlagBits::eCompute, csTest_.GetModule(), "main"); // パイプライン生成 vk::ComputePipelineCreateInfo pipelineCreateInfo(vk::PipelineCreateFlags(), shaderInfo, computePipeLayout_); computePipeline_ = device.GetDevice().createComputePipeline(device.GetPipelineCache(), pipelineCreateInfo); if (!computePipeline_) { return false; } return true;}

vk::PipelineLayout はグラフィクスパイプと同様の生成を行います。

最終的に作成されるパイプラインは、グラフィクスパイプと同様に vk::Pipeline です。

しかし、生成に使用する情報データは vk::ComputePipelineCreateInfo を使います。

設定するのは先に生成したパイプラインレイアウトとシェーダのみです。なので大変短いコードとなります。

なお、Compute Shaderで使用するテクスチャは、生成時に vk::ImageUsageFlagBits::eStorage を立てて生成します。

フォーマット的に問題がなければ、通常は vk::ImageUsageFlagBits::eSampled と一緒に立てておけばOKでしょう。

最後に実際の描画部分を見てみましょう。

// Dispatch{ cmdBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, computePipeline_); cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, computePipeLayout_, 0, descSets_[2], nullptr); cmdBuffer.dispatch(kScreenWidth / 16, kScreenHeight / 16, 1);}// Compute出力バッファのレイアウト変更{ vk::ImageSubresourceRange colorSubRange; colorSubRange.aspectMask = vk::ImageAspectFlagBits::eColor; colorSubRange.levelCount = 1; colorSubRange.layerCount = 1; computeBuffer_.SetImageLayout(cmdBuffer, vk::ImageLayout::eShaderReadOnlyOptimal, colorSubRange);}

Compute Shaderの起動はやっぱり dispatch() 命令です。

グラフィクスパイプと違って RenderPass を利用しなくていいのが簡単でいいですね。

パイプラインの設定とデスクリプタの設定を行ったらディスパッチして終了なので短くて済みます。

Compute Shader起動後にCompute Shaderの出力先バッファをポストプロセスで使用するためにレイアウトを変更しています。

ただ、この変更を行わなくてもエラーや警告は出なかったので、レイアウト変更はやらなくてもいいかもです。

今回のサンプルはグラフィクスキューでCompute Shaderを行うものなので、非同期コンピュートはやっていません。

非同期コンピュートをやるには別のQueueを作成しなければならないようですが、こちらはそのうちやります。

また、imgui で描画設定を切り替えられるようにしました。

ボタンを押すとCompute Shaderを使用したものと使用しないものを切り替えられます。

DescriptorSet を複数作成するのではなく、アップデートする方法を採用しています。

本来であれば必要な数だけ DescriptorSet を作成すべきだとは思います。

というわけで今回はここまで。