Browse code

Add preprocessing include

Robert Cranston authored on 26/02/2021 16:36:35
Showing 21 changed files

... ...
@@ -17,6 +17,7 @@ add_library(${PROJECT_NAME})
17 17
 include(common.cmake)
18 18
 common(
19 19
     CXX_STANDARD 11
20
+    DISABLE_WSHADOW
20 21
     PACKAGES
21 22
         OpenGL
22 23
         GLEW
... ...
@@ -169,6 +169,39 @@ defines)` are injected after the `#version`. `Shader::Defines` is an alias for
169 169
 
170 170
 [`#define`]: https://www.khronos.org/opengl/wiki/Core_Language_(GLSL)#Preprocessor_directives
171 171
 
172
+#### `#include`s
173
+
174
+Support for [`#include`][]s is provided, with the same syntax as the
175
+[`ARB_shading_language_include`][] extension. An `#include`d path must be
176
+surrounded by `<` `>`, in which case it is treated as relative to
177
+[`root`](#loading-files), or by `"` `"`, in which case it is treated as
178
+relative to the including file.
179
+
180
+Even though the extension is not used for including files, an `#extension
181
+GL_ARB_shading_language_include : require` line is required for portability,
182
+otherwise an error is thrown. The line is silently dropped before the source is
183
+fed to OpenGL so as to not upset the shader compiler if the extension is not
184
+available.
185
+
186
+If the [`ARB_shading_language_include`][] extension is available, `#line`
187
+statements that include the file path are injected before every line (after the
188
+`#version`) to improve error messages from the shader compiler.
189
+
190
+Note that the `#include` statements are expanded before the source is fed to
191
+OpenGL, and therefore before the [GLSL preprocessor][] is run which means
192
+things like [include guard][]s do not work. (For portability, it is recommended
193
+that include guards be used anyway.) Circular includes are broken by skipping
194
+`#include`s where the *including file, line number, included file* tuple has
195
+been seen before.
196
+
197
+[`#include`]: https://www.khronos.org/opengl/wiki/Core_Language_(GLSL)#Preprocessor_directives
198
+[`glCompileShader`]: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glCompileShader.xhtml
199
+[GLSL preprocessor]: https://www.khronos.org/opengl/wiki/Core_Language_(GLSL)#Preprocessor_directives
200
+[include guard]: https://en.wikipedia.org/wiki/Include_guard
201
+[`ARB_shading_language_include`]: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_shading_language_include.txt
202
+[`glNamedStringARB`]: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_shading_language_include.txt
203
+[`glCompileShaderIncludeARB`]: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_shading_language_include.txt
204
+
172 205
 ## Dependencies
173 206
 
174 207
 Public (interface):
... ...
@@ -1,7 +1,13 @@
1 1
 #version 110
2 2
 
3 3
 
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "all_green.h"
8
+
9
+
4 10
 void main()
5 11
 {
6
-    gl_FragColor = vec4(red, 0.5, 0.25, 1.0);
12
+    gl_FragColor = vec4(red, green, 0.25, 1.0);
7 13
 }
... ...
@@ -1,6 +1,23 @@
1 1
 #version 110
2 2
 
3 3
 
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include <tests/all_topdir.h>
8
+#include "all_topdir.h"
9
+#include <tests/all_subdir/all_subdir.h>
10
+#include "all_subdir/all_subdir.h"
11
+#include </tests/all_topdir.h>
12
+#include "/all_topdir.h"
13
+#include </tests/all_subdir/all_subdir.h>
14
+#include "/all_subdir/all_subdir.h"
15
+#include "all_recursive.h"
16
+#include <tests/all_recursive.h>
17
+#include "all_subdir/all_recursive.h"
18
+#include <tests/all_subdir/all_recursive.h>
19
+
20
+
4 21
 void main()
5 22
 {
6 23
     gl_Position = gl_Vertex;
7 24
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+#define green 0.5
0 2
new file mode 100644
... ...
@@ -0,0 +1,9 @@
1
+#ifndef RECURSIVE_H_
2
+#define RECURSIVE_H_
3
+
4
+
5
+#include "all_recursive.h"
6
+#include <tests/all_recursive.h>
7
+
8
+
9
+#endif // RECURSIVE_H_
0 10
new file mode 100644
... ...
@@ -0,0 +1,11 @@
1
+#ifndef SUBDIR_RECURSIVE_H_
2
+#define SUBDIR_RECURSIVE_H_
3
+
4
+
5
+#include <tests/all_subdir/all_recursive.h>
6
+#include "../all_subdir/all_recursive.h"
7
+#include <../../../../../tests/all_subdir/all_recursive.h>
8
+#include "../../../../../tests/all_subdir/all_recursive.h"
9
+
10
+
11
+#endif // SUBDIR_RECURSIVE_H_
0 12
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+#include <tests/all_topdir.h>
2
+#include "all_subdir_other.h"
0 3
new file mode 100644
1 4
new file mode 100644
2 5
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+#version 110
2
+
3
+
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "bad_include_malformed.h" malformed.h"
8
+
9
+
10
+void main()
11
+{
12
+    gl_Position = gl_Vertex;
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,10 @@
1
+#version 110
2
+
3
+
4
+#extension extension
5
+
6
+
7
+void main()
8
+{
9
+    gl_Position = gl_Vertex;
10
+}
0 11
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+#version 110
2
+
3
+
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "bad_include_quotes.h>
8
+
9
+
10
+void main()
11
+{
12
+    gl_Position = gl_Vertex;
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+#extension GL_ARB_shading_language_include : require
0 2
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+#version 110
2
+
3
+
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "include_nested_extension.h"
8
+
9
+
10
+void main()
11
+{
12
+    gl_Position = gl_Vertex;
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+#version 110
0 2
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+#version 110
2
+
3
+
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "include_nested_version.h"
8
+
9
+
10
+void main()
11
+{
12
+    gl_Position = gl_Vertex;
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,10 @@
1
+#version 110
2
+
3
+
4
+#include "bad_include_noextension.h"
5
+
6
+
7
+void main()
8
+{
9
+    gl_Position = gl_Vertex;
10
+}
0 11
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+#version 110
2
+
3
+
4
+#extension GL_ARB_shading_language_include : require
5
+
6
+
7
+#include "bad_include_nonexistent.h"
8
+
9
+
10
+void main()
11
+{
12
+    gl_Position = gl_Vertex;
13
+}
... ...
@@ -5,6 +5,7 @@
5 5
 #include <cerrno>
6 6
 #include <cstring>
7 7
 #include <fstream>
8
+#include <list>
8 9
 #include <memory>
9 10
 #include <regex>
10 11
 #include <sstream>
... ...
@@ -81,21 +82,48 @@ static std::string source_(
81 82
     std::string const & error,
82 83
     std::string const & path,
83 84
     std::string const & root,
84
-    Shader::Defines const & defines
85
+    Shader::Defines const & defines,
86
+    std::string extension_behavior = {},
87
+    std::list<Here> included_by = {}
85 88
 )
86 89
 {
87 90
     // Set here error.
88
-    auto const here_error = [](Here const & here)
91
+    auto const here_error = [&](std::list<Here> const & include_here)
89 92
     {
90
-        return STR(
91
-            std::get<0>(here) << ":" <<
92
-            std::get<1>(here) << ": " <<
93
-            std::get<2>(here)
93
+        return STR_JOIN(
94
+            "\n",
95
+            it,
96
+            std::get<0>(it) << ":" <<
97
+            std::get<1>(it) << ": " <<
98
+            std::get<2>(it),
99
+            include_here
94 100
         );
95 101
     };
96 102
 
103
+    // Set include helper.
104
+    auto const include_here = [&](Here const & here)
105
+    {
106
+        auto include_here = included_by;
107
+        include_here.push_front(here);
108
+        return include_here;
109
+    };
110
+
97 111
     // Set full path.
98
-    auto path_full = path;
112
+    auto path_full = std::string{};
113
+    {
114
+        auto istream = std::istringstream(path);
115
+        auto part    = std::string{};
116
+        auto parts   = std::vector<std::string>{};
117
+        parts.reserve((size_t)std::count(path.begin(), path.end(), '/') + 1);
118
+        while (std::getline(istream, part, '/'))
119
+        {
120
+            if (part == ".." && !parts.empty())
121
+                parts.pop_back();
122
+            if (part != ".." && part != ".")
123
+                parts.push_back(std::move(part));
124
+        }
125
+        path_full = STR_JOIN('/', it, it, parts);
126
+    }
99 127
     if (!root.empty())
100 128
         path_full = STR(root << "/" << path_full);
101 129
 
... ...
@@ -105,6 +133,7 @@ static std::string source_(
105 133
         throw std::runtime_error{STR(
106 134
             error << "; " <<
107 135
             "could not open file '" << path_full << "':\n" <<
136
+            here_error(included_by) << (included_by.empty() ? "" : ":\n") <<
108 137
             std::strerror(errno)
109 138
         )};
110 139
 
... ...
@@ -112,16 +141,21 @@ static std::string source_(
112 141
     auto ostream = std::ostringstream{};
113 142
 
114 143
     // Define parse regexes.
115
-    static auto const re_ignored = std::regex{R"(\s*//.*$)"};
116
-    static auto const re_words   = std::regex{R"((\w+(?:\s+\w+)*))"};
117
-    static auto const re_version = std::regex{R"(\s*#\s*version\s*(.*))"};
144
+    static auto const re_ignored   = std::regex{R"(\s*//.*$)"};
145
+    static auto const re_words     = std::regex{R"((\w+(?:\s+\w+)*))"};
146
+    static auto const re_spec      = std::regex{R"((\w+)\s*:\s*(\w+))"};
147
+    static auto const re_quoted    = std::regex{R"((["<])([^">]*)([">]))"};
148
+    static auto const re_version   = std::regex{R"(\s*#\s*version\s*(.*))"};
149
+    static auto const re_extension = std::regex{R"(\s*#\s*extension\s*(.*))"};
150
+    static auto const re_include   = std::regex{R"(\s*#\s*include\s*(.*))"};
118 151
 
119 152
     // Parse.
120
-    auto version_number = 0;
121
-    auto line_number    = 0;
122
-    auto line           = std::string{};
123
-    auto match          = std::smatch{};
124
-    auto here           = [&]()
153
+    auto version_number    = 0;
154
+    auto extension_enabled = false;
155
+    auto line_number       = 0;
156
+    auto line              = std::string{};
157
+    auto match             = std::smatch{};
158
+    auto here              = [&]()
125 159
     {
126 160
         return Here{path_full, line_number, line};
127 161
     };
... ...
@@ -130,6 +164,16 @@ static std::string source_(
130 164
         // Remove ignored.
131 165
         auto const content = std::regex_replace(line, re_ignored, "");
132 166
 
167
+        // Output `#line`.
168
+        auto const line_number_offset = version_number < 330 ? -1 : 0;
169
+        if (GLEW_ARB_shading_language_include)
170
+            if (!extension_behavior.empty() && extension_behavior != "disable")
171
+                ostream
172
+                    << "#line" << " "
173
+                    << line_number + line_number_offset << " "
174
+                    << "\"" << path_full << "\""
175
+                    << "\n";
176
+
133 177
         // Process version.
134 178
         if (std::regex_match(content, match, re_version))
135 179
         {
... ...
@@ -139,7 +183,7 @@ static std::string source_(
139 183
                 throw std::runtime_error{STR(
140 184
                     error << "; " <<
141 185
                     "malformed #version:\n" <<
142
-                    here_error(here())
186
+                    here_error(include_here(here()))
143 187
                 )};
144 188
             auto const version = match.str(1);
145 189
 
... ...
@@ -148,7 +192,13 @@ static std::string source_(
148 192
                 throw std::runtime_error{STR(
149 193
                     error << "; " <<
150 194
                     "found repeated #version:\n" <<
151
-                    here_error(here())
195
+                    here_error(include_here(here()))
196
+                )};
197
+            if (!included_by.empty())
198
+                throw std::runtime_error{STR(
199
+                    error <<  "; " <<
200
+                    "found #version in #include:\n" <<
201
+                    here_error(include_here(here()))
152 202
                 )};
153 203
 
154 204
             // Process.
... ...
@@ -156,6 +206,16 @@ static std::string source_(
156 206
 
157 207
             // Output.
158 208
             ostream << line << "\n";
209
+            if (GLEW_ARB_shading_language_include)
210
+            {
211
+                if (extension_behavior.empty())
212
+                {
213
+                    extension_behavior = "enable";
214
+                    ostream
215
+                        << "#extension GL_ARB_shading_language_include : "
216
+                        << extension_behavior << "\n";
217
+                }
218
+            }
159 219
             for (auto const & define : defines)
160 220
                 ostream
161 221
                     << "#define "
... ...
@@ -163,6 +223,103 @@ static std::string source_(
163 223
                     << define.second << "\n";
164 224
         }
165 225
 
226
+        // Process extension.
227
+        else if (std::regex_match(content, match, re_extension))
228
+        {
229
+            // Parse.
230
+            auto const spec = match.str(1);
231
+            if (!std::regex_match(spec, match, re_spec))
232
+                throw std::runtime_error{STR(
233
+                    error << "; " <<
234
+                    "malformed #extension:\n" <<
235
+                    here_error(include_here(here()))
236
+                )};
237
+            auto const extension = match.str(1);
238
+            auto const behavior  = match.str(2);
239
+
240
+            if (extension == "GL_ARB_shading_language_include")
241
+            {
242
+                // Check for errors.
243
+                if (!included_by.empty())
244
+                    throw std::runtime_error{STR(
245
+                        error <<  "; " <<
246
+                        "found #extension GL_ARB_shading_language_include " <<
247
+                        "in #include:\n" <<
248
+                        here_error(include_here(here()))
249
+                    )};
250
+
251
+                // Process.
252
+                extension_enabled = behavior != "disable";
253
+                extension_behavior = behavior;
254
+                line = "";
255
+            }
256
+
257
+            // Output.
258
+            ostream << line << "\n";
259
+        }
260
+
261
+        // Process include.
262
+        else if (std::regex_match(content, match, re_include))
263
+        {
264
+            // Parse.
265
+            auto const quoted = match.str(1);
266
+            if (!std::regex_match(quoted, match, re_quoted))
267
+                throw std::runtime_error{STR(
268
+                    error << "; " <<
269
+                    "malformed #include:\n" <<
270
+                    here_error(include_here(here()))
271
+                )};
272
+            auto const quote_open   = match.str(1);
273
+            auto const include_path = match.str(2);
274
+            auto const quote_close  = match.str(3);
275
+
276
+            // Check for errors.
277
+            if (!(
278
+                (quote_open == "\"" && quote_close == "\"") ||
279
+                (quote_open == "<"  && quote_close == ">" )
280
+            ))
281
+                throw std::runtime_error{STR(
282
+                    error << "; " <<
283
+                    "mismatched #include quotes '" << quote_open << "' and '"
284
+                    << quote_close << "':\n" <<
285
+                    here_error(include_here(here()))
286
+                )};
287
+            if (!extension_enabled && included_by.empty())
288
+                throw std::runtime_error{STR(
289
+                    error << "; " <<
290
+                    "#include found but #extension " <<
291
+                    "GL_ARB_shading_language_include not enabled:\n" <<
292
+                    here_error(include_here(here()))
293
+                )};
294
+
295
+            // Process.
296
+            auto source = std::string{};
297
+            if (included_by.end() == std::find(
298
+                included_by.begin(), included_by.end(), here()
299
+            ))
300
+            {
301
+                auto include_path_full = include_path;
302
+                if (quote_open == "\"")
303
+                {
304
+                    auto const pos = path.rfind('/');
305
+                    if (pos != path.npos && pos != 0)
306
+                        include_path_full = STR(
307
+                            path.substr(0, pos + 1) << include_path
308
+                        );
309
+                }
310
+                source = source_(
311
+                    error,
312
+                    include_path_full, root,
313
+                    defines,
314
+                    extension_behavior,
315
+                    include_here(here())
316
+                );
317
+            }
318
+
319
+            // Output.
320
+            ostream << source << "\n";
321
+        }
322
+
166 323
         // Non-processed line.
167 324
         else
168 325
         {
... ...
@@ -172,7 +329,7 @@ static std::string source_(
172 329
     }
173 330
 
174 331
     // Check for version.
175
-    if (!version_number)
332
+    if (!version_number && included_by.empty())
176 333
         throw std::runtime_error{STR(
177 334
             error << "; " <<
178 335
             "found no #version."
... ...
@@ -31,7 +31,7 @@ GLTEST(2, 0, 640, 480, glshader)
31 31
     GLTEST_EXPECT_EXCEPTION(true,
32 32
         Shader({"tests/create_bad_compile.vert"}),
33 33
         "Failed to compile shader 'tests/create_bad_compile.vert' of shader program 'tests/create_bad_compile.vert':\n"
34
-        "0:4(5): error: main() must return void\n"
34
+        "\"assets/shaders/tests/create_bad_compile.vert\":4(5): error: main() must return void\n"
35 35
     )
36 36
     GLTEST_EXPECT_EXCEPTION(true,
37 37
         Shader({"tests/create_bad_link.vert"}),
... ...
@@ -78,6 +78,53 @@ GLTEST(2, 0, 640, 480, glshader)
78 78
         {"red", std::to_string(red)},
79 79
     });
80 80
 
81
+    // Include.
82
+    GLTEST_EXPECT_EXCEPTION(true,
83
+        Shader({"tests/include_malformed_extension.vert"}),
84
+        "Failed to source shader 'tests/include_malformed_extension.vert' of shader program 'tests/include_malformed_extension.vert'; "
85
+        "malformed #extension:\n"
86
+        "assets/shaders/tests/include_malformed_extension.vert:4: #extension extension"
87
+    )
88
+    GLTEST_EXPECT_EXCEPTION(true,
89
+        Shader({"tests/include_malformed.vert"}),
90
+        "Failed to source shader 'tests/include_malformed.vert' of shader program 'tests/include_malformed.vert'; "
91
+        "malformed #include:\n"
92
+        "assets/shaders/tests/include_malformed.vert:7: #include \"bad_include_malformed.h\" malformed.h\""
93
+    )
94
+    GLTEST_EXPECT_EXCEPTION(true,
95
+        Shader({"tests/include_mismatched_quotes.vert"}),
96
+        "Failed to source shader 'tests/include_mismatched_quotes.vert' of shader program 'tests/include_mismatched_quotes.vert'; "
97
+        "mismatched #include quotes '\"' and '>':\n"
98
+        "assets/shaders/tests/include_mismatched_quotes.vert:7: #include \"bad_include_quotes.h>"
99
+    )
100
+    GLTEST_EXPECT_EXCEPTION(true,
101
+        Shader({"tests/include_no_extension.vert"}),
102
+        "Failed to source shader 'tests/include_no_extension.vert' of shader program 'tests/include_no_extension.vert'; "
103
+        "#include found but #extension GL_ARB_shading_language_include not enabled:\n"
104
+        "assets/shaders/tests/include_no_extension.vert:4: #include \"bad_include_noextension.h\""
105
+    )
106
+    GLTEST_EXPECT_EXCEPTION(true,
107
+        Shader({"tests/include_nonexistent.vert"}),
108
+        "Failed to source shader 'tests/include_nonexistent.vert' of shader program 'tests/include_nonexistent.vert'; "
109
+        "could not open file 'assets/shaders/tests/bad_include_nonexistent.h':\n"
110
+        "assets/shaders/tests/include_nonexistent.vert:7: #include \"bad_include_nonexistent.h\":\n"
111
+        "No such file or directory"
112
+    )
113
+    GLTEST_EXPECT_EXCEPTION(false,
114
+        Shader({"tests/include_nested_version.vert"}),
115
+        "Failed to source shader 'tests/include_nested_version.vert' of shader program 'tests/include_nested_version.vert'; "
116
+        "found #version in #include:\n"
117
+        "assets/shaders/tests/include_nested_version.h:1: #version 110\n"
118
+        "assets/shaders/tests/include_nested_version.vert:7: #include \"include_nested_version.h\""
119
+    )
120
+    GLTEST_EXPECT_EXCEPTION(false,
121
+        Shader({"tests/include_nested_extension.vert"}),
122
+        "Failed to source shader 'tests/include_nested_extension.vert' of shader program 'tests/include_nested_extension.vert'; "
123
+        "found #extension GL_ARB_shading_language_include in #include:\n"
124
+        "assets/shaders/tests/include_nested_extension.h:1: #extension GL_ARB_shading_language_include : require\n"
125
+        "assets/shaders/tests/include_nested_extension.vert:7: #include \"include_nested_extension.h\""
126
+    )
127
+
81 128
     auto all = Shader({
82 129
         "tests/all.vert",
83 130
         "tests/all.frag",