0
$\begingroup$

If you have a shader pipeline in OpenGL that goes something like the following:

// Pseudocode

// FragmentShader
// in vec3 inVertexPosition;
// in vec2 inTexCoords
// 
// int main()
{
    // Sample texture coordinate and write to fragment out
}

Then you have another fragment shader that does something a little different:

// int main()
{
      // Sample texture, calculate noise and mix with texture colour
}

In OpenGL if you want to execute this second shader you only compile this second shader and bind to the fragment shader stage of the shader pipeline. You can then create another a different vertex shader and attach that to the vertex shader stage whenever you need it.

Let's just say I have 5 vertex shaders and 5 fragment shaders, in OpenGL (because it supports separable shader objects) you only need to compile 5 x 2 = 10 shaders. However, as in Vulkan, as far as I know it doesn't support it, which means you have to compile 25 programs if you want to use each combination.

Does Vulkan have the equivalent of the OpenGL separable shader objects, or are there any plans for it? They seem just so much more flexible.

I've thought about being able to bind shader stages in my code, and then every combination of shader stages could be mapped to a single compiled program, however I don't think the mapping could be searched efficiently. I'm not sure. It seems being forced to compile an entirely new pipeline when inserting a shader stage is wasteful when you make many shaders.

It seems in principle this is like compiling a new program every time you created a new function.

$\endgroup$
3
$\begingroup$

Does Vulkan have the equivalent of the OpenGL separable shader objects, or are there any plans for it? They seem just so much more flexible.

It would be "so much more flexible". But Vulkan is a low-level rendering API. It's job is to give you access to the hardware as close to the metal as possible; user-convenience isn't even in the top-5 goals for the API.

Putting all of the context state into one gigantic, immutable (more or less) object gives the implementation all the power it needs to make your code fast. Forcing implementations to deal with stitching together shaders would make implementations more complicated (and another goal of Vulkan is to be relatively easy to implement).

The best way to solve this (where possible) is to just have fewer shader variations. Use uniforms/push-constants to detect what variations you want to use, then have conditionals based on them. Obviously this can't be done in every situation, but the uber-shader approach is the way to handle it to the degree possible/reasonable.

$\endgroup$
3
  • $\begingroup$ I am curious why this doesn't mention separate SPIR-V modules each with its own vertex and fragment shader? $\endgroup$ – pmw1234 Mar 11 at 18:11
  • $\begingroup$ @pmw1234: Because the question asked about pipelines, not modules. The OP asked about the need to build multiple pipelines for different combinations of shaders. The fact that you can have distinct SPIR-V modules for different shader stages is irrelevant to that: if you have 5 VSs and 5 FSs that all need to combine together, you may have only 10 SPIR-V modules, but you will still have 25 distinct pipelines. $\endgroup$ – Nicol Bolas Mar 11 at 18:13
  • 1
    $\begingroup$ "distinct SPIR-V modules", are they equivalent to OpenGL shader programs compiled with GL_PROGRAM_SEPARABLE, except that because the pipeline is immutable you need to compile a separate pipeline object anyway? If that's the case I don't see how "SPIR-V modules" make any difference. $\endgroup$ – Zebrafish Mar 11 at 20:31
3
$\begingroup$

On current GPU architectures, at the machine level, compilation of a shader program can depend on which other pipeline stages are active, what the inputs/outputs to those shaders are, as well as what the inputs and outputs of the overall pipeline are (vertex inputs and fragment outputs, for example). There's a lot of complicated architectural reasons why this can be true, for instance that some "builtin" inputs to the FS might actually need to be generated by an earlier stage, that VSes might need to output values differently depending on whether they're going to the FS or to a GS or tessellation shaders, etc.

In OpenGL, the driver is doing a lot of work under the hood to take care of these details. From your perspective, you may be only compiling 5 VS and 5 FS and using them in different combinations, but the driver is very likely actually compiling distinct programs (transparently to you) for every combination of VS/FS you use, and moreover also sometimes compiling distinct programs based on other OpenGL state that you wouldn't think was part of the shader at all.

With Vulkan, one of the major design goals of the API is to make the driver less "smart" and "magic", and move a lot of the driver's responsibilities into the app. This was done because OpenGL drivers became increasingly complex and opaque over time, and often caused performance issues that were unpredictable or burdensome to app developers—such as needing to suddenly compile new shaders in the background when a draw call was issued with a not-before-seen combination of shaders and state, causing stalls. The thinking is that by giving the app visibility into and control over such things, it can better manage how shader compilation work is done (e.g. compiling all needed combinations up front, compiling things on a background thread and not stalling rendering, etc). This philosophy extends to many other aspects of the API, such as memory management, command buffers, resource binding, synchronization between CPU and GPU, etc.

So, rather than thinking about it like "Vulkan is inconvenient and wasteful" I would try to see it more like "Vulkan makes clearer how much work is actually needed to set things up properly for the GPU, and gives me the control over how that work is done."

BTW,

I've thought about being able to bind shader stages in my code, and then every combination of shader stages could be mapped to a single compiled program, however I don't think the mapping could be searched efficiently.

This sort of thing can totally be done with a hash table; you just have to make a key struct that includes pointers to (or another identifier for) the individual VSes/FSes. But then you have to solve the same problem as the OpenGL driver had to solve, which is that you have to stop and create a new pipeline every time a new combination of VS/FS is seen, and that can result in stalls. It's better, if possible, to pre-create all the combinations you're going to need up front.

FWIW, in practice, I think in most applications you don't actually create a lot of combinations of different VSes and FSes. Most rendering engines only have a single VS for each FS (or a small handful of VSes for each FS), so there's not usually going to be a huge combinatoric explosion of different combinations.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.