1
$\begingroup$

I have some existing OpenGL code that I am porting to Vulkan. The question is somewhat relevant to both OpenGL and Vulkan.

I am rendering some primitives using textures, some single channel, others RGBA, as well as flat colored primitives with per-vertex colors. The GL code that I am porting frequently switches shader programs with glUseProgram.

In Vulkan, using two distinct sets of shaders requires two pipelines created with vkCreateGraphicsPipelines and later bound with vkCmdBindPipeline plus vkCmdBindDescriptorSets, thus changing shader programs equates program switches with graphics pipeline reconfiguration.

I am wondering whether it is efficient to switch to a single shader program and hence a single graphics pipeline with a vertex layout that contains texture coordinates, a color, and texture sampler. When I want to draw flat colored objects, the code would set the color in the vertex and bind a 1x1 white sampler and when I want to render images the code can bind a full image. I currently render the following:

  • flat color using color from vertex
  • alpha from sampler and color from vertex
  • RGBA from sampler and alpha from vertex

The question is from a hardware, driver/performance perspective, is it more efficient to use a single pipeline and descriptor layout and only performing uniform updates and have a slightly larger Vertex, or is switching pipelines low enough cost to warrant two or three pipelines and two of three sets of shaders and different vertex formats. i.e. where is the best balance between these concerns?

  • three sets of shaders, three small vertex structs; program, uniform, and sampler switches
  • one set of shaders, one slightly larger vertex struct and just uniform, and sampler switches

We could benchmark this but wonder if someone already has data to compare these approaches. I am aware of Ubershaders however this is hardly an Ubershader (see below) rather concerns any data on the ratio of program switch cost overhead versus the overhead of a 1x1 NOP texture lookups and texture color versus switching to a shader with no texture lookups.

With multiple shaders, the required program switches are likely to be in the order of 100's or possibly 1000's because of transparent content that requires a specific rendering order. In my case, it is for a mostly 2D app with a mix of line/block/bezier art, fonts using single alpha channel texture atlas plus vertex color and finally, RGBA images, typically just using the vertex alpha value and all currently using an orthographic projection with no lighting.

Ideally, I would like data on pipeline/program switch cost versus descriptor set switch cost.

For the shared program model, I am thinking about using the following:

This is the vertex struct:

struct Vertex {
    glm::vec3 vertex;
    glm::vec2 texcoord;
    uint32_t color;
};

and this is the vertex shader:

#version 450

layout (location = 0) in vec4 vcolor;
layout (location = 1) in vec2 vtexcoord;

layout (binding = 1) uniform sampler2D tex;

layout (location = 0) out vec4 outColor;

void main()
{
    outColor = vcolor * texture(tex, vtexcoord);
}

and this is the fragment shader:

#version 450

layout (location = 0) in vec3 vertex;
layout (location = 1) in vec2 texcoord;
layout (location = 2) in vec4 color;

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

layout (location = 0) out vec4 vcolor;
layout (location = 1) out vec2 vtexcoord;

out gl_PerVertex 
{
    vec4 gl_Position;
};

void main()
{
    vcolor = color;
    vtexcoord = texcoord;
    gl_Position = ubo.modelViewProjection * vec4(vertex.xyz, 1.0);
}

Note: as pointed out in the comments, swizzle masks in VkImageViewCreateInfo components field can be used to create VkImageView that translates the color and alpha channels instead of tieing shader code to the number of image channels in the sampler.

To broadcast the first color channel to all elements in OpenGL:

glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA,
    (GLint[]) { GL_RED, GL_RED, GL_RED, GL_RED } );

and for Vulkan, use the VkImageViewCreateInfo components field:

VkImageViewCreateInfo imageViewInfo;
imageViewInfo.components = {
    VK_COMPONENT_SWIZZLE_R,
    VK_COMPONENT_SWIZZLE_R,
    VK_COMPONENT_SWIZZLE_R,
    VK_COMPONENT_SWIZZLE_R
};
$\endgroup$
  • $\begingroup$ "I also use a matrix to transform textureColor as this allows me to configure the uniform for single channel alpha textures (texture(tex, vtexcoord).r) vs 4 channel RGBA textures (texture(tex, vtexcoord).rgba)." You really should look into swizzle masks. $\endgroup$ – Nicol Bolas Nov 28 '18 at 3:13
  • $\begingroup$ "I read that 2x2 is required because 1x1 pixel samplers have different behaviors" Where did you read this? $\endgroup$ – Nicol Bolas Nov 28 '18 at 3:15
  • $\begingroup$ @NicolBolas I saw the reference to using 2x2 vs 1x1 here "[bug?] sampling of 1x1 texture just returns black" "Rescaling texture to the 2x2 size solves the problem." forums.ogre3d.org/viewtopic.php?t=52995 It could be an Ogre loader issue but it influenced my thinking towards using 2x2. Apparently it should be possible to use the so-called unbound sampler. Sampler 0 in OpenGL has defined semantics but I also read there are potentially driver bugs. I am not sure what is defined for Vulkan samplers and whether it has the equivalent of sampler 0 in OpenGL. $\endgroup$ – Michael Clark Nov 28 '18 at 3:40
  • $\begingroup$ @NicolBolas 1x1 pixel works okay with Vulkan here. It is a power of two i.e. 2^0 = 1, so one would expect it to work, however, there is misleading information when searching regarding this topic (folk mentioning driver bugs). Unbound sampler in Vulkan on Nvidia 410.78 driver and GTX 1080 Ti returns a black pixel. Using a sampler with an incorrect descriptor config can lead to machine freezes with the current Nvidia Linux driver. $\endgroup$ – Michael Clark Nov 28 '18 at 7:35
  • $\begingroup$ "Perhaps there is a way to specify swizzle masks when creating a VkImage or VkSampler" It's part of VkImageView, not of the image itself. $\endgroup$ – Nicol Bolas Nov 28 '18 at 14:55
2
$\begingroup$

This is a really hardware-dependent question. But in general the best way to think about this is bottlenecks.

You mentioned:

one set of shaders, one slightly larger vertex struct and just uniform, and sampler switches

The slightly larger vertex struct doesn't seem like it will be the more likely bottleneck compared to:

With multiple shaders, the required program switches are likely to be in the order of 100's or possibly 1000's

On modern desktop/console hardware each graphics pipe might have a single-digit # of active contexts. Switching the state too frequently is sometimes to referred to as a context roll stall.

In this case whether you hit that stall case is also dependent on how fast your shader and blend operations complete to make the context available. Given that you're going to do a lot of alpha blending, you might be bound by blending operations.

By the way modern GPUs are much better at dynamic branching, and skipping over fetches with a shader uniform is possible so you don't need to pay the additional memory reads. That said, a 1x1 dummy texture is going to be in the texture cache anyway so again it probably doesn't matter.

I'd say try using a single shader first before you introduce new shader permutations. And in general, do the easiest implementation first, measure on your target hardware, then optimize :)

$\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.