Skip to main content

SPIR-V Shader Binary Converter

·691 words·4 mins
Code Vulkan C/Cpp
Table of Contents

To accomplish this, the program reads in the generated SPIR-V binary, and outputs a text file containing the data, but in 4 byte chunks, so that it can be used as if it were a uint32 array.

The Shader
#

Below is the shader that we want to embed into the source code of our demo program, it’s a simple vertex shader, taking in uniform buffer binding and performing basic transformation around a triangle.

// camera_ubo.vert
#version 450
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable

layout (binding = 0) uniform UBO
{
    mat4 projectionViewMatrix;
} ubo;

layout (location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

void main()
{
    gl_Position = ubo.projectionViewMatrix * vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

The Converter
#

What the converter does is open the targeted binary file generated byt he glsllangvalidator run earlier, and checks a few basic. It checks that the file is not empty, and the content is a multiple of 4. This is because the size of an instruction in the SPIR-V is 4 bytes, thus a valid file must also be a multiple of 4 bytes. After this point, it then gathers the full size, reads the data, and closes the source file.

    // Open the file.
    FILE* fp = fopen(argv[argc - 1], "rb");
    if (!fp) {
        printf("assimpbinaryconverter: could not open shader file: %s", argv[1]);
        return 0;
    }

    // Read the file.
    fseek(fp, 0L, SEEK_END);
    unsigned int byteSize = static_cast<unsigned int>(ftell(fp));
    if (byteSize == 0) {
        printf("File is empty.");
        return 0;
    }
    if (byteSize % 4 != 0) {
        printf("assimpbinaryconverter: file content is not multiple of 4.");
        return 0;
    }

    // Read the data in.
    fseek(fp, 0L, SEEK_SET);
    std::unique_ptr<uint32_t[]> data(new uint32_t[byteSize / 4]);
    if (fread(data.get(), byteSize, 1, fp) != 1) {
        printf("assimpbinaryconverter: error reading file.");
        return 0;
    }
    // Close the file
    fclose(fp);

With all of the data well in hand, the program then opens the output file, and converts, 4 bytes a time, and outputs a text version of the 4-byte chunk.

    // Open the output file.
    fp = fopen(outFileName.c_str(), "w");

    int currentWidth = 0;
    for (unsigned int i = 0; i < byteSize / 4; ++i) {
        std::stringstream ss;
        // Convert the 4-bytes into a 0x00000000 hexadecimal representation.
        ss << "0x" << std::hex << std::setw(sizeof(uint32_t) * 8 / 4) << std::uppercase << std::setfill('0') << data[i];
        // Write it out.
        fwrite(ss.str().c_str(), ss.str().size(), 1, fp);
        // Add a comma and space.
        fwrite(", ", 2, 1, fp);
        ++currentWidth;

        if (currentWidth == itemsWide) {
            currentWidth = 0;
            fwrite("\n", 1, 1, fp);
        }
    }
    fclose(fp);

The Result
#

With that accomplished, one would be left with a file looking like this:

0x07230203, 0x00010000, 0x00080001, 0x0000003E, 0x00000000, 0x00020011, 0x00000001, 0x0006000B, 
0x00000001, 0x4C534C47, 0x6474732E, 0x3035342E, 0x00000000, 0x0003000E, 0x00000000, 0x00000001, 
0x0008000F, 0x00000000, 0x00000004, 0x6E69616D, 0x00000000, 0x00000022, 0x0000002D, 0x00000039, 
0x00030003, 0x00000002, 0x000001C2, 0x00090004, 0x415F4C47, 0x735F4252, 0x72617065, 0x5F657461, 
0x64616873, 0x6F5F7265, 0x63656A62, 0x00007374, 0x00090004, 0x415F4C47, 0x735F4252, 0x69646168, 
0x6C5F676E, 0x75676E61, 0x5F656761, 0x70303234, 0x006B6361, 0x00040005, 0x00000004, 0x6E69616D, 
0x00000000, 0x00050005, 0x0000000C, 0x69736F70, 0x6E6F6974, 0x00000073, 0x00040005, 0x00000017, 
0x6F6C6F63, 0x00007372, 0x00060005, 0x00000020, 0x505F6C67, 0x65567265, 0x78657472, 0x00000000, 
0x00060006, 0x00000020, 0x00000000, 0x505F6C67, 0x7469736F, 0x006E6F69, 0x00070006, 0x00000020,
...

Which can then be embedded into a program’s source like so:

const uint32_t vertexBinary[] = 
{
    0x07230203, 0x00010000, 0x00080001, 0x0000003E, 0x00000000, 0x00020011, 0x00000001, 0x0006000B, 
    0x00000001, 0x4C534C47, 0x6474732E, 0x3035342E, 0x00000000, 0x0003000E, 0x00000000, 0x00000001, 
    0x0008000F, 0x00000000, 0x00000004, 0x6E69616D, 0x00000000, 0x00000022, 0x0000002D, 0x00000039, 
    0x00030003, 0x00000002, 0x000001C2, 0x00090004, 0x415F4C47, 0x735F4252, 0x72617065, 0x5F657461, 
    0x64616873, 0x6F5F7265, 0x63656A62, 0x00007374, 0x00090004, 0x415F4C47, 0x735F4252, 0x69646168, 
    0x6C5F676E, 0x75676E61, 0x5F656761, 0x70303234, 0x006B6361, 0x00040005, 0x00000004, 0x6E69616D, 
    0x00000000, 0x00050005, 0x0000000C, 0x69736F70, 0x6E6F6974, 0x00000073, 0x00040005, 0x00000017, 
    0x6F6C6F63, 0x00007372, 0x00060005, 0x00000020, 0x505F6C67, 0x65567265, 0x78657472, 0x00000000, 
    0x00060006, 0x00000020, 0x00000000, 0x505F6C67, 0x7469736F, 0x006E6F69, 0x00070006, 0x00000020,
    ...
};

// Use the array of the compiled shader to create a shader module
vk::ShaderModuleCreateInfo moduleCI;
moduleCI.pCode = vertexBinary;
moduleCI.codeSize = sizeof(vertexBinary);

vk::Result err = m_device.createShaderModule(&moduleCI,
                                             nullptr,
                                             &m_shaderModules[(uint32_t) vk::ShaderStageFlagBits::eVertex]);

Source Code
#

StableCoder/vksbc

Program that takes in a Vulkan shader SPIR-V program and converts it to uint32_t’s that can be used directly in the source code of a program. Can also generate C/C++ headers directly.

C++
2
0