Shader Processing

Last of the Vertex Processors

As I’ve touched upon previously, zink does some work in ntv to perform translations of certain GLSL variables to SPIR-V versions of those variables. Also sometimes we add and remove variables for compatibility reasons.

The key part of these translations, for some cases, is to ensure that they occur on the right shader. As an example that I’ve talked about several times, Vulkan coordinates are different from GL coordinates, specifically in that the Z axis is compressed and thus values there must be converted to perform as expected in the underlying Vulkan driver. This means that gl_Position needs to be converted before reaching the fragment shader.

Conversion Timing

Early approaches to handling this in zink, as in the currently-released versions of mesa, performed all translation in the vertex shader. Only vertex and fragment shaders are supported here, so this is fine and makes total sense.

Once more types of shaders become supported, however, this is no longer quite as good to be doing. Consider the following progression:

  • vertex shader converts gl_Position
  • vertex shader outputs gl_Position
  • geometry shader consumes gl_Position
  • geometry shader uses gl_Position for vertex output
  • geometry shader outputs gl_Position

In this scenario, the geometry shader is still executing instructions that assume a GL coordinate input, which means they will not function as expected when they receive the converted VK coordinates. The simplest fix results in:

  • vertex shader converts gl_Position
  • vertex shader outputs gl_Position
  • geometry shader consumes gl_Position
  • geometry shader unconverts gl_Position
  • geometry shader uses gl_Position for vertex output
  • geometry shader converts gl_Position
  • geometry shader outputs gl_POsition

I say simplest here because this requires no changes to the shader compiler ordering in zink, meaning that shaders don’t need to be “aware” of each other, e.g., a vertex shader doesn’t need to know whether a geometry shader exists and can just do the same conversion in every case. This is useful because at present, shaders in zink are compiled in a “random” order, meaning that it’s impossible to know whether a geometry shader exists at the time that a vertex shader is being compiled.

This is still not ideal, however, as it means that the vertex and geometry shaders are going to be executing unnecessary instructions, which yields a big frowny face in benchmarks (probably not actually that big, but this is the sort of optimizing that lets you call your code “lightweight”). The situation is further complicated with the introduction of tessellation shaders, where the flow now starts looking like:

  • vertex shader converts gl_Position
  • vertex shader outputs gl_Position
  • tessellation shader consumes gl_Position
  • tessellation shader unconverts gl_Position
  • tessellation shader converts gl_Position
  • tessellation shader outputs gl_Position
  • geometry shader consumes gl_Position
  • geometry shader unconverts gl_Position
  • geometry shader uses gl_Position for vertex output
  • geometry shader converts gl_Position
  • geometry shader outputs gl_Position

Not great.

Once Per Pipeline

The obvious change required here is to ensure that zink compiles shaders in pipeline order. With this done, all the uncompiled shaders are available to scan for info and existence, and the process can now be:

  • vertex shader converts gl_Position if no tessellation or geometry shader is present
  • vertex shader outputs gl_Position
  • tessellation shader consumes gl_Position
  • tessellation shader converts gl_Position if no geometry shader is present
  • tessellation shader outputs gl_Position
  • geometry shader consumes gl_Position
  • geometry shader uses gl_Position for vertex output
  • geometry shader converts gl_Position
  • geometry shader outputs gl_Position

Now that’s some lightweight shader execution. #optimized

Written on July 22, 2020