Search code examples
c++openglglslpreprocessor

How does the #line directive work in GLSL?


I have an OpenGL program where the shader sources are defined using an array. Vertex and fragment shader are stored in the single file and compiled based on a macro definition. For the simplicity a shader file is defined as const char* in the following example:

const char* ver = "#version 120\n";
const char* vsDef = "#define VERTEX_SHADER\n";
const char* fsDef = "#define FRAGMENT_SHADER\n";
const char* body =
"#line 0 1\n"
"#ifdef VERTEX_SHADER\n"
"void main()\n"
"{\n"
"    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
"}\n"
"#endif\n"
"#ifdef FRAGMENT_SHADER\n"
"void main()\n"
"{\n"
"    gl_FragData[0] = vec4(0.0, 1.0, 1.0, 1.0);\n"
"}\n"
"#endif\n";

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);

const char* src[3] = { ver, vsDef, body };
int length[3] = { strlen(ver), strlen(vsDef), strlen(body) };

glShaderSource(vertexShader, 3, src, length);
glCompileShader(vertexShader);

//next compile fragment shader, link program etc.

How the line directive - #line 0 1 works in the above example ? Does it has something to do with defining shader source using an array and a shader logs which can be read using glGetShaderInfoLog ?


Solution

  • Formal definition

    Form GLSL Preprocessor directives section:

    #line directive

    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.

    Plus one (quite important) remark mentioned in the following section:

    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.

    Answering the questions

    How the line directive - #line 0 1 works in the above example ?

    It works pretty much like the same-named directive from C/C++ language. I.e. from the line followed by this directive, the value __LINE__ and __FILE__ macroses are manually changed, and the track of line number and file name remains altered until end of the translation unit. E.g.

    // file foo.cpp
    int logLines() {
        std::cout << __FILE__ << ":" << __LINE__ << std::endl; // foo.cpp:3
    #line 256 "bar.cpp"
        std::cout << __FILE__ << ":" << __LINE__ << std::endl; // bar.cpp:256
        // Pay attention, that effect remains active from this point forward. See the output of the next line
        std::cout << __FILE__ << ":" << __LINE__ << std::endl; // bar.cpp:258
        return 0;
    }
    

    This, however, is predominantly debugging information, it does not alter the code and (to my knowledge) is used purely by compilers to report the proper line in the source in case this text was changed after optimisation/preprocessing/obfuscation. So as a consumer of a programming language, you don't usually use this directive, but it helps to understand where a problem is if it happens.

    In case of GLSL the situation is slightly different: it doesn't has the notions of files, but instead use unnamed source strings (referred by numbers). As you can see from the previous section, in place of the second argument this directive accepts so-called source-string-number. I explain what this number is and where you, as a GLSL programmer, can get use of it in the following sections. Also, unlike C #line directive, and despite what the GLSL documentation says, the GLSL #line directive seems to apply changes for the line where the directive itself is located, not the line which follows it, if you set #line 4, consider the next line to be 5'th, not 4'th.

    Does it has something to do with defining shader source using an array...

    Not exactly. No matter which way you pass the source code to the glShaderSource function, all parts get concatenated into a single source-string, with the same source-string-number. The following example:

    const GLchar* ver = "#version 120\n";
    const GLchar* body =
    "attribute vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\n";
    const GLchar* sources[] { ver, body };
    glShaderSource(shader, 2, sources, nullptr);
    

    Is considered identical to this:

    const GLchar* body =
    "#version 120\n"
    "attribute vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\n";
    glShaderSource(shader, 1, &body, nullptr);
    

    The reason to split the source code sometimes is just to reuse some parts of GLSL code.

    Does it has something to do with ... a shader logs which can be read using glGetShaderInfoLog ?

    This is what it's all about essentially. If you have the source code split between different strings it gets more complicated to locate the problematic line of code. Let's remove the #line directive and add some ill-formed text in the following shader:

     1 #ifdef VERTEX_SHADER
     2 attribute vec3 aPos;
     3 void main()
     4 {
     5     error; // ill-formed part
     6     gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
     7 }
     8 #endif
     9 #ifdef FRAGMENT_SHADER
    10 void main()
    11 {
    12     gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    13 }
    14 #endif
    

    Like in your example, I complement it with two other strings, which define shader type and GLSL version:

    const GLchar* ver = "#version 120\n";
    const GLchar* vsDef = "#define VERTEX_SHADER\n";
    const GLchar* sources[] { ver, vsDef, body };
    glShaderSource(shader, 3, sources, nullptr);
    glCompileShader(shader);
    

    If at this point you read the compilation error message, it will look something like this:

    ERROR: 0:7: Use of undeclared identifier 'error'
    

    The numbers next to ERROR: denote where the ill-formed part of GLSL code is located. The latter number 7 is the code line, while the former 0 is exactly what we called source-string-number earlier. If you now go the content of body and try to examine 7'th line, you will not find anything related, because, as we discussed earlier, all three strings are just concatenated in a single one, and in order to see where exactly the error is now, we have to concatenate the sources ourselves, so it looks like that:

     1 #version 120
     2 #define VERTEX_SHADER
     3 #ifdef VERTEX_SHADER
     4 attribute vec3 aPos;
     5 void main()
     6 {
     7     error; // ill-formed part
    ...
    

    However, if you alter the line manually (like it's done in your example):

       #line 0 1
     1 #ifdef VERTEX_SHADER
     2 attribute vec3 aPos;
     3 void main()
     4 {
     5     error; // ill-formed part
     ...
    

    you don't even have to concatenate the strings, the log will report it relative to the beginning of the body string:

    ERROR: 1:5: Use of undeclared identifier 'error'
    

    That might not look like a big deal, if you work with just a one big string, which contains 99% of the GLSL code, but if your code is split between dozens of strings, which can be composed in arbitrary order, this technique comes quite handy. The possibility to change the source-string-number (the second argument of the directive) is also very useful, because you can assign different number to each piece of code and just look through your strings by this line directive. E.g. for the error example above, i can quickly search source code which contains #line 0 1, and locate which string the error is in (if i have another big chunk of code under another string, i can put #line 0 2 to differentiate it from other parts of GLSL code)