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 #
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.