diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 63e8b5b64f655..d60a66db0eb3a 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -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); + + 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 + // + // #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 { + parent_accepting = scopes[scopes.len() - 2].is_accepting_lines(); + } + + 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 is_parent_scope_truthy = true; + 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() { + // 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 + // + // #endif + // #endif + // + // We don't want to actually add . + + 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, }; +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + + const WGSL_ELSE_IFDEF: &str = r" +struct View { + view_proj: mat4x4, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +#ifdef TEXTURE +// Main texture +@group(1) @binding(0) +var sprite_texture: texture_2d; +#else ifdef SECOND_TEXTURE +// Second texture +@group(1) @binding(0) +var sprite_texture: texture_2d; +#else ifdef THIRD_TEXTURE +// Third texture +@group(1) @binding(0) +var sprite_texture: texture_2d; +#else +@group(1) @binding(0) +var sprite_texture: texture_2d_array; +#endif + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + + const WGSL_ELSE_IFDEF_NO_ELSE_FALLBACK: &str = r" +struct View { + view_proj: mat4x4, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +#ifdef TEXTURE +// Main texture +@group(1) @binding(0) +var sprite_texture: texture_2d; +#else ifdef OTHER_TEXTURE +// Other texture +@group(1) @binding(0) +var sprite_texture: texture_2d; +#endif + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + @vertex fn vertex( @location(0) vertex_position: vec3, @@ -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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +@group(1) @binding(0) +var sprite_texture: texture_2d_array; + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +// Main texture +@group(1) @binding(0) +var sprite_texture: texture_2d; + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +// Second texture +@group(1) @binding(0) +var sprite_texture: texture_2d; + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +// Third texture +@group(1) @binding(0) +var sprite_texture: texture_2d; + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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, + world_position: vec3, +}; +@group(0) @binding(0) +var view: View; + +// Second texture +@group(1) @binding(0) +var sprite_texture: texture_2d; + +struct VertexOutput { + @location(0) uv: vec2, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(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]