Skip to content

seanbaxter/shaders

Repository files navigation

Circle C++ shaders

2021 Sean Baxter (@seanbax) circle-lang.org

  1. Shader quick guide

  2. Shaders as an embedded language

  3. The vertex and fragment stages

    cube

  4. The geometry stage

    geom

  5. The tessellation stages

    teapot

  6. The compute stage

    nbody nbody

  7. Physically-based rendering of glTF models

    viewer

  8. Reflection and attributes for Shadertoy development

    egg bands hypercomplex segment_tracer raymarcher bandlimited

  9. Compiled sprites with compile-time PNG decoding

    sprites tyrian

  10. Compiling CUDA code with C++ shaders

    particles

  11. Advanced compute with Vulkan

  12. Ray Tracing with Vulkan RTX

    ray_tracing1 ray_tracing2 mini_path_tracer ray_tracing_tutorial

  13. The mesh shader pipeline

    meshlet1 meshlet2

  14. DXIL D3D12 compilation

    nbody shadertoy tessellation wave

  15. Vectors and matrices

  16. Shader extension declarations

  17. Under construction features

Shader quick guide

Driver disclaimers

Working on this compiler has exposed a lot of graphics driver bugs. The vendors test their drivers on glslang output, and glslang doesn't generate multi-shader SPIR-V modules. But Circle does, and this causes the graphics drivers to segfault or error or render incorrectly in most of the samples, if run without workarounds.

  • NVIDIA: You must use the Vulkan beta driver 455.46.04 or later. It must be the beta driver. A release driver will fail in the teacups (segfault) and ray-tracing samples (no pretty pictures, just sky). The compiler itself has a workaround to prevent a segfault in the glTF sample viewer. The workarounds will be removed as driver bugs are fixed.

  • INTEL/AMD: Use a recent release driver. There are a number of driver bugs preventing proper execution, but I'm working to get them fixed. Check out the intel branch in this repository for Intel-compatible source for all the samples. Note that this branch significantly trails the master branch; namely, it lacks many of the advanced Shadertoy shaders.

Compiling the samples

This repository requires Git Large File Storage to support the HDR environment maps used by the glTF viewer project. Make sure to install Git LFS before pulling this project.

The OpenGL samples require the gl3w library. This isn't supported directly by common package managers. Compile a libgl3w.so with these instructions.

The repository has some submodule dependencies. Pull them with:

git submodule update --init --recursive

These samples include individual CMakeLists.txt files for easy compilation:

# cd into each sample folder
$ cd cube

# invoke cmake to construct a Makefile. point CMAKE_CXX_COMPILER at the circle
# binary on your system.
$ cmake -DCMAKE_CXX_COMPILER=circle .
-- Configuring done
-- Generating done
-- Build files have been written to: /home/sean/projects/shaders/cube

# build the project with make. -jN uses N threads for parallel compilation.
$ make -j4
[ 50%] Building CXX object CMakeFiles/cube.dir/cube.cxx.o
... wrote CMakeFiles/cube.dir/cube.cxx.spv
[100%] Linking CXX executable cube
[100%] Built target cube

$ ./cube

To build the sprites2 sample you'll need to compile STB image into a shared object:

$ cd thirdparty
$ gcc -shared -fPIC stbi.c -o libstbi.so

Using the compiler

Enable shader generation with the -shader the command line switch. This switch injects the contents of the SPIR-V declarations file implicit/spirv.cxx into the translation unit, enabling builtin vectors and matrices, along with the full host of standard GLSL intrinsic functions.

By default, shaders that are named inside the @spirv or @dxil extensions are compiled to shader binary code and bound to the compiled object. This is the preferred single-source mode of translation. However, there are are additional options for separate-source translation:

  • -emit-spirv -c to emit a SPIR-V module. This has .spv extension.
  • -emit-dxil -c to emit a DXIL module. This has .dxil extension.
  • -emit-dxil -S to emit a DXIL IR file. This is human-readable LLVM and has a .dxilasm extension.
  • -emit-dxil -S -console to print DXIL disassembly to the terminal.

Use the -E entry point option with any of the above options to specify which shader to compiler.

  • -E <entry-point> chooses the shader entry point.

Examples

vert.cxx

[[spirv::vert]]
void vert_main() { 
  glvert_Output.Position = vec4(.5, 0, 0, 0);
}
$ circle -shader -c -emit-spirv vert.cxx
$ spirv-dis vert.spv
  OpCapability Shader
  OpExtension "GL_EXT_scalar_block_layout"
  OpMemoryModel Logical GLSL450
  OpEntryPoint Vertex %_Z9vert_mainv "_Z9vert_mainv" %glvert_Output
  ...

When an output filename is provided, Circle will infer what kind of operation you want. You can drop the -c -emit-spirv switches, but you'll need to keep -shader:

$ circle -shader -o vert.spv vert.cxx
$ spirv-dis vert.spv
  OpCapability Shader
  OpExtension "GL_EXT_scalar_block_layout"
  OpMemoryModel Logical GLSL450
  OpEntryPoint Vertex %_Z9vert_mainv "_Z9vert_mainv" %glvert_Output
  ...

You can define as many shaders as you'd like in one translation unit. When using Circle to generate external SPIR-V modules, you'll need to explicitly specialize shader function templates:

vert2.cxx

template<int X>
[[spirv::vert]]
void vert_main() { 
  glvert_Output.Position = vec4(X, 0, 0, 0);
}

template void vert_main<5>();
template void vert_main<10>();
$ circle -shader -c -emit-spirv vert2.cxx
$ spirv-dis vert2.spv
  OpCapability Shader
  OpExtension "GL_EXT_scalar_block_layout"
  OpMemoryModel Logical GLSL450
  OpEntryPoint Vertex %_Z9vert_mainILi5EEvv "_Z9vert_mainILi5EEvv" %glvert_Output
  OpEntryPoint Vertex %_Z9vert_mainILi10EEvv "_Z9vert_mainILi10EEvv" %glvert_Output
  ...

To turn off name mangling of shaders, mark the shader entry points extern "C" or use an asm-label between the function signature and the the curly braces.

Recommended operation is to define shader code directly in a normal translation unit. This is the "single-source" mode. Use -shader without -emit-spirv to enable this mode.

In single-source mode only shader functions that are ODR-used are included in the SPIR-V module. The SPIR-V binary is embedded right into the compiled object file, accessible at the symbol __spirv_data with a length in bytes of __spirv_size. ODR use shaders by using them as arguments in the @spirv operator. This intrinsic yields the name of the shader, for use with glSpecializeShader or VkPipelineShaderStageCreate, and causes the shader to be lowered to SPIR-V and included in the compiled object's binary. In single-source mode, the SPIR-V module is written to file as a side effect, and is given the name of the input C++ file but with the extension replaced with ".spv".

vert3.cxx

#include <cstdio>

template<int X>
[[spirv::vert]]
void vert_main() { 
  glvert_Output.Position = vec4(X, 0, 0, 0);
}

int main() {
  // Using shader functions in the @spirv operator ODR uses them
  // and returns their module names.
  printf("%s\n", @spirv(vert_main<5>));
  printf("%s\n", @spirv(vert_main<10>));
}
$ circle -shader vert3.cxx 
... wrote vert3.spv
$ ./vert3
_Z9vert_mainILi5EEvv
_Z9vert_mainILi10EEvv
$ spirv-dis vert3.spv
  OpCapability Shader
  OpExtension "GL_EXT_scalar_block_layout"
  OpMemoryModel Logical GLSL450
  OpEntryPoint Vertex %_Z9vert_mainILi5EEvv "_Z9vert_mainILi5EEvv" %glvert_Output
  OpEntryPoint Vertex %_Z9vert_mainILi10EEvv "_Z9vert_mainILi10EEvv" %glvert_Output
  ...

Shader attributes

Mark shader entry points with one of these attributes:

  • [[spirv::vert]] - Vertex stage.
  • [[spirv::tesc(primitive, num_vertices)]] - Tessellation control stage. num_vertices - Incoming patch size.
  • [[spirv::tese(primitive)]] - Tessellation evaluation stage.
enum class gltese_primitive_t : unsigned {
  triangles_cw,
  triangles_ccw,
  quads,
  isolines,
};
  • [[spirv::geom(input, output, max_vertices)]] - Geometry stage. input and output must be values from these enumerations:
enum class glgeom_input_t : unsigned {
  points,
  lines,
  lines_adjacency,
  triangles,
  triangles_adjacency,
};

enum class glgeom_output_t : unsigned {
  points,
  line_strip,
  triangle_strip,
};
  • [[spirv::frag(origin)]] - Fragment stage. origin is an optional argument and must be a value from glfrag_origin_t:
enum class glfrag_origin_t : unsigned {
  lower_left,   // OpenGL style
  upper_left,   // Vulkan style
};
  • [[spirv::comp]] - Compute stage.
  • [[spirv::task]] - Task stage.
  • [[spirv::mesh(output)]] - Mesh stage. output must be a value from glmesh_output_t.
enum class glmesh_output_t : unsigned {
  points,
  lines,
  triangles,
};
  • [[spirv::rgen]] - Ray generation stage.
  • [[spirv::rint]] - Ray intersection stage.
  • [[spirv::rahit]] - Ray any hit stage.
  • [[spirv::rchit]] - Ray closest hit stage.
  • [[spirv::rmiss]] - Ray miss stage.
  • [[spirv::rcall]] - Ray callable stage.

The spirv::local_size attribute is required when declaring compute, task and mesh stages.

  • [[spirv::local_size(x, y, z)]] - The workgroup size. The x argument is mandatory. The y and z arguments are optional.

Tessellation shaders incorporate additional optional attributes. These two attributes may be provided on tessellation control shader when targeting D3D12 tessellation evaluation when targeting OpenGL. Vulkan supports the attribute on either shader stage.

  • [[spirv::output(output)]] - The type of geometry emitted. This is different from the patch primitive topology, which describes the input geometry.
  • [[spirv::spacing(spacing)]] - The subdivision algorithm.
enum class gltess_primitive_t : unsigned {
  triangles,
  quads,
  isolines,
};

enum class gltess_output_t : unsigned {
 points,
 lines,
 triangle_cw,
 triangle_ccw,
};

enum class gltess_spacing_t : unsigned {
  equal,
  fractional_even,
  fractional_odd,
};

Circle supports DXIL constructs with a few new attributes:

  • [[dxil::patch_constant(f)]] - Tessellation control shaders/hull shaders may set the gltesc_LevelInner and gltesc_LevelOuter variables from a special patch constant function, which is invoked once per patch. This is mandatory for DXIL shaders and optional for SPIR-V shaders.
  • [[dxil::max_tess_factor(factor)]] - Hull shaders may specify a max tessellation factor, up to 64. DXIL only.

Interface attributes

Shaders stages are distinguished by a shader attribute. Interface variables are distinguished by interface attributes. In all cases, the interface attribute name indicates the storage class for the variable.

  • [[spirv::in]] - Input scalar or vector. Requires a spirv::location attribute.
  • [[spirv::in(location)]] - Input scalar or vector that takes location operand.
  • [[spirv::out]] - Output scalar or vector. Requires a spirv::location attribute.
  • [[spirv::out]] - Output scalar or vector that takes location operation.
  • [[spirv::uniform]] - A Uniform or UniformConstant interface variable. The former require a spirv::binding and optional spirv::set. The latter require a spirv::location attribute.
  • [[spirv::uniform(binding [, set])]] - Specify the uniform storage class and take the resource binding and optional descriptor set values as operands.
  • [[spirv::buffer]] - Shader stage storage buffer. Requires spirv::binding. Optional spirv::set.
  • [[spirv::buffer(binding [, set])]] - Specify the buffer storage class and take the resource binding and optional descriptor set values as operands.
  • [[spirv::shared]] - Workgroup shared memory. Declare these inside a compute, task or mesh shader.
  • [[spirv::push]] - Push constant. Vulkan only. Defaults to 128 byte structure.
  • [[spirv::push(max-size)]] - Push constant with explicit structure size limit.
  • [[spirv::rayPayload]]
  • [[spirv::rayPayloadIn]]
  • [[spirv::hitAttribute]]
  • [[spirv::callableData]]
  • [[spirv::callableDataIn]]
  • [[spirv::shaderRecord]] - Ray tracing storage classes. See GLSL_nv_ray_tracing.
  • [[spirv::perTaskOut]] - Outgoing payload from task shader into mesh shader. See GLSL_nv_mesh_shader
  • [[spirv::perTaskIn]] - Incoming payload to mesh shader from task shader.
  • [[spirv::constant(index)]] - Specialization constant or structure of specialization constants. The index is equivalent to GLSL's constant_id. This is not technically a storage class. See Specialization constants.

In addition to the storage class attributes, GLSL includes a number of layout qualifiers:

  • [[spirv::location(index)]] - Binding location for in, out and uniform interface variables.
  • [[spirv::component(index)]] - Specifies a component within a shader stage in or out attribute. This allows packing multiple scalars or small vectors inside the vec4 layout for shader stage atrtibutes.
  • [[spirv::binding(index)]] - Binding location for uniform buffers, storage buffers, images, textures and samplers.
  • [[spirv::set(index)]] - Descriptor set that may optionally be provided with spirv::binding. Vulkan only.
  • [[spirv::format(format)]] - A compatible internal format for image buffers. Must be a value from the gl_format_t enumeration:
  • [[spirv::invocations(count)]] - The number of times to invoke the geometry shader. glgeom_InvocationID specifies the particular instance.
enum class gl_format_t : unsigned {
  unknown,
  
  // float-image-format-qualifier:
  rgba32f,
  rgba16f,
  r32f,
  rgba8,
  rgba8_snorm,
  rg32f,
  rg16f,
  r11f_g11f_b10f,
  r16f,
  rgba16,
  rgb10_a2,
  rg16,
  rg8,
  r16,
  r8,
  rgba16_snorm,
  rg16_snorm,
  rg8_snorm,
  r16_snorm,
  r8_snorm,

  // int-image-format-qualifier:
  rgba32i,
  rgba16i,
  rgba8i,
  r32i,
  rg32i,
  rg16i,
  rg8i,
  r16i,
  r8i,

  // uint-image-format-qualifier:
  rgba32ui,
  rgba16ui,
  rgba8ui,
  r32ui,
  rgb10_a2ui,
  rg32ui,
  rg16ui,
  rg8ui,
  r16ui,
  r8ui,
};

There are some mutually-exclusive auxiliary modifiers:

  • [[spirv::noperspective]] and [[spirv::flat]] - Control interpolation of shader stage attributes. Use at most one of these.

  • [[spirv::patch]], [[spirv::centroid]] and [[spirv::sample]] - Use at most one of these.

  • [[dxil::semantic(name)]] - Identifies a semantic name to an in or out interface variable. A spirv::location attribute cannot be declared for the same variable. Special variables starting with "SV_" are not supported with this attribute.

  • [[dxil::semantic(name, index)]] - The two-operand version is easier to specialize. The string name identifies the element without an index. The index operand may be any constant expression, including one dependent on a template parameter. During instantiation, the integer index is evaluated, turned into a string, and concatenated with the preceding name.

Shaders as an embedded language

The Circle shader extension aims to bring the entire capability of the OpenGL Shading Language (GLSL) into C++ as a first-class language feature. Write graphics code using Standard C++ and mark interface variables and shader functions with C++ attributes to indicate their role in the graphics pipeline. When the program is compiled, all shader declarations are lowered to the SPIR-V binary intermediate representation, which is the portable shader storage format Vulkan and OpenGL programs.

The guiding vision behind this extension was to make shader programming idiomatic to both C++ and GLSL users, at the same time. GLSL has a lot of conveniences that Standard C++ lacks, so these were added to the Circle frontend:

Approaching Circle shaders from GLSL

GLSL-like vector and matrix types are now built into the C++ compiler. These builtins provide a canonical definition for interface variables which expect vector and matrix types. Additionally, the vector type provides both source and destination component swizzle, a critical convenience that both GLSL and HLSL offer but is impractical to implement using Standard C++.

Nearly the entire slate of GLSL builtin functions has been added to the Circle compiler. In addition to the expected elementary math functions, you get things like mix, smoothstep, refract and clamp. When invoked, these functions lower directly to their SPIR-V and GLSL.std.450 opcodes, resulting in a compact shader encoding that more closely resembles the glslang output that graphics drivers are tested and tuned on.

The many choices in storage classes, layout modifiers, formats, thread group sizes and the like have been moved inside C++ attributes. Recognizable semantics, modern syntax.

Uniform and storage buffers were not cleanly defined in GLSL, requiring an apparently superfluous block structure that encapsulates buffer contents. While the block requirement remains in SPIR-V, the Circle shader extension internalizes their definition. You can now define uniform and storage buffers directly with ordinary C++ structs and arrays.

Array members in GLSL storage buffers may have a "runtime" length. This length may be queried with the .length operator. This operator has been added to the Circle compiler. I'm committed to implementing even marginal GLSL features. I want GLSL users to be able to move to an all-C++ enivronment and continue banging out shaders with minimal retraining.

Approaching Circle shaders from C++

From the front-end perspective, anything you can express in C++ you can now express in a C++ shader. Your shaders can be function templates. Your interface variables (such as ins, outs, samplers, uniform buffers) can be variable templates. Shader attribute values can be template-dependent expressions. You can bind C++ structures directly to interface variables, and member functions can be invoked on objects of any shader storage class. If you're a C++ power user, you don't have to dumb down how you code: modern C++ can now be brought to bear on graphics.

From the back-end perspective, the shader abstract machine model is less flexible than a CPU. Indirect and virtual function calls, exceptions, dynamic memory, virtual inheritance and RTTI cannot be provided. More troubling, most operations involving pointers are incompatible with the "Logical" memory model that SPIR-V graphics defines. Of course, pointers and references (which are just pointers after a value category transformation) are at the core of C++. Circle has special control-flow graph passes to de-pointerize functions. In practice this involves splitting apart base pointer from offset terms, propagating this split up the control flow graph, and replacing accesses via pointers, which is prohibited, into direct indexing on variables, which is supported. In short, pointers aren't guaranteed to lower to SPIR-V, but with the help of these transforms, pointers in the code you wrote likely will.

Other aspects of C++ reside in an implementation gray area, including unions and bit-fields. These can be supported for most use cases with CFG transformations. They will be incorporated into the compiler when I'm satisfied with their robustness.

Approaching Circle shaders from Circle

The original design principle of Circle was to rotate C++ from the runtime onto the compile-time axis, allowing the developer to metaprogram C++ using plain C++, as opposed to a ghoulish recursive template and SFINAE dialect. This principle motivates the integration of GLSL functionality directly into C++. What's the big reason for choosing such a rich, general purpose language to write shaders? I've already conceded that it can't use virtual and indirect calls, exceptions, dynamic memory, file or network i/o, and many other features that C++ programmers consider essential. But those features may still be used in shader definitions at compile time.

I think it makes sense to target devices with limited flexibility in a maximally-expressive language like C++ if that power can be used in the service of program generation. Two example projects examine this idea:

  1. The Shadertoy demo includes five simple fragment shaders, each encapsulated with its uniform data in a C++ function object. User attributes annotate data members with configuration metadata, and Circle reflection generically iterate over the uniform data members and defines an ImGui user interface automatically.

    Attributes are an important tool for connecting host code (C# in the case of Unity and C++ in the case of Unreal Engine) with script engines, editors and other subsystems. Many frameworks define their own "pipeline" file formats which combine shader text and attribute metadata. With the Circle shader extension, user attributes are a first-class part of the language.

  2. The sprite compiler knocks down preconceptions about what is supported during C++ source translation. Games from the pre-accelerator era often compiled sprites directly into functions, eliminating branching over transparent values and lowering to machine code that was basically a continuous stream of store operations. Developers would write sprite compiler tools using C or a scripting language which would consume an image file and sprite sheet metadata, and emit textual C code or assembler, which effectively drew the sprite into memory.

    This Circle example eliminates the discrete sprite compiler by loading the sprite sheet and PNG image at compile time. (It uses the the popular STB image decoder.) Compile-time control flow visits sections of the decompressed image defined by the sprite sheet and emits imageStore operations into a compute shader, thereby encoding the information in a sprite directly into SPIR-V binary. Allow me to repeat: you can #include the code for a PNG decoder immediately use that at compile time to load an image and generate custom shader logic from its pixel values. Whatever imaginative leap you've made with regard to generative metaprogramming, this compiler can probably do it.

The limited semantics of GLSL, while by construction adequete for expressing any accelerated graphics operation, is insufficient for expressing higher-level functionality that may be useful for metaprogramming your shader. While the C++ shaders use only a subset of Standard C++, the surrounding metaprogram may use the whole thing, plus user-attributes, reflection and compile-time execution in addition to that.

The vertex and fragment stages

cube

All programs using the raster pipeline require a vertex and fragment shader. A vertex array, which is defined using the API, defines the mapping from buffer memory to vertex stage interface variables, which are defined by the shader extension. Data may be passed from the vertex stage to the fragment stage, via interpolators, using interface variables which are reconciled when the shader program is linked by the graphics driver.

Even in their vanilla forms, the shader functions and interface variables as expressed with Circle look quite different from GLSL:

cube/cube.cxx

[[spirv::in(0)]]
vec3 in_position_vs;

[[spirv::in(1)]]
vec2 in_texcoord_vs;

[[spirv::out(1)]]
vec2 out_texcoord_vs;

[[spirv::in(1)]]
vec2 in_texcoord_fs;

[[spirv::out(0)]]
vec4 out_color_fs;

[[spirv::uniform(0)]]
sampler2D texture_sampler;

struct uniforms_t {
  mat4 view_proj;
  float seconds;
};

[[spirv::uniform(0)]]
uniforms_t uniforms;

[[spirv::vert]]
void vert_main() {
  // Create a rotation matrix.
  mat4 rotate = make_rotateY(uniforms.seconds);

  // Rotate the position.
  vec4 position = rotate * vec4(in_position_vs, 1);

  // Write to a builtin variable.
  glvert_Output.Position = uniforms.view_proj * position;

  // Pass texcoord through.
  out_texcoord_vs = in_texcoord_vs;
}

[[spirv::frag]]
void frag_main() {
  // Load the inputs. The locations correspond to the outputs from the
  // vertex shader.
  vec2 texcoord = in_texcoord_fs;
  vec4 color = texture(texture_sampler, texcoord);

  // Write to a variable template.
  out_color_fs = color;
}

Maybe the most conspicuous difference is that the vertex and fragment shaders are written in the same file! GLSL requires each shader be written in its own translation unit, and that it must be called "main". In Circle, shaders can be given any name, and may be function templates. Write as many of them in one translation unit, and they'll all be compiled into a single SPIR-V module.

The next thing to catch your eye is probably the unfamiliar look of the C++ attribute:

[[spirv::in(0)]]
vec3 in_position_vs;

This declaration is the C++ equivalent of this bit of GLSL:

layout(location=0)
in vec3 in_position_vs;

How do you declare a uniform buffer? Simply use a [[spirv::uniform]] storage class on a namespace-scope object declaration. A binding attribute is also required, or can be passed as an operand of spirv::uniform. The shader uses the layout of the type according to the Itanium C++ ABI. This is different from the std140 layout of GLSL. See the layout section for C++ attributes to arrange data members according to the std140, std430 or scalar buffer layouts.

The interface variables appear to be global variables, but they don't actually emit any storage on the host side. Fundamentally these declarations relate type with layout qualifiers and storage class. On the host, the user can't write to or read from any of these interface variables; you'd get an undefined symbol error. To change the data the uniform buffer refers to, use the appropriate Vulkan or OpenGL API with the interface variable's binding location.

glvert_Output.Position is the Circle name for gl_Position. Builtin variables are named according to their shader stage and storage class. Because shaders from all stages may coexist in a single C++ translation unit, more specific names are required to distinguish the type and storage class of builtin objects.

Specializing SPIR-V shaders

void myapp_t::create_shaders() {
  GLuint vs = glCreateShader(GL_VERTEX_SHADER);
  GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
  GLuint shaders[] { vs, fs };
  glShaderBinary(2, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V_ARB,
    __spirv_data, __spirv_size);

  glSpecializeShader(vs, @spirv(vert_main), 0, nullptr, nullptr);
  glSpecializeShader(fs, @spirv(frag_main), 0, nullptr, nullptr);

  program = glCreateProgram();
  glAttachShader(program, vs);
  glAttachShader(program, fs);
  glLinkProgram(program);
}

All shaders in a C++ translation unit are linked in a single SPIR-V binary module, which is bound in the byte array __spirv_data with length, in bytes, __spirv_size. Pass these terms to glShaderBinary or VkShaderModuleCreate where you'd typically pass data loaded from file. Due to the multi-entry-point module nature of the system, you only need one glShaderBinary or VkCreateShaderModule per C++ translation unit. All the glSpecializeShader and VkPipelineShaderStageCreate calls source into this common module.

Shader functions are only lowered to SPIR-V if they are ODR-used. ODR usage is a complex topic, but for our purposes, it just means that the shader name or the specialization of a shader function template must appear inside a @spirv operator. This special operator causes the shader to be lowered to SPIR-V binary and returns the mangled name of the shader. It's intended to provide the name argument of glSpecializeShader and VkPipelineShaderStageCreate. Requesting the name of the shader is its ODR usage. This is similar to 'inline' linkage for normal functions: you can include as many shaders in your translation unit as you want, either directly or through headers, but only those used by @spirv are ODR-used and lowered to code.

Variable templates

The many interface declarations in our cube sample is unacceptably clumsy. You need a separate variable for every input and output to every shader stage in your program. GLSL limits you to one shader per file, so they don't look too bad; but when you define many shaders in one translation unit, you really see how they pile up, and choosing names for each of them is a burden.

Variable templates are a little-known C++14 feature. They're little-known because they're very rarely used outside of the implementation of C++17 type traits. (I mean the type traits with a "_v" on the end). They're basically template versions of objects, but because their dynamic initialization order is hard to pin down, in practice they're only used to hold constexpr values, for which dynamic initialization order does not matter.

I use variable templates for shader interface variables all the time. Interface variables don't have storage so they don't get initialized. They only exist to relate types with layout qualifiers and storage classes. This is an ideal use case for variables templates. Rather than defining interface as a bunch of global variables for each location, shader stage and storage class, we can define the interface as specializations from a library of interface variable templates.

// The interface variable template. This can go in a library.
template<typename type_t, int I>
[[using spirv: out(I)]]
type_t shader_out;

// A fragment shader that writes a blue output.
[[spirv::frag]]
void frag_main() {
  shader_out<vec4, 0> = vec4(0, 0, 1, 1);
}

The shader_out variable template can be defined once and then put out of sight. It's not an interface variable by itself. Rather, its specialization generates an interface variable. The shader frag_main generates an out storage class variable with type vec4 and location 0 simply by using the template with those arguments.

Typed enums

Circle includes a powerful type called the typed enum. It's like an ordinary enum, but each enumerator constant also has an associated type. It provides the capability of a compile-time type list. Associated keywords like @enum_type (to yield the type associated with a single enumerator) and @enum_types (to yield a parameter pack of types associated with all the enumerators) provide reflection on typed enums. The associated type is specified in the enum-specifier. In a normal enum, the name is required and the value is optional. In a typed enum, the name is optional, the value is automatically assigned, and the type is required.

#include <cstdio>

enum typename type_list_t {
  Nothing = void,
  Counter = int,
  char*,
  double[5],
};

int main() {
  printf("type_list_t has values:\n");
  @meta for enum(type_list_t x : type_list_t)
    printf("  %s (%d) = %s\n", @enum_name(x), (int)x, @enum_type_string(x));
}
$ circle typelist.cxx
$ ./typelist
type_list_t has values:
  Nothing (0) = void
  Counter (1) = int
  _2 (2) = char*
  _3 (3) = double[5]

type_list_t is a typed enum serving as a type list. It has four enumerators: Nothing, Counter, _2 and _3. A name that isn't specified is set to its ordinal, automatically. Converting these enumerators to integers also reveal they have ascending values: Nothing is 0, Counter is 1, _2 is 2 and _