読者です 読者をやめる 読者になる 読者になる

Vulkanでシェーダリフレクション(Shader Reflection)を取得してみる

SEGA 画像処理 Vulkan Shader

みなさん初めまして。工藤@セガゲームス開発技術部です。

社内ライブラリを開発する仕事を長年しています。これまでゲーム機のSDKやDirectX, OpenGLなどのグラフィックスAPIを使い、グラフィックスライブラリを作成してきました。最近のAPIとしてはDirectX12, Metal等がありますが、昨年にはさらにVulkanがリリースされました。VulkanはKhronosグループが策定しているマルチプラットフォーム向けグラフィックスAPIです。


情報が乏しく複雑怪奇なこのVulkanには苦戦させられています。Vulkanがリリースされてから一年が経ち、昨年秋には赤本(Vulkan Programming Guide)が発売されたり、GDC2017(Game Developers Conference 2017)での発表があるなど、やっと情報が増えてきました。みなさんはいかがでしょうか。


最近はゲームエンジンを使う機会が増え、ローレベル(低階層)のグラフィックスAPIを直接使う人の数も減っていると思います。ここでVulkanの情報を発信しても役立つ人がどれだけいるのかわからないような状況ですが、今回のブログはこのVulkanでのシェーダリフレクションの使い方について取り上げたいと思います。
※「シェーダリフレクション」とはシェーダの中にある変数の情報を取得することです。


Vulkanでシェーダを使うには一般的にシェーダ言語にGLSLを使用しSPIR-Vへ変換して使用します。SPIR-VはVulkanで導入されたシェーダの中間言語です。HLSLからSPIR-Vへの変換なども今後は対応していくようです。


GLSLの使い方はOpenGLで使用していた場合とほとんど同じですがVulkan用にキーワードが追加されています。Vulkan用に追加されたキーワードにはシェーダへユニフォームバッファやサンプラなどのリソースをバインドするために必要なsetとbindingがあります。この値は上位ライブラリを実装するときに必要になりますが、この値を取得する関数はVulkan SDKには用意されていません。

前置きが長くなりましたが、今回はVulkanで追加されたsetとbindingの値をシェーダからと取得したいと思います。


目次

VulkanでのGLSLの例を見てみよう

まずVulkanでリフレクションに触れる前に簡単なシェーダの例を見てみましょう。

#version 450 core
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable
layout(set = 0, binding = 0) uniform buf {
        mat4 MVP;
        vec4 position[12*3];
        vec4 attr[12*3];
} ubuf;
void main() 
{
    // Do nothing!
}

このシェーダではuniform bufにset = 0とbinding = 0が設定されています。この値がVulkan SDKからユニフォームバッファへリソースをバインドするときに必要になります。set、bindingがない場合はset、bindingとも0として扱われます。Vulkan SDKへのdescriptorsetとbindingへの設定は話が長くなりますので今回は説明しません。uniform bufにはmat4のMVP変数、vec4のpositionの36個の配列、vec4のattrの36個の配列がメンバーにいます。

GLSLをロードしSPIR-Vへ変換する

ゲーム開発中で絵作りがなかなか決まらない場合、何度もシェーダを書き換える必要が出てきます。ゲーム中でGLSLをSPIR-Vへ変換できるとゲームを一旦終了することなしにシェーダを書き換えた時点でシェーダを切り替えることが可能になります。開発終盤になりシェーダが確定したら事前コンパイルしたSPIR-Vを直接ロードして使用します。GLSLをglslangを使用してSPIR-Vへ変換します。glslangはValkan SDKに入っています。

bool ShaderReflection::GLSLtoSPV(const vk::ShaderStageFlagBits shader_type, const char *pshader, std::vector<uint32_t> &spirv)
{
    glslang::InitializeProcess();
    glslang::TProgram &program = *new glslang::TProgram;
    const char *shaderStrings[1];
    TBuiltInResource Resources;
    init_resources(Resources);

    EShLanguage stage = FindLanguage(shader_type);
    glslang::TShader *shader = new glslang::TShader(stage);

    shaderStrings[0] = pshader;
    shader->setStrings(shaderStrings, 1);

    EShMessages messages = (EShMessages)(EShMsgSpvRules | EShMsgVulkanRules);
    if (!shader->parse(&Resources, SHADER_VERSION, false, messages)) {
        delete &program;
        delete shader;
        return false;
    }

    program.addShader(shader);

    if (!program.link(messages)) {
        delete &program;
        delete shader;
        return false;
    }

    glslang::GlslangToSpv(*program.getIntermediate(stage), spirv);
    glslang::FinalizeProcess();

    delete &program;
    delete shader;
    return true;
}

glslangにリフレクションがあるけど使えないの?

先ほどGLSLからSPIR-Vへ変換にglslangを使いました。glslangを見ていますとglslang\glslang\MachineIndependentにreflection.hとreflection.cppがあります。結論から言えば変数名や型、サイズなどは取得できます。ですがVulkanで追加された今回の目的であるsetやbindingの値を取得することができません。ソース提供されているので改造すれば取得できるようになるかもしれませんし、今後対応されるかもしれません。

SPIRV-Crossを使用してリフレクションを取得する

glslangを使用してsetとbindingが取得できないので次の一手を探しました。SPIR-Vからsetとbinding値をとることができないものかとSPIRV-Cross-masterを入手してソースコードを眺めていたところ、spirv_cross.hppにspirv_cross::Compilerクラスを発見。get_nameやget_typeなどの関数の他にメンバー変数にはsetやbindingもあったのでSPIRV-Crossを使用してみます。ユニフォームバッファのメンバーも取得したいのでメンバー数とメンバータイプを取得する関数を派生クラスDemoCompilerクラスを作成しました。

class DemoCompiler :public spirv_cross::Compiler
{
public:
    DemoCompiler(std::vector<uint32_t>& ir) :Compiler(ir) {}
    virtual ~DemoCompiler() {};

    size_t get_member_count(uint32_t id) const
    {
        const spirv_cross::Meta &m = meta.at(id);
        return m.members.size();
    }

    spirv_cross::SPIRType get_member_type(const spirv_cross::SPIRType &struct_type, uint32_t index) const
    {
        return get<spirv_cross::SPIRType>(struct_type.member_types[index]);
    }
};


取得した情報を入れる構造体を2つ定義します。UniformInfo構造体とBufferInfo構造体です。SPIR-Vから取得した情報をこれらの構造体へ入れていきます。

typedef struct
{
    spirv_cross::SPIRType::BaseType baseType;
    std::string     name;     // ユニフォーム名
    size_t          bytesize; // バイトサイズ
    int             arraysize;// 配列数
    int             offset;   // バッファ先頭からのオフセット
}  UniformInfo;

typedef struct
{
    spirv_cross::SPIRType::BaseType baseType;   // Struct, Image, SampledImage,Samplerなど
    std::string     name;         // バッファ名
    size_t          bytesize;     // バッファバイトサイズ
    int             arraysize;    // 配列数 ない場合は0
    int             offset;       // バッファ先頭からのオフセット
    int             descriptorSet;// descriptorSetID
    int             binding;      // bindingID
    std::vector<UniformInfo> uniformInfos;
} BufferInfo;

spirv_cross::Compilerクラスに入った情報はspirv_cross::Resourceのvectorで定義されている各変数へ入ります。getReflection関数を作成しDemoCompilerから情報を収集します。初めにユニフォームバッファ情報やサンプラ情報をBufferInfo構造体へ収集し、ユニフォームバッファの場合メンバー変数の情報もUniformInfo構造体を使用して収集します。今回のブログの目的であるsetとbindingの値はcomp.get_decorationの第2引数をspv::DecorationDescriptorSetとspv::DecorationBindingにすることで取得することができます。各値の取得方法についてはspirv_cross.cppにソースコードがあるので参考にしてください。

void ShaderReflection::getReflection(const std::vector<spirv_cross::Resource> &resources, const DemoCompiler &comp, std::vector<BufferInfo> &bufferinfos)
{
    using namespace spirv_cross;

    for ( const auto& resource : resources)
    {
        const SPIRType spirv_type = comp.get_type(resource.type_id);

        BufferInfo binfo;
        binfo.baseType      = spirv_type.basetype;
        binfo.name          = resource.name.c_str();
        binfo.offset        = comp.get_decoration(resource.id, spv::DecorationOffset);
        binfo.arraysize     = spirv_type.array.empty() ? 0 : spirv_type.array[0];
        binfo.bytesize      = spirv_type.basetype == SPIRType::Struct ? comp.get_declared_struct_size(spirv_type) : 0;
        binfo.descriptorSet = comp.get_decoration(resource.id, spv::DecorationDescriptorSet);
        binfo.binding       = comp.get_decoration(resource.id, spv::DecorationBinding);

        size_t num_value = comp.get_member_count(resource.base_type_id);
        for (uint32_t index = 0; index < num_value; ++index)
        {
            const SPIRType &member_type = comp.get_member_type(spirv_type, index);

            UniformInfo uinfo;
            uinfo.baseType  = member_type.basetype;
            uinfo.name      = comp.get_member_name(resource.base_type_id, index).c_str();
            uinfo.bytesize  = comp.get_declared_struct_member_size(spirv_type, index);
            uinfo.offset    = comp.get_member_decoration(resource.base_type_id, index, spv::DecorationOffset);
            uinfo.arraysize = member_type.array.empty() ? 0 : member_type.array[0];
            binfo.uniformInfos.push_back(uinfo);
        }
        bufferinfos.push_back(binfo);
    }
}

次に作成した関数を使用してSPIR-Vから情報を収集します。DemoCompilerを作成しresourcesを取得しuniform_buffers、sampled_images、separate_images、separate_samplersからリフレクションを収集します。

void ShaderReflection::getSPVtoReflection( const void *pBinSPV, size_t BinSPVBytes)
{
    using namespace spirv_cross;
    std::vector<uint32_t> spirv_binary;
    spirv_binary.resize(align4(BinSPVBytes) / sizeof(uint32_t));
    memcpy(spirv_binary.data(), pBinSPV, BinSPVBytes);
    DemoCompiler comp(spirv_binary);
    ShaderResources resources = comp.get_shader_resources();
    //uniform_buffersから情報取得
    getReflection(resources.uniform_buffers, comp, m_bufferuniform_info);
    //sampled_imagesから情報取得
    getReflection(resources.sampled_images, comp, m_sampleruniform_info);
    //separate_imagesから情報取得
    getReflection(resources.separate_images, comp, m_sampleruniform_info);
    //separate_samplersから情報取得
    getReflection(resources.separate_samplers, comp, m_sampleruniform_info);
}

収集した情報を標準出力へ表示するサンプル関数です。

void ShaderReflection::printReflection(const std::vector<BufferInfo> &bufferInfos )
{
    const char *BaseTypeNmae[] =
    {
        "Unknown",  "Void", "Boolean",  "Char", "Int",  "UInt", "Int64","UInt64",
        "AtomicCounter","Float","Double","Struct","Image","SampledImage","Sampler"  
    };

    for ( const auto& binfo : bufferInfos)
    {
        std::cout << "name         :" << binfo.name << std::endl;
        std::cout << "baseType     :" << BaseTypeNmae[binfo.baseType] << std::endl;
        std::cout << "size         :" << binfo.bytesize << " Byte" << std::endl;
        std::cout << "arraysize    :" << binfo.arraysize << std::endl;
        std::cout << "offset       :" << binfo.offset << std::endl;
        std::cout << "descriptorSet:" << binfo.descriptorSet << std::endl;
        std::cout << "binding      :" << binfo.binding << std::endl;

        if (!binfo.uniformInfos.empty())
        {
            std::cout << "uniformInfo" << std::endl;
            for ( const auto& uinfo : binfo.uniformInfos)
            {
                std::cout << "    name         :" << uinfo.name << std::endl;
                std::cout << "    baseType     :" << BaseTypeNmae[uinfo.baseType] << std::endl;
                std::cout << "    size         :" << uinfo.bytesize << " Byte" << std::endl;
                std::cout << "    arraysize    :" << uinfo.arraysize << std::endl;
                std::cout << "    offset       :" << uinfo.offset << std::endl << std::endl;
            }
        }
        std::cout << std::endl;
    }
}

実験

test.fragからリフレクションを取得し出力してみます。テスト用のシェーダですのでシェーダ内容には特に意味はありません。

#version 400
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable

layout(std140, set = 0, binding = 0) uniform buf {
        mat4    MVP;
        vec4    position[12*3];
        vec4    attr[12*3];
        float   f1_val;
        vec2    f2_val;
        vec3    f3_val;
        bool    bool_val;
} ubuf;

layout (set = 0, binding = 1) uniform sampler2D samp2d;
layout (set = 1, binding = 0) uniform texture2D tex2d;
layout (set = 1, binding = 5) uniform sampler samp;

layout (location = 0) in vec4 texcoord;
layout (location = 0) out vec4 uFragColor;

void main() {
   uFragColor = texture(samp2d, texcoord.xy);
}

サンプルを実行結果は以下のとおりになります。

---------------------------------------------
filename     :shader/test.frag
---------------------------------------------
name         :buf
baseType     :Struct
size         :1248 Byte
arraysize    :0
offset       :0
descriptorSet:0
binding      :0
uniformInfo
    name         :MVP
    baseType     :Float
    size         :64 Byte
    arraysize    :0
    offset       :0

    name         :position
    baseType     :Float
    size         :576 Byte
    arraysize    :36
    offset       :64

    name         :attr
    baseType     :Float
    size         :576 Byte
    arraysize    :36
    offset       :640

    name         :f1_val
    baseType     :Float
    size         :4 Byte
    arraysize    :0
    offset       :1216

    name         :f2_val
    baseType     :Float
    size         :8 Byte
    arraysize    :0
    offset       :1224

    name         :f3_val
    baseType     :Float
    size         :12 Byte
    arraysize    :0
    offset       :1232

    name         :bool_val
    baseType     :UInt
    size         :4 Byte
    arraysize    :0
    offset       :1244


name         :samp2d
baseType     :SampledImage
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:0
binding      :1

name         :tex2d
baseType     :Image
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:1
binding      :0

name         :samp
baseType     :Sampler
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:1
binding      :5

push any key

まとめ

Vulkanでのシェーダリフレクションを取得をやってみましたがいかがだったでしょうか。今回はspirv_cross::Compilerクラスを使うことでシェーダの必要なデータを簡単に取得することができました。すぐに導入することができますのでVulkanを使って上位ライブラリを作る方の助けになれば幸いです。

このような取り組みにも積極的な方と一緒に働きたいと考えています。もしご興味を持たれましたら、弊社グループ採用サイトをご確認ください。
採用情報 | セガグループ


それでは次回の更新をお楽しみに。