diff options
author | HampusM <hampus@hampusmat.com> | 2024-11-23 20:20:24 +0100 |
---|---|---|
committer | HampusM <hampus@hampusmat.com> | 2024-11-23 20:57:27 +0100 |
commit | 999264a46f9a545771c6710b0893ca32cf6e7ee3 (patch) | |
tree | 44d1a0787e8670fbf645c9eca22fcffceec0f96d /engine/src | |
parent | af121c3a3aebdd1e10c768530ee0fbab5c8d6677 (diff) |
refactor(engine): move & improve glsl preprocessing
Diffstat (limited to 'engine/src')
-rw-r--r-- | engine/src/lib.rs | 1 | ||||
-rw-r--r-- | engine/src/opengl/glsl.rs (renamed from engine/src/shader_preprocessor.rs) | 535 | ||||
-rw-r--r-- | engine/src/opengl/mod.rs | 1 | ||||
-rw-r--r-- | engine/src/shader.rs | 34 |
4 files changed, 291 insertions, 280 deletions
diff --git a/engine/src/lib.rs b/engine/src/lib.rs index abf26f5..555e1ed 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -21,7 +21,6 @@ use crate::event::{ }; mod opengl; -mod shader_preprocessor; mod util; pub mod camera; diff --git a/engine/src/shader_preprocessor.rs b/engine/src/opengl/glsl.rs index 70696ac..6fd5638 100644 --- a/engine/src/shader_preprocessor.rs +++ b/engine/src/opengl/glsl.rs @@ -1,183 +1,181 @@ +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::string::FromUtf8Error; const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; -/// Preprocessor for shaders written in the OpenGL Shading Language. -pub struct ShaderPreprocessor +pub fn preprocess<'content>( + shader_content: impl Into<Cow<'content, str>>, + read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, +) -> Result<Cow<'content, str>, PreprocessingError> { - read_file: fn(&Path) -> Result<Vec<u8>, std::io::Error>, - base_dir: PathBuf, + do_preprocess(shader_content, SpanPath::Original, read_file) } -impl ShaderPreprocessor +fn do_preprocess<'content>( + shader_content: impl Into<Cow<'content, str>>, + shader_path: SpanPath<'_>, + read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, +) -> Result<Cow<'content, str>, PreprocessingError> { - pub fn new(base_dir: PathBuf) -> Self - { - Self { - read_file: |path| std::fs::read(path), - base_dir, - } - } + let shader_content = shader_content.into(); - pub fn preprocess( - &self, - shader_content: String, - shader_file_path: &Path, - ) -> Result<String, Error> - { - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); + let mut preincludes = shader_content + .match_indices(PREINCLUDE_DIRECTIVE) + .peekable(); - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content); - }; + if preincludes.peek().is_none() { + // Shader content contains no preincludes + return Ok(shader_content.into()); + }; - let mut preprocessed = shader_content.clone(); + let mut preprocessed = shader_content.to_string(); - let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); + let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); - let mut last_start = 0; - let mut span_line_offset = 0; + let mut last_start = 0; + let mut span_line_offset = 0; - while let Some(preinclude_start) = curr { - let replacement_job = self.handle_preinclude( - &preprocessed, - shader_file_path, - preinclude_start, - span_line_offset, - )?; + while let Some(preinclude_start) = curr { + let replacement_job = handle_preinclude( + &preprocessed, + &shader_path, + preinclude_start, + span_line_offset, + )?; - let path = replacement_job.path.clone(); + let path = replacement_job.path.clone(); - let mut included = String::from_utf8( - (self.read_file)(&self.base_dir.join(replacement_job.path.clone())) - .map_err(|err| Error::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - })?, - ) - .map_err(|err| Error::IncludedShaderInvalidUtf8 { - source: err, - path: path.clone(), + let mut included = + String::from_utf8(read_file(&replacement_job.path).map_err(|err| { + PreprocessingError::ReadIncludedShaderFailed { + source: err, + path: replacement_job.path.clone(), + } + })?) + .map_err(|err| { + PreprocessingError::IncludedShaderInvalidUtf8 { + source: err, + path: path.clone(), + } })?; - if let Some(first_line) = included.lines().next() { - if first_line.starts_with("#version") { - included = included - .chars() - .skip_while(|character| *character != '\n') - .collect(); - } + if let Some(first_line) = included.lines().next() { + if first_line.starts_with("#version") { + included = included + .chars() + .skip_while(|character| *character != '\n') + .collect(); } - - let included_preprocessed = - self.preprocess(included, &replacement_job.path)?; - - let start = replacement_job.start_index; - let end = replacement_job.end_index; - - preprocessed.replace_range(start..end, &included_preprocessed); - - curr = preprocessed[last_start + 1..] - .find(PREINCLUDE_DIRECTIVE) - .map(|index| index + 1); - - last_start = preinclude_start + included_preprocessed.len(); - - span_line_offset += included_preprocessed.lines().count(); } - Ok(preprocessed) - } + let included_preprocessed = do_preprocess( + &included, + SpanPath::Path(replacement_job.path.as_path().into()), + read_file, + )?; - fn handle_preinclude( - &self, - shader_content: &str, - shader_file_path: &Path, - preinclude_start_index: usize, - span_line_offset: usize, - ) -> Result<ReplacementJob, Error> - { - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - Error::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; + let start = replacement_job.start_index; + let end = replacement_job.end_index; - if token_found != token { - return Err(Error::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - }); - } + preprocessed.replace_range(start..end, &included_preprocessed); - Ok(()) - }; + curr = preprocessed[last_start + 1..] + .find(PREINCLUDE_DIRECTIVE) + .map(|index| index + 1); - let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); - let quote_open_index = space_index + 1; + last_start = preinclude_start + included_preprocessed.len(); - expect_token(' ', space_index)?; - expect_token('"', quote_open_index)?; + span_line_offset += included_preprocessed.lines().count(); + } - let buf = shader_content[quote_open_index + 1..] - .chars() - .take_while(|character| *character != '"') - .map(|character| character as u8) - .collect::<Vec<_>>(); + Ok(preprocessed.into()) +} - if buf.is_empty() { - return Err(Error::ExpectedToken { - expected: '"', +fn handle_preinclude( + shader_content: &str, + shader_path: &SpanPath<'_>, + preinclude_start_index: usize, + span_line_offset: usize, +) -> Result<ReplacementJob, PreprocessingError> +{ + let expect_token = |token: char, index: usize| { + let token_found = shader_content.chars().nth(index).ok_or_else(|| { + PreprocessingError::ExpectedToken { + expected: token, span: Span::new( shader_content, - &self.base_dir.join(shader_file_path), - shader_content.len() - 1, + shader_path.to_owned(), + index, span_line_offset, preinclude_start_index, ), - }); - } - - let path_len = buf.len(); + } + })?; - let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { - Error::PreincludePathInvalidUtf8 { - source: err, + if token_found != token { + return Err(PreprocessingError::InvalidToken { + expected: token, + found: token_found, span: Span::new( shader_content, - &self.base_dir.join(shader_file_path), - quote_open_index + 1, + shader_path.to_owned(), + index, span_line_offset, preinclude_start_index, ), - } - })?); + }); + } - Ok(ReplacementJob { - start_index: preinclude_start_index, - end_index: quote_open_index + 1 + path_len + 1, - path, - }) + Ok(()) + }; + + let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); + let quote_open_index = space_index + 1; + + expect_token(' ', space_index)?; + expect_token('"', quote_open_index)?; + + let buf = shader_content[quote_open_index + 1..] + .chars() + .take_while(|character| *character != '"') + .map(|character| character as u8) + .collect::<Vec<_>>(); + + if buf.is_empty() { + return Err(PreprocessingError::ExpectedToken { + expected: '"', + span: Span::new( + shader_content, + shader_path.to_owned(), + shader_content.len() - 1, + span_line_offset, + preinclude_start_index, + ), + }); } + + let path_len = buf.len(); + + let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { + PreprocessingError::PreincludePathInvalidUtf8 { + source: err, + span: Span::new( + shader_content, + shader_path.to_owned(), + quote_open_index + 1, + span_line_offset, + preinclude_start_index, + ), + } + })?); + + Ok(ReplacementJob { + start_index: preinclude_start_index, + end_index: quote_open_index + 1 + path_len + 1, + path, + }) } struct ReplacementJob @@ -187,15 +185,14 @@ struct ReplacementJob path: PathBuf, } -/// Shader preprocessing error. #[derive(Debug, thiserror::Error)] -pub enum Error +pub enum PreprocessingError { #[error( "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", span.line, span.column, - span.file.display(), + span.path, expected, found )] @@ -211,7 +208,7 @@ pub enum Error expected, span.line, span.column, - span.file.display(), + span.path )] ExpectedToken { @@ -222,7 +219,7 @@ pub enum Error "Preinclude path at line {}, column {} of {} is invalid UTF-8", span.line, span.column, - span.file.display(), + span.path )] PreincludePathInvalidUtf8 { @@ -249,18 +246,19 @@ pub enum Error } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct Span { - line: usize, - column: usize, - file: PathBuf, + pub line: usize, + pub column: usize, + pub path: SpanPath<'static>, } impl Span { fn new( file_content: &str, - file_path: &Path, + path: SpanPath<'static>, char_index: usize, line_offset: usize, line_start_index: usize, @@ -272,7 +270,49 @@ impl Span Self { line, column: char_index - line_start_index + 1, - file: file_path.to_path_buf(), + path, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanPath<'a> +{ + Original, + Path(Cow<'a, Path>), +} + +impl<'a> SpanPath<'a> +{ + fn to_owned(&self) -> SpanPath<'static> + { + match self { + Self::Original => SpanPath::Original, + Self::Path(path) => SpanPath::Path(Cow::Owned(path.to_path_buf().into())), + } + } +} + +impl<'a> Display for SpanPath<'a> +{ + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Original => write!(formatter, "original file"), + Self::Path(path) => write!(formatter, "file {}", path.display()), + } + } +} + +impl<'a, PathLike> PartialEq<PathLike> for SpanPath<'a> +where + PathLike: AsRef<Path>, +{ + fn eq(&self, other: &PathLike) -> bool + { + match self { + Self::Original => false, + Self::Path(path) => path == other.as_ref(), } } } @@ -290,21 +330,17 @@ fn find_line_of_index(text: &str, index: usize) -> usize mod tests { use std::ffi::OsStr; - use std::path::{Path, PathBuf}; + use std::path::Path; - use super::{Error, ShaderPreprocessor}; + use super::{preprocess, PreprocessingError}; + use crate::opengl::glsl::SpanPath; #[test] fn preprocess_no_directives_is_same() { assert_eq!( - ShaderPreprocessor { - read_file: |_| { unreachable!() }, - base_dir: PathBuf::new() - } - .preprocess("#version 330 core\n".to_string(), Path::new("foo.glsl"),) - .unwrap(), - "#version 330 core\n".to_string() + preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), + "#version 330 core\n" ); } @@ -312,20 +348,15 @@ mod tests fn preprocess_with_directives_works() { assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( + preprocess( concat!( "#version 330 core\n", "\n", "#preinclude \"foo.glsl\"\n", "\n", "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + ), + &|_| { Ok(b"out vec4 FragColor;".to_vec()) } ) .unwrap(), concat!( @@ -338,11 +369,7 @@ mod tests ); assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( + preprocess( concat!( "#version 330 core\n", "\n", @@ -351,9 +378,8 @@ mod tests "in vec3 in_frag_color;\n", "\n", "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + ), + &|_| { Ok(b"out vec4 FragColor;".to_vec()) } ) .unwrap(), concat!( @@ -368,8 +394,19 @@ mod tests ); assert_eq!( - ShaderPreprocessor { - read_file: |path| { + preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "#preinclude \"foo.glsl\"\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(b"out vec4 FragColor;".to_vec()) } else { @@ -381,22 +418,6 @@ mod tests .to_vec()) } }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ) .unwrap(), concat!( @@ -415,15 +436,9 @@ mod tests } #[test] - fn preprocess_invalid_shader_does_not_work() + fn preprocess_invalid_directive_does_not_work() { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |_| Ok(b"out vec4 FragColor;".to_vec()), - base_dir: PathBuf::new(), - } - .preprocess( + let res = preprocess( concat!( "#version 330 core\n", "\n", @@ -431,12 +446,11 @@ mod tests "#preinclude foo.glsl\"\n", "\n", "void main() {}", - ) - .to_string(), - path, + ), + &|_| Ok(b"out vec4 FragColor;".to_vec()), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -446,16 +460,25 @@ mod tests assert_eq!(found, 'f'); assert_eq!(span.line, 3); assert_eq!(span.column, 13); - assert_eq!(span.file, path); + assert_eq!(span.path, SpanPath::Original); } #[test] fn preprocess_error_has_correct_span() { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |path| { + let res = preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "#preinclude \"foo.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( "out vec4 FragColor;\n", @@ -464,29 +487,25 @@ mod tests ) .as_bytes() .to_vec()) + } else if path == OsStr::new("foo.glsl") { + Ok(concat!( + "uniform sampler2D input_texture;\n", + "\n", + // Missing space before first " + "#preinclude\"shared_types.glsl\"\n", + ) + .as_bytes() + .to_vec()) } else { - Ok(b"uniform sampler2D input_texture;".to_vec()) + panic!(concat!( + "Expected read function to be called with ", + "either path bar.glsl or foo.glsl" + )); } }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude\"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -494,17 +513,26 @@ mod tests assert_eq!(expected, ' '); assert_eq!(found, '"'); - assert_eq!(span.line, 7); + assert_eq!(span.line, 3); assert_eq!(span.column, 12); - assert_eq!(span.file, path); + assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into())); } #[test] fn preprocess_included_shader_with_include_works() { assert_eq!( - ShaderPreprocessor { - read_file: |path| { + preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( "#preinclude \"foo.glsl\"\n", @@ -521,21 +549,7 @@ mod tests .as_bytes() .to_vec()) } - }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + } ) .unwrap(), concat!( @@ -556,8 +570,17 @@ mod tests #[test] fn preprocess_included_shader_with_include_error_span_is_correct() { - let res = ShaderPreprocessor { - read_file: |path| { + let res = preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( // ' instead of " @@ -576,23 +599,9 @@ mod tests .to_vec()) } }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -602,6 +611,6 @@ mod tests assert_eq!(found, '\''); assert_eq!(span.line, 1); assert_eq!(span.column, 13); - assert_eq!(span.file, Path::new("bar.glsl")); + assert_eq!(span.path, Path::new("bar.glsl")); } } diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs index 0b1bb8a..a4d3959 100644 --- a/engine/src/opengl/mod.rs +++ b/engine/src/opengl/mod.rs @@ -4,6 +4,7 @@ use crate::data_types::dimens::Dimens; use crate::vector::Vec2; pub mod buffer; +pub mod glsl; pub mod shader; pub mod texture; pub mod vertex_array; diff --git a/engine/src/shader.rs b/engine/src/shader.rs index 89f7b7c..b43d538 100644 --- a/engine/src/shader.rs +++ b/engine/src/shader.rs @@ -5,7 +5,10 @@ use std::path::{Path, PathBuf}; use ecs::Component; -use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor}; +use crate::opengl::glsl::{ + preprocess as glsl_preprocess, + PreprocessingError as GlslPreprocessingError, +}; const VERTEX_SHADER_FILE: &str = "vertex.glsl"; const FRAGMENT_SHADER_FILE: &str = "fragment.glsl"; @@ -116,23 +119,22 @@ impl Shader /// Returns `Err` if preprocessing fails. pub fn preprocess(self) -> Result<Self, Error> { - let shader_preprocessor = ShaderPreprocessor::new( - self.file - .parent() - .ok_or(Error::SourcePathHasNoParent)? - .to_path_buf(), - ); - - let source_preprocessed = shader_preprocessor - .preprocess(self.source, &self.file) - .map_err(|err| Error::PreprocessFailed { - source: err, - shader_file: self.file.clone(), - })?; + let parent_dir = self + .file + .parent() + .ok_or(Error::SourcePathHasNoParent)? + .to_path_buf(); + + let source_preprocessed = + glsl_preprocess(self.source, &|path| std::fs::read(parent_dir.join(path))) + .map_err(|err| Error::PreprocessFailed { + source: err, + shader_file: self.file.clone(), + })?; Ok(Self { kind: self.kind, - source: source_preprocessed, + source: source_preprocessed.into_owned(), file: self.file.clone(), }) } @@ -177,7 +179,7 @@ pub enum Error PreprocessFailed { #[source] - source: ShaderPreprocessorError, + source: GlslPreprocessingError, shader_file: PathBuf, }, |