Core Language (GLSL)

From OpenGL Wiki
(Redirected from GLSL Core Language)
Jump to navigation Jump to search

The OpenGL Shading Language is a C-style language, so it covers most of the features you would expect with such a language. Control structures (for-loops, if-else statements, etc) exist in GLSL, including the switch statement. This section will not cover the entire language in detail; the GLSL specification can handle that. This Wiki page will note the differences between GLSL and C.

Compilation settings[edit]

The OpenGL Shading Language requires certain information to be presented early in a shader object's compilation. In a command-line-based compiler, these would be command-line compiler options. GLSL's compilation model instead requires them to be part of the language.

These should be in the first lines of the first string associated with a shader object. If you want to globally apply these, then you should put them in a string that is first in the array of strings passed to the shader object with glShaderSource, or to glCreateShaderProgram.

Version[edit]

The OpenGL Shading Language has gone through a number of revisions, some of them quite substantial. As part of the OpenGL Specification, each version of OpenGL is required to support specific versions of GLSL; it may optionally support more.

To specify which version of GLSL should be used to compile/link a shader, use this directive:

  #version 150

This would tell the compiler to compile for version 1.50, or error if that version is not available.

The version number can be followed by the profile name. This can be core or compatibility. If a profile name is not specified, the default is core.

The #version directive must appear before anything else in a shader, save for whitespace and comments. If a #version directive does not appear at the top, then it assumes 1.10, which is almost certainly not what you want.

OpenGL and GLSL versions[edit]

Every OpenGL version since 2.0 has been released with a corresponding GLSL version. However, the GLSL version numbers were not always in sync with the GL version. Here is a table:

OpenGL Version GLSL Version
2.0 1.10
2.1 1.20
3.0 1.30
3.1 1.40
3.2 1.50

For all versions of OpenGL 3.3 and above, the corresponding GLSL version matches the OpenGL version. So GL 4.1 uses GLSL 4.10.

Extensions[edit]

Many OpenGL Extensions modify GLSL's behavior and functionality as well. Unlike regular OpenGL, where extensions are implicitly always there whether you use it or not, GLSL extensions must explicitly be specified in the particular shader string being compiled.

Similar to the #version directive, the user can activate specific extensions with the "#extension" directive. You should put these definitions before any other language features, but after the version declaration. The syntax is as follows:

 #extension extension_name​ : behavior​

The extension_name​ can also be the string all. This means it works for all extensions. The available behaviors are:

  • enable: Causes the named extension to work; if the implementation does not support the extension, it only gives a warning. Fails if used with all.
  • require: Causes the named extension to work; if the implementation does not support the extension, it fails. It also fails if used with all.
  • warn: Causes the named extension to work; however, using the extension will emit warnings. If used with all, then the use of any extensions will emit warnings.
  • disable: Prevents the named extension from working at all. Thus, any use of it will be seen as undefined syntax and cause an error. If used with all, then this prevents any extensions from working.

Preprocessor directives[edit]

All of the keywords beginning with # are preprocessor directives, much like with C, although #line is different. GLSL provides most of the standard C set of preprocessor directives (#define, #if, etc), in addition to the ones listed above. The most notable omission is #include.

Macro expansion does not work on #version and #extension directives.

#line directive[edit]

The #line directive allows you to change the current __FILE__ and __LINE__ values, and is different from C. It has the forms:

#line line
#line line source-string-number

The line after the #line directive in the source will be set to the given line number. For example, if you have "#line 4" on one line and "error" on the next, then "error" will be on line 4 if it is an error. The #line directive does not support source files as in C.

Standard macros[edit]

GLSL defines a number of macros. __FILE__ is not a filename; it is a decimal integer representing which string in the list of strings given to the shader. __LINE__ is the line number. __VERSION__ is a decimal integer representing the GLSL version being compiled. If the version is 3.30, then __VERSION__ will be "330" (as an integer).

The macro GL_core_profile is always defined to be "1". The macro GL_compatibility_profile is only defined to be "1" if the version for this shader was set to be compatibility.

Reserved names[edit]

GLSL reserves any name beginning with "gl_"; attempts to define variables or functions that begin with this string will result in an error. Also, GLSL has a number of keywords, which cannot be used as identifiers in any context.

Types[edit]

The GLSL defines a number of types. Some of them are familiar to C/C++ users, while others are quite different.

Qualifiers[edit]

Variables declared at global and local scope can have a number of qualifiers associated with them. Most of these are unique to shading languages.

Expressions[edit]

GLSL has some unique expression definitions.

Constant expression[edit]

Constant expressions are expressions that can be evaluated at compile time. Constant expressions in GLSL are expressions that consist of:

  • A literal value.
  • A const-qualified variable (not a function parameter) with an explicit initializer, but only if the initializer is itself one of the following:
    • A constant expression.
    • An Initializer List, whose components are themselves constant expressions. Initializer lists are not expressions themselves.
  • The result of the length() function of an array, but only if the array has an explicit size (who's size must be a constant expression).
  • The result of most operators, so long as all the operands are themselves constant expressions. The operators not on this list are any assignment operators (+= and so forth), and the comma operator.
  • The result of a constructor for a type, but only if all of the arguments to the constructor are themselves constant expressions.
  • The return value of any built-in function, but only if all of the arguments to the function are themselves constant expressions. Opaque Types are never constant expressions. Note that the functions dFdx, dFdy, fwidth, and their Coarse and Fine variations will return 0, when given a constant expression as an argument.

Integral constant expression[edit]

These are constant expressions that result in an integer, signed or unsigned.

Dynamically uniform expression[edit]

Dynamically Uniform Expression
Core in version 4.6
Core since version 4.0

A dynamically uniform expression is a GLSL expression in which all invocations of that shader within a particular set of invocations will have the same value.

Only GLSL 4.00 and above makes a distinction about dynamically uniform expressions. Any restrictions you see that require a "dynamically uniform expression" will, in older GLSL versions require a "constant expression".

An expression in a GLSL shader is said to be dynamically uniform if, and only if, every shader invocation within the same invocation group will have the same value, in the same order. Whether an expression is dynamically uniform depends on the values provided; it cannot always be a priori determined from a shader's code.

Some expressions are always dynamically uniform, by the rules of the language. These include:

Other expressions may or may not be dynamically uniform. For example, a shader stage input value will usually not be dynamically uniform. However, if every shader in the rendering command is given the exact same input value, then it will be dynamically uniform. It is the value of the expression which must be uniform; every shader invocation must get the same value, even if that same value is being read from different memory location.

gl_InstanceID is an example. If you render only a single instance, then gl_InstanceID will be dynamically uniform for that rendering command. However, if you render multiple instances in the same command, it will not. The same goes for Instanced Array input values.

Most important of all, if you execute a loop over values, where the initializing expression, offset, and test condition all use dynamically uniform expressions, then the loop counter itself will be dynamically uniform.

While accessing textures is guaranteed to be dynamically uniform if the texture coordinates are dynamically uniform, accessing image variables or Atomic Counters is not necessarily so. These are modifiable memory, so each invocation may not get the same value. If the image variable is read-only, then accessing it is dynamically uniform if the image coordinates and so forth are dynamically uniform.

Invocation group[edit]

Dynamically uniform expressions are based on shader invocations "within the same invocation group." The definition of "invocation group" depends on the shader stage in question. For Compute Shaders, the scope is all invocations within a work group. For shaders launched through Rendering Commands, this gets more complex.

Rendering scope extends to all invocations of all shader stages created to service a single drawing command. This means that, for Fragment Shaders, two distinct primitives in a rendering command are considered part of the same scope. This means that gl_PrimitiveID is not dynamically uniform (unless you render only one primitive). This is also why gl_InstanceID is only dynamically uniform when rendering a single instance; instanced rendering is considered a single drawing command.

However, there are drawing commands that create multiple scopes. The Multidraw Rendering commands, including the indirect versions, have separate scopes for their separate drawing sub-commands. So while doing instanced rendering in a draw command places all instances in a single scope, rendering multiple times with a multidraw command with a base instance creates multiple scopes, one per internal draw.

Note: The above information in this section is based on GLSL 4.60. As such, this may not be entirely correct for earlier versions; they defined the idea of "dynamically uniform", but didn't really go into explicit details. However, since they are based mainly on the hardware rather than the compiler, and this definition is really about the behavior of the hardware, it is not unreasonable to assume that they apply to older GLSL versions as well.

Uses[edit]

There are places where GLSL requires the expression to be dynamically uniform. All of the following must use a dynamically uniform expression:

  • The array index for arrays of opaque types.
  • The index to buffer-backed interface block arrays. Note that arrays within blocks do not have this restriction. It only applies to a block which is arrayed.
  • In Compute Shaders, the expressions leading to the execution of a barrier() call. So if there is a loop or a conditional statement or somesuch, all of these conditions must be dynamically uniform expressions, so that every compute shader invocation in the same workgroup encounters every barrier() statement in the same sequence and the same number of times.

Examples[edit]

V · E

This example code shows what are and are not dynamically uniform expressions.

in vec3 fromPrevious;
in uvec2 fromRange;

const int foo = 5;
const uvec2 range = uvec2(2, 5);
uniform vec2 pairs;

uniform sampler2d tex;

void main()
{
  foo; // constant expressions are dynamically uniform.
  
  uint value = 21; // 'value' is dynamically uniform.
  value = range.x; // still dynamically uniform.
  value = range.y + fromRange.y; // not dynamically uniform; current contents come from a non-dynamically uniform source.
  value = 4; // dynamically uniform again.
  if (fromPrevious.y < 3.14)
    value = 12;
  value; // NOT dynamically uniform. Current contents depend on 'fromPrevious', an input variable.

  float number = abs(pairs.x); // 'number' is dynamically uniform.
  number = sin(pairs.y); // still dynamically uniform.
  number = cos(fromPrevious.x); // not dynamically uniform.

  vec4 colors = texture(tex, pairs.xy); // dynamically uniform, even though it comes from a texture.
                                        // It uses the same texture coordinate, thus getting the same texel every time.
  colors = texture(tex, fromPrevious.xy); // not dynamically uniform.

  for(int i = range.x; i < range.y; ++i)
  {
       // loop initialized with, compared against, and incremented by dynamically uniform expressions.
    i; // Therefore, 'i' is dynamically uniform, even though it changes.
  }

  for(int i = fromRange.x; i < fromRange.y; ++i)
  {
    i; // 'i' is not dynamically uniform; 'fromRange' is not dynamically uniform.
  }
}


Functions[edit]

Functions can be defined in GLSL. Function declarations and definitions work similarly in GLSL compared to C/C++, but there are a few gotchas here.

Just like in C/C++, execution of a shader begins with the function main. This function takes no parameters and returns no values.

User-defined functions can be declared and defined. GLSL even allows user-defined functions to use function overloading; the same function name can be defined multiple times with different parameter lists.

Recursion[edit]

The C/C++ function model allows functions to be recursive. That is, function A can call function B, which itself calls function A. Indeed, function A can call itself. Obviously, there has to be some condition to prevent infinite recursion, but C/C++ allows this to work.

GLSL does not. The GLSL memory model does not allow for recursive function calls. This allows GLSL to execute on hardware that simply doesn't allow for recursion. It allows GLSL to function when there is no ability to write arbitrarily to memory, which is true of most shader hardware (though it is becoming less true with time).

So, no recursion in GLSL. Of any kind.

Parameters[edit]

GLSL functions are declared and defined similarly to C/C++ functions. A function declaration in GLSL looks like this:

void MyFunction(in float inputValue, out int outputValue, inout float inAndOutValue);

Functions in GLSL use a calling convention called "value-return." This means that values passed to functions are copied into parameters when the function is called, and outputs are copied out when the function returns.

The in, out, and inout qualifiers are not the same as type qualifiers, even though some of them are named the same. These are parameter qualifiers, and they have a different meaning here.

A parameter declared as in means that the value given to that parameter will be copied into the parameter when the function is called. The function may then modify that parameter as they see fit, but those changes will not affect the calling code.

A parameter declared as out will not have its value initialized by the caller. The function will modify the parameter, and after the function's execution is complete, the value of the parameter will be copied out into the variable that the user specified when calling the function. Note that the initial value of the parameter at the start of the function being called is undefined, just as if one had simply created a local variable.

The inout declaration combines both. The parameter's value will be initialized by the value supplied by the user, and its final value will be output

The default if no qualifier is specific is in.

Consider the following:

void MyFunction(in float inputValue, out int outputValue, inout float inAndOutValue)
{
  inputValue = 0.0;
  outputValue = int(inAndOutValue + inputValue);
  inAndOutValue = 3.0;
}

void main()
{
  float in1 = 10.5;
  int out1 = 5;
  float out2 = 10.0;
  MyFunction(in1, out1, out2);
}

MyFunction will receive a 10.5, an undefined value, and a 10.0 as parameters. After the function is called, the values in main are:

in1 10.5
out1 10
out2 3.0

Notice that the value of in1 does not change.

For parameters declared as out or inout, the caller must pass an l-value. To put it simply, it must be a variable that can be set. So it cannot be an expression (calling MyFunction(in1, 2+4, out2) is an error). They also cannot be called on a value that is unsettable. This includes shader inputs (global variables declared with the in qualifier), uniforms, or const-qualified values.

Functions can have return values just like C/C++. But the out and inout qualifiers allow functions to return multiple values.

Input parameters can be defined with const; this means that they cannot be changed. The const qualifier must come before the in qualifier.

Control flow[edit]

GLSL uses the standard C/C++ set of statements. It has selection statements (if-else and switch-case), iteration statements (for, while, and do-while), and Jump statments (break, continue, and return). These statements work essentially as C++ defines them (you can declare variables in a for statement, for example), though there are some limitations. For example, you can declare variables in if conditions in C++, but not in GLSL.

Notice that goto is absent from the list of jump statements. GLSL has no goto construct.

It does have a new jump command (effectively): discard. This keyword can only be used in Fragment Shaders. It causes the fragment shader's results to be discarded and ignored by the rest of the pipeline. That fragment's value(s) will not be written to the framebuffer.

Note: This does not mean that fragment shaders actually stop processing once the discard command is reached. In general, fragment shaders are grouped into blocks of four; so long as one of them is running, they all must be running and executing the same code. Because of that, only discarding from all four fragments will cause processing to stop.

Using discard is different from return from main. Executing return still means that the values written to the shader outputs will be used by the rest of the pipeline. Executing discard means that they will not. Also, returning before writing outputs to all of the outputs yields undefined behavior for the unwritten outputs, whereas discard has defined behavior for unwritten outputs.