| Age | Commit message (Collapse) | Author |
|
Closes #38
- Change overlapping bindings case from error to warning (it is *technically* allowed in HLSL/GLSL)
- Make diagnostic messages for these cases include a note to point at the "other" declaration in each case, so that user can more easily isolate the problem
- Unrelated fix: make sure `slangc` sets up its diagnostic callback *before* parsing command-line options so that error messages output during options parsing will be visible
- Unrelated fix: make sure that formatting for diagnostic messages doesn't print diagnostic ID for notes (all have IDs < 0).
- Note: eventually I'd like to not print diagnostic IDs at all (I think they are cluttering up our output), but doing that requires touching all the test cases...
|
|
Fixes #160
If the front-end runs into a type it doesn't understand in the parameter list of an entry point, it will create an `ErrorType` for that parameter, but then the parameter binding/layout rules will fail to create a `TypeLayout` for the prameter (and return `NULL`).
There were some places where the code was expecting that operation to succeed unconditionally, and so would crash when there was a bad type.
The specific case in the bug report was when the return type of a shader entry point was bad:
// `vec4` is not an HLSL type
vec4 main(...) { ... }
Note that the specific case in the buf report only manifests in "rewriter" mode (when the Slang compiler isn't allowed to issue error messages from the front-end), but the same basic thing would happen if the varying parameter/output had used a type that is invalid for varying input/output:
Texture2D main(...) { ... }
I'm not 100% happy with just adding more `NULL` checks for this, because there is no easy way to tell if they are exhaustive.
A better solution in the longer term might be to construct a kind of `ErrorTypeLayout` to represent cases where we wanted a type layout, but none could be constructed.
|
|
Fixes #23
Up to this point, the compiler has used the ordinary `String` type to represent declaration names, which means a bunch of lookup structures throughout the compiler were string-to-whatever maps, which can reduce efficiency.
It also means that things like the `Token` type end up carying a `String` by value and paying for things like reference-counting.
This change adds a `Name` type that is used to represent names of variables, types, macros, etc.
Names are cached and unique'd globally for a session, and the string-to-name mapping gets done during lexing.
From that point on, most mapping is from pointers, which should make all the various table lookups faster.
More importantly (possibly), this brings us one step closer to being able to pool-allocate the AST nodes.
|
|
This is in preparation for using `Name` as a type name.
|
|
Just like the previous change did for declaration keywords, this change uses the lexical environment to drive the lookup and dispatch of modifier parsing.
This allows us to easily add modifiers to Slang, even when they might conflict with identifiers used in user code (because the modifier names are no longer special keywords, but ordinary identifiers).
There was already some support for ideas like this with `__modifier` declarations (`ModifierDecl`) used to introduce some GLSL-specific keywords (so that they wouldn't pollute the namespace of HLSL files).
The new approach changes these to be actual `syntax` declarations (`SyntaxDecl`) with the same representation as those used to introduce declaration keywords.
Because many modifiers just introduce a single keyword that maps to a simple AST node (no further tokens/data), I modified the handling of syntax declarations so that they can take a user-data parameter, and this allows the common case ("just create an AST node of this type...") to be handled with minimal complications.
This also adds in a general-purpose string-based lookup path for AST node classes, that should support programmatic creation in more cases.
Statements are now the main case of keywords that need to be made table driven.
|
|
The existing parser code was doing string-based matching on the lookahead token to figure out how to parse a declaration, e.g.:
```
if(lookAhead == "struct") { /* do struct thing */ }
else if(lookAhead == "interface") { /* do interface thing * }
...
```
That approach has some annoying down-sides:
- It is slower than it needs to be
- It is annoying to deal with cases where the available declaration keywords might differ by language
- Most importantly, it is not possible for us to introduce "extended" keywords that the user can make use of, but which can be ignored by the user and treated as an ordinary identifier.
That last part is important. Suppose the user wanted to have a local variable named `import`, but we also had a Slang extension that added an `import` keyword. Then a line of code like `import += 1` would lead to a failure because we'd try to parse an import declaration, even when it is obvious that the user meant their local variable. This would mean that Slang can't parse existing user code that might clash with syntax extensions. This issue is the reason why we currently have keywords like `__import`.
A traditional solution in a compiler is to map keywords to distinct token codes as part of lexing, which eliminates the first conern (performance) because now we can dispatch with `switch`. It can also aleviate the second concern if we add/remove names from the string->code mapping based on language (the rest of the parsing logic doesn't have to know about keywords being added/removed).
The solution we go for here is more aggressive.
Instead of mapping keyword names to special token codes during lexing, we instead introduce logical "syntax declarations" into the AST, which are looked up using the ordinary scoping rules of the language.
Depending on what code is imported into the scope where parsing is going on, different keywords may then be visible.
This solves our last concern, since a user-defined variable that just happens to use the same name as a keyword is now allowed to shadow the imported declaration for syntax (this is akin to, e.g., Scheme where there really aren't any "keywords").
This also opens the door to the possibility of eventually allowing user to define their own syntax (again, like Scheme).
For now I'm only using this for the declaration keywords.
With this change it should be pretty easy to also add statement keywords in the same fashion.
|
|
Fixes #24
So far the code has used a representation for source locations that is heavy-weight, but typical of research or hobby compilers: a `struct` type containing a line number and a (heap-allocated) string.
This is actually very convenient for debugging, but it means that any data structure that might contain a source location needs careful memory management (because of those strings) and has a tendency to bloat.
The new represnetation is that a source location is just a pointer-sized integer.
In the simplest mental model, you can think of this as just counting every byte of source text that is passed in, and using those to name locations.
Finding the path and line number that corresponds to a location involves a lookup step, but we can arrange to store all the files in an array sorted by their start locations, and do a binary search.
Finding line numbers inside a file is similarly fast (one you pay a one-time cost to build an array of starting offsets for lines).
More advanced compilers like clang actually go further and create a unique range of source locations to represent a file each time it gets included, so that they can track the include stack and reproduce it in diagnostic messages.
I'm not doing anything that clever here.
|
|
- `ExpressionSyntaxNode` becomes `Expr`
- `StatementSyntaxNode` becomes `Stmt`
- `StructSyntaxNode` becomes `StructDecl`
- `ProgramSyntaxNode` becomes `ModuleDecl`
- `ExpressionType` becomes `Type`
- Existing fields names `Type` become `type`
- There might be some collateral damage here if there were, e.g., `enum`s named `Type`, but I can live with that for now and fix those up as a I see them
|
|
The so-called "lowering" pass (really a kind of AST-to-AST legalization pass right now) needs to handle some basic scalarization of structured types, and it does this by inventing what I call "pseuo-expressions" and "pseudo-declarations."
For example, there is a pseudo-expression node type that represents a tuple of N other expressions, and certain operations act element-wise over such tuples.
The problem was that the implementation introduced these out-of-band expression/declaration types into the existing AST hierarchy which led to a dilemma:
- If these new AST nodes were declared like all the others (and integrated into the visitor dispatch approach, etc.) then every pass would need to deal with them even though they are meant to be a transient implementation detail of this one pass
- But if the new nodes *aren't* declared like the others, then they can't meaningfully interact with visitor dispatch, and will just crash the compiler if they somehow "leak" through to latter passes. And because they are just ordinary AST nodes from a C++ type-system perspective, such leaking is entirely possible (if not probable)
Hopefully that setup helps make the solution clear: instead of having the "lowering" pass map an expression to an expression, it needs to map an expression to a new data type (here called `LoweredExpr`) that can wrap *either* an ordinary expression (the common case) or one of the new out-of-band values. Any code that accepts a `LoweredExpr` needs to handle all the cases, or explicitly decide that it can't/won't deal with anything other than ordinary expressions.
Most of the code changes are straightforward at that point, although the whole "lowering" approach is a bit fiddly right now, so gertting the tests passing took a bit of attention. I'm not sure our test coverage of all this code is great, so I wouldn't be surprised if some failures are lurking still.
|
|
There were two main places where global variables were used in the Slang implementation:
1. The "standard library" code was generated as a string at run-time, and stored in a global variable so that it could be amortized across compiles.
2. The representation of types uses some globals (well, class `static` members) to store common types (e.g., `void`) and to deal with memory lifetime for things like canonicalized types.
In each case the "simple" fix is to move the relevant state into the `Session` type that controlled their lifetime already (the `Session` destructor was already cleaning up these globals to avoid leaks).
For the standard library stuff this really was easy, but for the types it required threading through the `Session` a bit carefully.
One more case that I found: there was a function-`static` variable used to generate a unique ID for files output when dumping of intermediates is enabled (this is almost strictly a debugging option).
Rather than make this counter per-session (which would lead to different sessions on different threads clobbering the same few files), I went ahead and used an atomic in this case.
Note that the remaining case I had been worried about was any function-`static` counter that might be used in generating unique names.
It turns out that right now the parser doesn't use such a counter (even in cases where it probably should), and the lowering pass already uses a counter local to the pass (again, whether or not this is a good idea).
This change should be a major step toward allowing an application to use Slang in multiple threads, so long as each thread uses a distinct `SlangSession`. The case of using a single session across multiple threads is harder to support, and will require more careful implementation work.
|
|
Fixes #11
- This adds a `-o` command-line option for specifying an output file.
- The code tries to be a bit smart, to glean an output format from a file extension, and also to associate multiple `-o` options with multiple `-entry` options if needed.
- There is a restriction that all the output files need to agree on the code generation target. This is reasonable for now, but might be something to lift eventualy
- There is a restriction that only one output file is allowed per entry point
- Together with the previous item this means you can't output both a `.spv` and a `.spv.asm` in one pass, even though both should be possible
- There is currently a restriction that output paths only apply to entry points
- This means there is no way to output reflection JSON to a file with `-o` (but that is mostly just a debugging feature for now)
- This also means we don't support any "container" formats that can encapsulate multiple compiled entry points
|
|
There was a bug where the intialization expression for a variable was being lowered after the declaration was added to the output code, so that any sub-expressions that get hoisted out actually get computed *after* the original variable. This obviously led to downstream compilation failure.
I've updated the test case to stress this scenario.
|
|
The basic bug there is that if you have a member of `struct` type in a `uniform` block and then pass a reference to that member directly to a call:
```
struct Foo { vec4 bar; };
uniform U { Foo foo; };
void main() { doSomething(foo); }
```
then glslang generates invalid SPIR-V which seems to cause an issue for some drivers.
This change works around the problem by detecting cases where an argument to a function call is a reference to `uniform` block member (of `struct` type) and then rewrites the code to move that value to a temporary before the call.
|
|
- We use this to work around the fact that, e.g., `Texture2D.Load` doesn't take a sampler, but the equivalent GLSL operation `texelFetch` requires one
- Previously we tried to hide the sampler from the user, hoping that glslang would drop it and we could just ignore it, but that doesn't work
- For now we'll go ahead and explicitly show the sampler in the reflection info so that an app can react appropriately
- We also generate a unique binding for the sampler, instead of the old behavior that fixed it with `binding = 0`
- We still fix it with `set = 0`, so it might still surprise users
|
|
|
|
Fixes #133
We already had logic to skip adding `flat` to a vertex input, and this just extends it to not adding `flat` to a fragment output.
Note that explicit qualifiers in the input HLSL/Slang will still be carried through to the output, so it is still possible for a Slang user to shoot themself in the foot with interpolation qualifiers.
|
|
- API users can use this to get "clean" output to aid with debugging Slang issues
- Also changes the prefix on intermediate files that Slang dumps, to make them easier to ignore with a regexp
|
|
The requirements for using `gl_Layer` differ by stage, and so we need to pick an appropriate GL version based on the target stage, and then also require a specific extension for anything other than geometry or fragment.
|
|
- The easy part here is treating `NV_` prefixed semantics as another case of "system-value" semantics
- Mapping the new semantics (`NV_X_RIGHT` and `NV_VIEWPORT_MASK`) to their GLSL equivalents is harder
- Instead of a single "right-eye vertex" output, GLSL defines an array of per-view positions
- Instead of a vector of masks, GLSL defines an array of per-view masks
- Another point here is that a lot of semantics that appear as `uint` in HLSL are `int` in GLSL, which can lead to conversion issues.
- The approach here is to have the lowering pass introduce a notion of assignment with "fixups," which will try to cast things as needed
- When assigning to a simple value with the "wrong" type, introduce a cast
- When assigning to an array from a vector, break out multiple assignments of individual vector/array elements
- In order to facilitate the above, I needed to add actual types to the magic expressions I introduce to represent GLSL builtin variables. These were taken by scanning the online documentation for GL, so they might not be perfect.
- Major issues with the approach in this change:
- No attempt is being made here to check that the original declaration used a type appropriate to the semantic. The assumption is that this logic only ever triggers for Slang entry points, or GLSL entry points using a Slang `struct` type for input/output (and for right now Slang code is only ever written by "understanding" developers)
- In the case of a Slang entry point, we always copy varying parameters in/out around the call to `main_`, so this approach should handle calls to functions with `out` or `in out` parameters okay, but it is *not* robust to cases where we don't want to copy in all the entry point parameters first thing (e.g., a GS), so that will have to change
- In the GLSL case (or if we revise the approach to Slang entry points), there is going to be a problem if these converted varying parameters are ever passed as arguments to `out` or `in out` parameters. In these cases we need to do more sleight-of-hand to reify a temporary variable and do the necessary copy-in/copy-out. Being able to do that logic relies on having correct information about callees, which requires having robust semantic analysis of the function body. There is only so much we can do...
- A better long-term approach would not rely on an ad-hoc "fixup" conversion during assignment, but would instead implement the GLSL builtin variables as, effectively, global "property" declarations that have both `get` and `set` accessors, and then tunnel a reference to such a property down through lowering, where it can lower to uses of the "getter" or "setter" as appropriate in context (and the result type of the getter/setter can be what we'd want/expect).
|
|
The change is mostly about trying to make sure the compiler "fails safe" when it encounters an internal assumption that isn't met.
Most internal errors will now throw exceptions (yes, exceptions are evil, but this will work for now), and these get caught in `spCompile` so that they don't propagate to the user (they just see a message that compilation aborted due to an internal error).
Subsequent changes are going to need to work on diagnosing as many of these situations as possible, so that users can at least know what construct in their code was unexpected or unhandled by the compiler.
|
|
- Change the `slang` project from a static library to a dynamic one
- Add some details around `slang.h` to make sure DLL export stuff is working
- Make the `slangc` executable use the dynamic library
- Rename the `glslang` sub-project to `slang-glslang` and move it into the main source hierarchy
- This reflects the fact that it isn't a stand-alone tool, and isn't in any way a standard binary of glslang, but rather just an artifact of how Slang uses glslang
|
|
Fixes #122
- In cases with an explicit mip level being specified, there was a mistake in how the argument for setting the mip level in the GLSL code was constructed that led to a parse error in GLSL
- Also, that argument is a `uint` in HLSL and an `int` in GLSL, so an explicit cast was needed
- The GLSL functions here seem to require a newer GLSL (at least higher than `420`), so I had to add in a capability for builtins to specify a required GLSL version. For now I made these ones require `450`.
- Added a test case to confirm that our lowering works (for some definition of "works")
|
|
We generate implicit names for global-scope parameter blocks (including HLSL `cbuffer`s, since the "name" the user sees is really just for reflection purposes), but this had a few problems:
- We used the generated names for parameter-binding purposes
- Except for a GLSL block with an explicit name, in which case we'd use the internal name and not the reflection name for matching
- The generated named didn't match between GLSL and HLSL/Slang declarations
This change tries to fix both of these issues. I changed the name generation to try to make it identical between HLSL and GLSL (to the extent we can control it), just in case. But then I also went and changed the parameter-binding-generation logic to use the *reflection* name instead of the internal name when deciding if things are the "same" parameter.
|
|
When lowering `buf[i]` to `texelFetch(..., i)` we need to deal with the case where the type of `b` might be `Buffer<float>` in which case we want to add a `.x` swizzle to the end of the fetch.
|
|
- This isn't going to work for writable buffers, and certainly not for writes
- As it exists right now, this shows a flaw in how I'm handling texture-type results on fetches
|
|
The basic syntax is:
$for(i in Range(0,99))
{
/* stuff goes here */
}
Note that the exact form is very restrictive. All that you are allowed to change is `i`, `0`, `99` or `/* stuff goes here */`.
As a tiny bit of syntax sugar, the following should work:
$for(i in Range(99))
{
/* stuff goes here */
}
Note that the range given is half-open (C++ iterator `[begin,end)` style).
Both the beginning and end of the range must be compile-time constant expressions that Slang knows how to constant-fold.
The implementation will basically generate code for `/* stuff goes here */` N times, once for each value in the half-open range.
Each time, the variable `i` will be replaced with a different compile-time-constant expression.
While I was working on a test case for this, I also found that our build of glslang had an issue with resource limits, so I fixed that.
Clients will need to build a new glslang to use the fix.
|
|
GLSL technically supports varying (`in`, `out`) parameters of `struct` type, but there are some annoying constraints (not allowed for VS input), and it doesn't work with how an HLSL user would usually put "system-value" inputs/outputs into a `struct` together with ordinary inputs/outputs.
To work around this, this change adds support for using an imported Slang `struct` type for an `in` or `out` parameter, in which case it will (1) be scalarized and (2) will have system-value semantics mapped appropriately, just as for an entry-point parameter when cross-compiling an HLSL-style `main()`.
Changes:
- Add a notion of a `VaryingTupleExpr` and `VaryingTupleVarDecl`, similar to those for the resources-in-structs case
- Trigger use of these when we have a global-scope varying in/out using an imported `struct` type
- Also use these in the cross-compilation case for ordinary varying input/output (since this approach seems like it should be more general, and can hopefully handle stuff like GS input/output some day)
- When generating parameter binding information, special case global-scope input/output, and treat it the same as entry-point-parameter input/output
- Revamp how used resource ranges are computed so that we can eventually make this specific to an entry point
- Actually implement first signs of life for `maybeMoveTemp` so that assignments to the tuple-ified outputs will work better
- Add first test case that actually seems to work
- Add diagnostics for conflicting explicit bindings on a parameter
- Add diagnostic for different parameters with overlapping bindings
- Make global-scope varying input/output use a tracking data structure specific to the translation unit for computing locations (so that they are independent of other TUs)
|
|
This is a straightforward mapping given the infrastructure already in place.
|
|
All varying input/output parameters need to be specified to the entry point that declared them.
In the case of HLSL/Slang this happens for free, but in the case of GLSL we need to be careful not to merge global-scope `in` or `out` parameters in ways that don't make sense.
|
|
- When generating parameter binding/reflection info, treated imported modules as Slang code, instead of the source language of the outer translation unit
- This fixes an issue where global-scope shader parameters in a `.slang` file were getting ignored for binding-generation purposes when imported by a GLSL file
|
|
Fixes #94
We'd been handling HLSL `Buffer` and `RWBuffer` in a one-off fashion, and that led to a lot of code duplication, and also to the issue that we weren't handling `RasterizerOrderedBuffer` at all.
This change basically folds `Buffer` in so that it is conceptually a texture type (just with a unique shape). Hopefully all the other logic still works.
|
|
The basic idea is that an array of `struct`s will get scalarized into per-field arrays (for any fields that need to be scalarized). So given:
struct Foo { float x; Texture2D t; };
cbuffer C { Foo foo[4]; }
We'll get output like:
struct Foo { float x; };
cbuffer C { Foo foo[4]; }
Texture2D C_foo_t[4];
(Of course the output would also be translated over to GLSL, but I'm only concerned about this one transformation here).
|
|
- This was an easy case, as far as these things go.
|
|
This is hacky for two big reasons:
1. It uses "operator comma" in the output to deal with calling multiple functions in an expression context
2. The way I'm lowering things to GLSL ends up using certain function arguments more than once, which means they get emitted as GLSL more than once, which means their *side effects* get evaluated more than once. Please don't put an expression with side effects in as an argument to `GetDimensions` when cross-compiling.
Solving these issues requires the translation of builtins to be more directly handled as part of lowering, rather than a purely textual operation done during emission. I don't have time to fix that right now.
|
|
- We map `SampleCmpLevelZero` to either `textureLod` or `textureGrad` based on what the GLSL spec seems to allow
- We map `SamplerComparisonState` to `samplerShadow` (instead of just `sampler`)
|
|
The behavior of the `linear` modifier should be the default interpolation behavior in GLSL.
|
|
`gl_Layer` as a fragment input requires at least version 4.30 of GLSL, so we try to track that information when we see the name used.
Note that this does *not* override a user-specified `#version` line.
This required re-ordering when lowering happens relative to emitting the `#version` directive, since this code works by actually modifying the chosen profile for the entry point.
Yes, that is kind of gross and we should do something cleaner in the long term.
|
|
Don't crash-fail on errors in entry point parameters
|
|
Work on #105
These can occur in unchecked code (or code that had a semantic error), so we need to be able to handle them.
|
|
- This was being mapped to `HLSLLineStreamType` because of a copy-paste typo
|
|
Work on #105
If a semantic error occurs in the type of an entry-point parameter, we need to be able to skip over it when doing parameter binding and reflection-generation work.
|
|
Fixes #103
- Previously I was relying on scalar-to-vector promotion to pick the right type in these cases, but I hadn't implemented scalar-to-matrix promotion (I should...)
- Rather than relying on promotion behavior, this change goes ahead and adds explicit overloads. I think this is probably a better decision in the long term, since one might want to support these cases for operators, while warning (or erroring) on the more general cases of implicit conversion.
- This covers matrix/scalar, scalar/matrix, vector/scalar, and scalar/vector cases
|
|
Fixes #104
- Map HLSL `nointerpolation` to GLSL `flat`
- When lowering a `struct` type varying input/output, look for interpolation modifiers along the "chain" from the leaf field up to the original shader input variable (and take the first one found)
- Not sure if this is strictly needed, but it seems like a reasonable policy
- Add `flat` to varying input of integer type, with no other interpolation modifier
- Note: I do *not* do anything to ignore a manually imposed interpolation modifier that might be incorrect
|
|
Fixes #15
These are the modifiers like:
layout(local_size_x = 16) in;
Unlike the HLSL case, these don't get attache to the entry point function itself, so there is a bit more work involed in looking them up.
Just to make sure I didn't mess up the HLSL case, I went ahead and added two tests for this capability: one for GLSL and one for HLSL.
|
|
If we have something like to following in HLSL:
cbuffer C { Texture2D t; ... }
and we are compiling to GLSL, then both `C` and `C.t` consume the same kind of resource (a descriptor-table slot).
The way reflection was working right now, querying the index of `C` would return its binding (let's say it is `4` just to be concrete) and then a query on `C::t` would give its offset, which was being computed as `0` because it is the first field in the logical `struct` type.
That obviously leads to bad math and requires some subtle `+1`s in cases to get things right (e.g., when scalaring during lowering, I had to carefully add one in some cases).
It is unreasonable to expect users to deal with this.
This commit changes it so that the offset of field `C::t` is `1` so that hopefully more things Just Work.
The special-case logic in lowering is now gone.
One important catch here is that this pretty much only works in the case where the element type of a parameter block is a `struct` type (which is really all that makes sense right now).
If we ever want to generalize this in the future, then it will probably be necessary to change the `TypeLayout` case for parameter blocks to store a `VarLayout` for the element, rather than just a `TypeLayout`.
|
|
Fixes #12
- This was a latent issue, but the previous commit brought it to the front.
- As indicated in #12, I don't allocate a descriptor-table slot to the block
- Instead I allocate a `PushConstantBuffer`
- Unlike what #12 asks for, I don't use a different resource type for the contents of the block
- Pretty much all the logic is easiest if these continue to be just plain `Uniform` data
|
|
Calling:
spSetDumpIntermedites(compileRequest, true);
will set up a mode where Slang tries to dump every intermediate HLSL, GLSL, DXBC, SPIR-V, etc. file it generates. If SPIR-V or DXBC is requested then we also dump assembly of those.
Right now the files are all named as `slang-<counter>.<ext>`, and get dropped in whatever the working directory is, but I'm open to ideas on how to improve that.
Note: this change introduces a new binary interface to `glslang`, so pulling it requires an updated `glslang.dll`.
|
|
Fixes #84
- When computing resource usage for an array type, don't multiply the resource usage of the element type by the element count foor descriptor-table-slot resources.
- When reporting the "stride" of an array type through reflection, report the stride for descriptor table slots as zero, always.
|
|
Fixes #81
- This is based on a san over the GLSL spec (but is probably not exhaustive)
- There are some qualifiers that are currently being handled by general-case code for all languages, and some of these happen to cover GLSL qualifiers too
- Some of the qualifiers being handled by the general-case mechanism are *accidentally* working for GLSL (e.g., the HLSL `shared` qualifier doesn't mean the same thing as GLSL `shared`, but as long as we spit it back out nobody seems to care).
- This should be fixed sooner or later.
|
|
Fixes #83
- The basic idea is that I added a bunch of more specific profile names line `glsl_vertex_430` which indicate the desired GLSL version the user wants.
- An explicit `#version` line in the code always overrides one specified by profile, though
|