🌋

Vulkan 1.3.246の新機能Shader objectを使う方法

2023/05/17に公開

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は既に対応ベータドライバを公開しています。

パフォーマンスの高い順に並べると

  1. Pipeline
  2. Pipeline with dynamic state
  3. Shader object with link
  4. 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);


実際に使って描画してみました(ガビガビなのは圧縮の問題です)

以上です。のちほどサンプルコードも公開するかもしれません。

参考

https://github.com/KhronosGroup/Vulkan-Docs/blob/main/proposals/VK_EXT_shader_object.adoc

Discussion