Vulkan 1.3.246の新機能Shader objectを使う方法
Shader objectとは
Shader objectはVulkan 1.3.246から新しく追加された新機能です。
従来はVertex/Fragment shaderのペアと描画に関するstateを事前にpipelineとしてまとめて作成しておき、描画時にはそれをバインドすることで設定していました。しかし、shaderの組み合わせとstateごとにpipelineを作成しておくのが大変という問題がありました。そこで、pipelineを使わずに、各shaderを動的にバインドし、すべてのstateも動的に設定できるようにするための機能がShader objectです。
パフォーマンスについて
事前にshaderの組合せやstateが分かっていた方がドライバ側で最適化しやすいため、基本的にはpipelineによる描画が最高のパフォーマンスであるとされています。ある意味shader object拡張機能はVulkanをOpenGLに戻してしまうようなAPIになっていますが、実際にはpipelineによる描画と大差ないパフォーマンスが出せるようにベンダーが努力しているようです。NVIDIAは既に対応ベータドライバを公開しています。
パフォーマンスの高い順に並べると
- Pipeline
- Pipeline with dynamic state
- Shader object with link
- Shader object without link
となると思います。
Pipelineとの比較
簡単な疑似コードで示すとこのようになります。
従来のpipelineによる描画
Pipeline pipeline;
void init()
{
std::string vertexShaderCode = ...;
std::string fragmentShaderCode = ...;
pipeline = Pipeline{
.vertexShaderCode = vertexShaderCode,
.fragmentShaderCode = fragmentShaderCode,
.viewport = {...},
.scissor = {...},
.polygonMode = {...},
.frontFace = {...},
.cullMode = {...},
// ...
};
}
void render()
{
// ここではshaderやstateを変えられない
commandBuffer.bindPipeline(pipeline);
commandBuffer.draw(...);
}
新しいshader objectによる描画
Shader vertexShader;
Shader fragmentShader;
void init()
{
// pipelineの代わりにshader objectを作成する
std::string vertexShaderCode = ...;
std::string fragmentShaderCode = ...;
vertexShader = Shader{.code = vertexShaderCode};
fragmentShader = Shader{.code = fragmentShaderCode};
}
void render()
{
// shaderを動的に組み合わせられる
commandBuffer.bindShader(vertexShader);
commandBuffer.bindShader(fragmentShader);
// stateを動的に設定できる
commandBuffer.setViewport(...);
commandBuffer.setScissor(...);
commandBuffer.setPolygonMode(...);
commandBuffer.setFrontFace(...);
commandBuffer.setCullMode(...);
commandBuffer.draw(...);
}
具体的なVulkanコード
1. 拡張機能を追加
std::vector<const char*> deviceExtensions = {...};
deviceExtensions.push_back(VK_EXT_SHADER_OBJECT_EXTENSION_NAME);
2. Featureを追加
vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjectFeatures{true};
3. Shader objectを作成
Shader objectは各shaderごとに別々に作ることもできますし、Vertex/Fragmentのペアとしてリンクしてまとめて作ることもできます。リンクした場合は動的に組み合わせられなくなりますが、ドライバ側で最適化できる可能性があります。ここではリンクする方法で紹介します。
vk::ShaderCreateInfoEXT shaderInfo[2] = {
vk::ShaderCreateInfoEXT()
.setFlags(vk::ShaderCreateFlagBitsEXT::eLinkStage)
.setStage(vk::ShaderStageFlagBits::eVertex)
.setNextStage(vk::ShaderStageFlagBits::eFragment)
.setCodeType(vk::ShaderCodeTypeEXT::eSpirv)
.setCodeSize(vertSpvCode.size() * sizeof(uint32_t))
.setPCode(vertSpvCode.data())
.setPName("main")
.setSetLayouts(setLayout),
vk::ShaderCreateInfoEXT()
.setFlags(vk::ShaderCreateFlagBitsEXT::eLinkStage)
.setStage(vk::ShaderStageFlagBits::eFragment)
.setCodeType(vk::ShaderCodeTypeEXT::eSpirv)
.setCodeSize(fragSpvCode.size() * sizeof(uint32_t))
.setPCode(fragSpvCode.data())
.setPName("main")
.setSetLayouts(setLayout)
};
shaders = device.createShadersEXT(shaderInfo);
これだけで準備完了です。とてもシンプルですね。
4. 描画する
描画時にはShaderをバインドし、必要なstateを設定する必要があります。
// Shaderをバインド
std::vector<vk::ShaderStageFlagBits> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment,
};
commandBuffer.bindShadersEXT(stages, shaders);
// Stateを設定
commandBuffer.setViewport(width, height);
commandBuffer.setScissor(width, height);
// 描画
commandBuffer.draw(3, 1, 0, 0);
実際に使って描画してみました(ガビガビなのは圧縮の問題です)
以上です。のちほどサンプルコードも公開するかもしれません。
参考
Discussion