mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
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:
parent
3af6179076
commit
12f30f5667
1 changed files with 494 additions and 13 deletions
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue