summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2024-11-23 20:20:24 +0100
committerHampusM <hampus@hampusmat.com>2024-11-23 20:57:27 +0100
commit999264a46f9a545771c6710b0893ca32cf6e7ee3 (patch)
tree44d1a0787e8670fbf645c9eca22fcffceec0f96d /engine/src
parentaf121c3a3aebdd1e10c768530ee0fbab5c8d6677 (diff)
refactor(engine): move & improve glsl preprocessing
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/lib.rs1
-rw-r--r--engine/src/opengl/glsl.rs (renamed from engine/src/shader_preprocessor.rs)535
-rw-r--r--engine/src/opengl/mod.rs1
-rw-r--r--engine/src/shader.rs34
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,
},