Shaders can now have #else ifdef chains (#7431)

# Objective

Currently, shaders may only have syntax such as

```wgsl
#ifdef FOO
// foo code
#else
#ifdef BAR
// bar code
#else
#ifdef BAZ
// baz code
#else
// fallback code
#endif
#endif
#endif
```

This is hard to read and follow.
Add a way to allow writing `#else ifdef DEFINE` to reduce the number of scopes introduced and to increase readability.

## Solution

Refactor the current preprocessing a bit and add logic to allow `#else ifdef DEFINE`.
This includes per-scope tracking of whether a branch has been accepted.
Add a few tests for this feature.

With these changes we may now write:

```wgsl
#ifdef FOO
// foo code
#else ifdef BAR
// bar code
#else ifdef BAZ
// baz code
#else
// fallback code
#endif
```

instead.

---

## Changelog

- Add `#else ifdef` to shader preprocessing.
This commit is contained in:
Torstein Grindvik 2023-02-04 15:29:35 +00:00
parent 3af6179076
commit 12f30f5667

View file

@ -385,6 +385,7 @@ pub struct ShaderProcessor {
ifdef_regex: Regex,
ifndef_regex: Regex,
ifop_regex: Regex,
else_ifdef_regex: Regex,
else_regex: Regex,
endif_regex: Regex,
def_regex: Regex,
@ -397,6 +398,7 @@ impl Default for ShaderProcessor {
ifdef_regex: Regex::new(r"^\s*#\s*ifdef\s*([\w|\d|_]+)").unwrap(),
ifndef_regex: Regex::new(r"^\s*#\s*ifndef\s*([\w|\d|_]+)").unwrap(),
ifop_regex: Regex::new(r"^\s*#\s*if\s*([\w|\d|_]+)\s*([^\s]*)\s*([\w|\d]+)").unwrap(),
else_ifdef_regex: Regex::new(r"^\s*#\s*else\s+ifdef\s*([\w|\d|_]+)").unwrap(),
else_regex: Regex::new(r"^\s*#\s*else").unwrap(),
endif_regex: Regex::new(r"^\s*#\s*endif").unwrap(),
def_regex: Regex::new(r"#\s*([\w|\d|_]+)").unwrap(),
@ -405,6 +407,41 @@ impl Default for ShaderProcessor {
}
}
struct Scope {
// Is the current scope one in which we should accept new lines into the output?
accepting_lines: bool,
// Has this scope ever accepted lines?
// Needs to be tracked for #else ifdef chains.
has_accepted_lines: bool,
}
impl Scope {
fn new(should_lines_be_accepted: bool) -> Self {
Self {
accepting_lines: should_lines_be_accepted,
has_accepted_lines: should_lines_be_accepted,
}
}
fn is_accepting_lines(&self) -> bool {
self.accepting_lines
}
fn stop_accepting_lines(&mut self) {
self.accepting_lines = false;
}
fn start_accepting_lines_if_appropriate(&mut self) {
if !self.has_accepted_lines {
self.has_accepted_lines = true;
self.accepting_lines = true;
} else {
self.accepting_lines = false;
}
}
}
impl ShaderProcessor {
pub fn process(
&self,
@ -430,18 +467,23 @@ impl ShaderProcessor {
(k.clone(), v.clone())
}
}));
let mut scopes = vec![true];
let mut scopes = vec![Scope::new(true)];
let mut final_string = String::new();
for line in shader_str.lines() {
if let Some(cap) = self.ifdef_regex.captures(line) {
let def = cap.get(1).unwrap();
scopes
.push(*scopes.last().unwrap() && shader_defs_unique.contains_key(def.as_str()));
let current_valid = scopes.last().unwrap().is_accepting_lines();
let has_define = shader_defs_unique.contains_key(def.as_str());
scopes.push(Scope::new(current_valid && has_define));
} else if let Some(cap) = self.ifndef_regex.captures(line) {
let def = cap.get(1).unwrap();
scopes.push(
*scopes.last().unwrap() && !shader_defs_unique.contains_key(def.as_str()),
);
let current_valid = scopes.last().unwrap().is_accepting_lines();
let has_define = shader_defs_unique.contains_key(def.as_str());
scopes.push(Scope::new(current_valid && !has_define));
} else if let Some(cap) = self.ifop_regex.captures(line) {
let def = cap.get(1).unwrap();
let op = cap.get(2).unwrap();
@ -498,21 +540,83 @@ impl ShaderProcessor {
act_on(*def, val, op.as_str())?
}
};
scopes.push(*scopes.last().unwrap() && new_scope);
} else if self.else_regex.is_match(line) {
let mut is_parent_scope_truthy = true;
let current_valid = scopes.last().unwrap().is_accepting_lines();
scopes.push(Scope::new(current_valid && new_scope));
} else if let Some(cap) = self.else_ifdef_regex.captures(line) {
// When should we accept the code in an
//
// #else ifdef FOO
// <stuff>
// #endif
//
// block? Conditions:
// 1. The parent scope is accepting lines.
// 2. The current scope is _not_ accepting lines.
// 3. FOO is defined.
// 4. We haven't already accepted another #ifdef (or #else ifdef) in the current scope.
// Condition 1
let mut parent_accepting = true;
if scopes.len() > 1 {
is_parent_scope_truthy = scopes[scopes.len() - 2];
parent_accepting = scopes[scopes.len() - 2].is_accepting_lines();
}
if let Some(last) = scopes.last_mut() {
*last = is_parent_scope_truthy && !*last;
if let Some(current) = scopes.last_mut() {
// Condition 2
let current_accepting = current.is_accepting_lines();
// Condition 3
let def = cap.get(1).unwrap();
let has_define = shader_defs_unique.contains_key(def.as_str());
if parent_accepting && !current_accepting && has_define {
// Condition 4: Enforced by [`Scope`].
current.start_accepting_lines_if_appropriate();
} else {
current.stop_accepting_lines();
}
}
} else if self.else_regex.is_match(line) {
let mut parent_accepting = true;
if scopes.len() > 1 {
parent_accepting = scopes[scopes.len() - 2].is_accepting_lines();
}
if let Some(current) = scopes.last_mut() {
// Using #else means that we only want to accept those lines in the output
// if the stuff before #else was _not_ accepted.
// That's why we stop accepting here if we were currently accepting.
//
// Why do we care about the parent scope?
// Because if we have something like this:
//
// #ifdef NOT_DEFINED
// // Not accepting lines
// #ifdef NOT_DEFINED_EITHER
// // Not accepting lines
// #else
// // This is now accepting lines relative to NOT_DEFINED_EITHER
// <stuff>
// #endif
// #endif
//
// We don't want to actually add <stuff>.
if current.is_accepting_lines() || !parent_accepting {
current.stop_accepting_lines();
} else {
current.start_accepting_lines_if_appropriate();
}
}
} else if self.endif_regex.is_match(line) {
scopes.pop();
if scopes.is_empty() {
return Err(ProcessShaderError::TooManyEndIfs);
}
} else if *scopes.last().unwrap() {
} else if scopes.last().unwrap().is_accepting_lines() {
if let Some(cap) = SHADER_IMPORT_PROCESSOR
.import_asset_path_regex
.captures(line)
@ -715,6 +819,83 @@ struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
const WGSL_ELSE_IFDEF: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
#ifdef TEXTURE
// Main texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
#else ifdef SECOND_TEXTURE
// Second texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
#else ifdef THIRD_TEXTURE
// Third texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
#else
@group(1) @binding(0)
var sprite_texture: texture_2d_array<f32>;
#endif
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
const WGSL_ELSE_IFDEF_NO_ELSE_FALLBACK: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
#ifdef TEXTURE
// Main texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
#else ifdef OTHER_TEXTURE
// Other texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
#endif
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@ -918,6 +1099,306 @@ fn vertex(
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_ends_up_in_else() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
@group(1) @binding(0)
var sprite_texture: texture_2d_array<f32>;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF),
&[],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_no_match_and_no_fallback_else() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF_NO_ELSE_FALLBACK),
&[],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_ends_up_in_first_clause() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
// Main texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF),
&["TEXTURE".into()],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_ends_up_in_second_clause() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
// Second texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF),
&["SECOND_TEXTURE".into()],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_ends_up_in_third_clause() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
// Third texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF),
&["THIRD_TEXTURE".into()],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_only_accepts_one_valid_else_ifdef() {
#[rustfmt::skip]
const EXPECTED: &str = r"
struct View {
view_proj: mat4x4<f32>,
world_position: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> view: View;
// Second texture
@group(1) @binding(0)
var sprite_texture: texture_2d<f32>;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_ELSE_IFDEF),
&["SECOND_TEXTURE".into(), "THIRD_TEXTURE".into()],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_else_ifdef_complicated_nesting() {
// Test some nesting including #else ifdef statements
// 1. Enter an #else ifdef
// 2. Then enter an #else
// 3. Then enter another #else ifdef
#[rustfmt::skip]
const WGSL_COMPLICATED_ELSE_IFDEF: &str = r"
#ifdef NOT_DEFINED
// not defined
#else ifdef IS_DEFINED
// defined 1
#ifdef NOT_DEFINED
// not defined
#else
// should be here
#ifdef NOT_DEFINED
// not defined
#else ifdef ALSO_NOT_DEFINED
// not defined
#else ifdef IS_DEFINED
// defined 2
#endif
#endif
#endif
";
#[rustfmt::skip]
const EXPECTED: &str = r"
// defined 1
// should be here
// defined 2
";
let processor = ShaderProcessor::default();
let result = processor
.process(
&Shader::from_wgsl(WGSL_COMPLICATED_ELSE_IFDEF),
&["IS_DEFINED".into()],
&HashMap::default(),
&HashMap::default(),
)
.unwrap();
assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED);
}
#[test]
fn process_shader_def_unclosed() {
#[rustfmt::skip]