hsimpson / vscode-glsllint

VSCode extension to lint GLSL shading language files
MIT License
76 stars 11 forks source link

Support Godot compute shaders by excluding the `#[compute]` directive when validating #63

Open AlfishSoftware opened 1 year ago

AlfishSoftware commented 1 year ago

Godot 4 uses .glsl files for its compute shaders (it has a different language for normal shaders). Their docs shows one such file as an example:

#[compute]
#version 450

// Invocations in the (x, y, z) dimension
layout(local_size_x = 2, local_size_y = 1, local_size_z = 1) in;

// A binding to the buffer we create in our script
layout(set = 0, binding = 0, std430) restrict buffer MyDataBuffer {
    float data[];
}
my_data_buffer;

// The code we want to execute in each invocation
void main() {
    // gl_GlobalInvocationID.x uniquely identifies this invocation across all work groups
    my_data_buffer.data[gl_GlobalInvocationID.x] *= 2.0;
}

To preserve compatibility, a user could configure a stage association like .comp.glsl as a compute shader and use this extension in Godot.

However, even after this workaround, glslangValidator will show an error on the first line. It doesn't recognize the #[compute] line, which is necessary for Godot according to docs:

#[compute]
#version 450

These two lines communicate two things:

  1. The following code is a compute shader. This is a Godot-specific hint that is needed for the editor to properly import the shader file.
  2. The code is using GLSL version 450.

You should never have to change these two lines for your custom compute shaders.

To solve this, you can detect the first line with this directive (probably the first thing in the code, excluding comments) and replace all characters in #[compute] with spaces ' ' (preserving whitespace on this line, before and after the token) before passing the input to glslangValidator. When you do this, you should also recognize the .glsl file as a compute shader, without requiring users to make some custom association like .comp.glsl.

So, for example, a file my_shader.glsl like this:

// Copyright (C) whatever etc
/* Some License 1.0 License
blah blah...
*/
    #[compute]  
//etc...

Would be recognized as a compute shader and become this when being passed to the validator stdin:

// Copyright (C) whatever etc
/* Some License 1.0 License
blah blah...
*/

//etc...

I don't think any other changes are needed to make it work, so it should be simple.

hsimpson commented 1 year ago

Unfortunately those engines like Godot using non standard things in GLSL. #[compute] is not a valid preprocessor directive according to the GLSL Language Spec.

So the only way to lint these shaders correctly is to workaround the code. So what I can imagine is an additional configuration for replacing things like:

"relpacements": [
   ["#[compute]", ""]
]

Which will cycle through all replacements and replaces in this case #[compute] with an empty string. The mapping to the compute shader stage is already available via the additionalStageAssociations configuration.

AlfishSoftware commented 1 year ago

Yeah, since it's non-standard, it makes sense to have such a generic setting. In this case it's better as a regex, since it'd have best flexibility while being easy to implement. I think ideally it could be:

/** Specify special handling for files containing a line that matches a regex.
 Only the first match applies. */
"regexRules": {
  // (this rule could be included by default, or as an example on how to use the setting)
  /** Godot compute shaders */
  "^\\s*\\#\\[compute\\]\\s*$": {
    /** Optional substitution string for the match, replaced before sending the input to the validator. */
    "replacement": "          ",
    /** Optional stage association to assign when the file has a line matching the regex. */
    "stageAssociation": "comp",
  },
},

Then you could process it with something like this:

// At this point, stage was already set with the existing association/fallback, if any.
for (let key in regexRules) {
  let regex = new RegExp(key, "m");
  let match = regex.exec(text);
  if (match) {
    var r = regexRules[key].replacement;
    if (r != undefined)
      text = text.substring(0, match.index) + r + text.substring(match.index + match[0].length);
      // could also allow back-references to groups like $1 if using something like text.replace(regex, r)
    var s = regexRules[key].stageAssociation;
    if (s != undefined)
      stage = s;
    break;
  }
}
callValidator(text, stage, ...);
hsimpson commented 1 year ago

Yes something like this would be possible.