Linking a shader program
Once we have compiled our shaders and before we can actually install them into the OpenGL pipeline, we need to link them together into a shader program. Among other things, the linking step involves making the connections between input variables from one shader to output variables of another, and making the connections between the input/output variables of a shader to appropriate locations in the OpenGL environment.
Linking involves steps that are similar to those involved in compiling a shader. We attach each shader object to a new shader program object and then tell the shader program object to link (making sure that the shader objects are compiled before linking):
Getting ready
For this recipe, we'll assume that you've already compiled two shader objects whose handles are stored in the vertShader
and fragShader
variables.
For this and a few other recipes in this chapter, we'll use the following source code for the fragment shader:
#version 460 in vec3 Color; out vec4 FragColor; void main() { FragColor = vec4(Color, 1.0); }
For the vertex shader, we'll use the source code from the previous recipe, Compiling a shader.
How to do it...
In our OpenGL initialization function, and after the compilation of shader objects referred to by vertShader
and fragShader
, perform the following steps:
- Create the program object using the following code:
GLuint programHandle = glCreateProgram(); if( 0 == programHandle ) { std::cerr << "Error creating program object." << std::endl; exit(EXIT_FAILURE); }
- Attach the shaders to the program object as follows:
glAttachShader( programHandle, vertShader ); glAttachShader( programHandle, fragShader );
- Link the program:
glLinkProgram( programHandle );
- Verify the link status:
GLint status; glGetProgramiv( programHandle, GL_LINK_STATUS, &status ); if( GL_FALSE == status ) { std::cerr << "Failed to link shader program!" << std::endl; GLint logLen; glGetProgramiv(programHandle, GL_INFO_LOG_LENGTH, &logLen); if( logLen > 0 ) { std::string(logLen, ' '); GLsizei written; glGetProgramInfoLog(programHandle, logLen, &written, &log[0]); std::cerr << "Program log: " << std::endl << log; } }
- If linking is successful, we can install the program into the OpenGL pipeline with
glUseProgram
:
else glUseProgram( programHandle );
Regardless of whether the link was successful, it is a good idea to clean up our shader objects. Once the program is linked, they are not needed anymore:
// Detach and delete shader objects glDetachShader(programHandle, vertShader); glDetachShader(programHandle, fragShader); glDeleteShader(vertShader); glDeleteShader(fragShader);
How it works...
We start by calling glCreateProgram
to create an empty program object. This function returns a handle to the program object, which we store in a variable named programHandle
. If an error occurs with program creation, the function will return 0. We check for that, and if it occurs, we print an error message and exit.
Next, we attach each shader to the program object using glAttachShader
. The first argument is the handle to the program object, and the second is the handle to the shader object to be attached.
Then, we link the program by calling glLinkProgram
, providing the handle to the program object as the only argument. As with compilation, we check for the success or failure of the link, with the subsequent query.
We check the status of the link by calling glGetProgramiv
. Similar to glGetShaderiv
, glGetProgramiv
allows us to query various attributes of the shader program. In this case, we ask for the status of the link by providing GL_LINK_STATUS
as the second argument. The status is returned in the location pointed to by the third argument, in this case named status
.
The link status is either GL_TRUE
or GL_FALSE
, indicating the success or failure of the link. If the value of the status is GL_FALSE
, we retrieve and display the program information log, which should contain additional information and error messages. The program log is retrieved by the call to glGetProgramInfoLog
. The first argument is the handle to the program object, the second is the size of the buffer to contain the log, the third is a pointer to a GLsizei
variable where the number of bytes written to the buffer will be stored (excluding the null terminator), and the fourth is a pointer to the buffer that will store the log. The buffer can be allocated based on the size returned by the call to glGetProgramiv
with the GL_INFO_LOG_LENGTH
parameter. The string that is provided in log
will be properly null terminated.
Finally, if the link is successful, we install the program into the OpenGL pipeline by calling glUseProgram
, providing the handle to the program as the argument.
It is a good idea to detach and delete the shader object, regardless of whether the link is successful. However, if the shader objects might be needed to link another program, you should detach it from this program and skip deletion until later.
With the simple fragment shader from this recipe and the vertex shader from the previous recipe compiled, linked, and installed into the OpenGL pipeline, we have a complete OpenGL pipeline and are ready to begin rendering. Drawing a triangle and supplying different values for the Color
attribute yields an image of a multi-colored triangle where the vertices are red, green, and blue, and inside the triangle, the three colors are interpolated, causing a blending of colors throughout:
Note
For details on how to render the triangle, see Chapter 2, Working with GLSL Programs.
There's more...
You can use multiple shader programs within a single OpenGL program. They can be swapped in and out of the OpenGL pipeline by calling glUseProgram
to select the desired program.
Shader input/output variables
You may have noticed that the Color
variable is used to send data from the vertex shader to the fragment shader. There is an output variable (out vec3
) in the vertex shader and an input variable (in vec3
) in the fragment shader, both with the same name. The value that the fragment shader receives is a value that is interpolated from the values of the corresponding output variable for each of the vertices (hence the blended colors in the earlier image). This interpolation is automatically done by hardware rasterizer before the execution of the fragment stage.
When linking a shader program, OpenGL makes the connections between input and output variables in the vertex and fragment shaders (among other things). If a vertex shader's output variable has the same name and type as a fragment shader's input variable, OpenGL will automatically link them together.
It is possible to connect (link) variables that do not have the same name or type by using layout qualifiers. With a layout qualifier, we can specify the location for each variable specifically. For example, suppose that I used this set of output variables in my vertex shader:
layout (location=0) out vec4 VertColor; layout (location=1) out vec3 VertNormal;
I could use these variables in the fragment shader:
layout (location=0) in vec3 Color; layout (location=1) in vec3 Normal;
Despite the fact that these have different names (and for Color
, types), they will be connected by the linker when the program is linked due to the fact that they are assigned the same locations. In this example, VertColor
will be linked to Color
, and VertNormal
will be linked to Normal
. This makes things more convenient. We're not required to use the same names for input/output variables, which gives us the flexibility to use names that might be more descriptive in each shader stage. More importantly, it is part of a larger framework, called separate shader objects. A full example of separate shader objects can be found in the Using program pipelines recipe.
Note
In fact, this use of layout qualifiers to specify variable locations is required when compiling to SPIR-V (see the Loading an SPIR-V shader program recipe).
Deleting a shader program
If a program is no longer needed, it can be deleted from OpenGL memory by calling glDeleteProgram
, providing the program handle as the only argument. This invalidates the handle and frees the memory used by the program. Note that if the program object is currently in use, it will not be immediately deleted, but will be flagged for deletion when it is no longer in use.
Also, the deletion of a shader program detaches the shader objects that were attached to the program but does not delete them unless those shader objects have already been flagged for deletion by a previous call to glDeleteShader
. Therefore, as mentioned before, it is a good idea to detach and delete them immediately, as soon as the program is linked, to avoid accidentally leaking shader objects.
See also
- The
chapter01/scenebasic.cpp
file in the example code - The Compiling a shader recipe
- The Using program pipelines recipe
- The Loading an SPIR-V shader program recipe