Jump to content

Single-file SPIR-V shaders for Vulkan



It is now possible to compile shaders into a single self-contained file that can loaded by any Vulkan program, but it's not obvious how this is done. After poking around for a while I found all the pieces I needed to put this together.


First, you need to compile each shader stage from a source code file into a precompiled SPIR-V file. There are several tools available to do this, but I prefer GLSlangValidator because it supports the Google #include extension. Put your vertex shader code in a text file named "shader.vert" and your pixel shader code in a file called "shader.frag". Create a .bat file in the same directory with the following contents:

glslangValidator.exe "shader.vert" -V -o "vert.spv"
glslangValidator.exe "shader.frag" -V -o "frag.spv"

Run the bat file and two .spv files will be saved.


Now we want to combine our two files representing different shader stages into a single file. This is done with the link tool from Khronos. Add the following lines to your .bat file to compile the two .spv files into one. It will also delete the existing files to clean things up a little:

spirv-link "vert.spv" "frag.spv" -o "shader.spv"
del "vert.spv"
del "frag.spv"

This will save a single file named "shader.spv" that you can load as one shader module and use for different stages in Vulkan.

Here are the required executables and a .bat file:


If you always use vertex and fragment stages then there is no problem, but what if the combined .spv file contains other stages, or is missing a fragment stage? We can easily account for this with a minimal SPIR-V file parser. We're not going to include any big bloated libraries to do this because we only need some basic information about what stages are contained in the shader. Fortunately, the SPIR-V specification is pretty simple and it doesn't take much code to extract the information we want:

std::string entrypointname[6];

auto stream = ReadFile(L"Shaders/shader.spv");

// Parse SPIR-V data
Assert(stream->ReadInt() == 0x07230203);
int version = stream->ReadInt();
int genmagnum = stream->ReadInt();
int bound = stream->ReadInt();
int reserved = stream->ReadInt();

bool stages[6] = {false,false,false,false,false,false};		

// Instruction stream
while (stream->Ended() == false)
	int pos = stream->GetPos();
	unsigned int bytes = stream->ReadUInt();
	int opcode = LOWORD(bytes);
	int wordcount = HIWORD(bytes);
	if (opcode == 15)
		int executionmodel = stream->ReadInt();
		Assert(executionmodel >= 0);
		if (executionmodel < 6)
			stream->ReadInt(); // entry point
			stages[executionmodel] = true;
			entrypointname[executionmodel] = stream->ReadString();
	stream->Seek(pos + wordcount * 4);

This code even retrieves the entry point name for each stage, so you can be sure you are loadng the shader correctly.

Here are the different shader stages from the SPIR-V specification:

  • 0: Vertex
  • 1: TessellationControl
  • 2: TessellationEvaluation
  • 3: Geometry
  • 4: Fragment
  • 5: GLCompute

That's it! We now have a standard single-file shader format for Vulkan programs. Your code for creating these will look something like this:

VkShaderModule shadermodule;

// Create shader module
VkShaderModuleCreateInfo shaderCreateInfo = {};
shaderCreateInfo.codeSize = bank->GetSize();
shaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(bank->buf);
VkAssert(vkCreateShaderModule(device->device, &shaderCreateInfo, nullptr, &shadermodule));

// Create vertex stage info
VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = shadermodule;
vertShaderStageInfo.pName = entrypointname[0].c_str();

VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
if (stages[4])
  // Create fragment stage info			
  fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
  fragShaderStageInfo.module = shadermodule;
  fragShaderStageInfo.pName = entrypointname[4].c_str();

// Create your graphics pipeline...



Recommended Comments

How will it work with custom shaders from the user? Suppose they will need to compile them.

Share this comment

Link to comment

I plan to add a tool for Visual Studio Code that will automatically call the required steps.

Then in the engine you can just call this:

auto shader = LoadShader("Shaders/myshader.spv");


  • Like 1

Share this comment

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Create New...