byte-engine-resource-management 0.1.0

Asset, resource, shader, and storage management for Byte-Engine.
use colored::Colorize;

pub struct CompiledShader {
	artifact: shaderc::CompilationArtifact,
}

pub fn compile(source_code: &str, shader_name: &str) -> Result<CompiledShader, String> {
	let compiler = shaderc::Compiler::new().unwrap();
	let mut options = shaderc::CompileOptions::new().unwrap();

	options.set_optimization_level(shaderc::OptimizationLevel::Performance);
	options.set_target_env(shaderc::TargetEnv::Vulkan, shaderc::EnvVersion::Vulkan1_4 as u32);
	options.set_generate_debug_info();
	options.set_target_spirv(shaderc::SpirvVersion::V1_6);

	let binary = compiler.compile_into_spirv(
		source_code,
		shaderc::ShaderKind::InferFromSource,
		shader_name,
		"main",
		Some(&options),
	);

	match binary {
		Ok(binary) => Ok(CompiledShader { artifact: binary }),
		Err(err) => Err(pretty_format_glsl_error_lines(&process_glslc_error(
			shader_name,
			source_code,
			err.to_string().as_str(),
		))),
	}
}

impl<'a> std::ops::Deref for CompiledShader {
	type Target = shaderc::CompilationArtifact;

	fn deref(&self) -> &Self::Target {
		&self.artifact
	}
}

impl<'a> AsRef<[u8]> for CompiledShader {
	fn as_ref(&self) -> &[u8] {
		self.artifact.as_binary_u8()
	}
}

impl<'a> From<&'a CompiledShader> for &'a [u8] {
	fn from(val: &'a CompiledShader) -> Self {
		val.artifact.as_binary_u8()
	}
}

pub struct LineError<'a> {
	pub column: Option<usize>,
	pub symbol: &'a str,
	pub error: &'a str,
}

pub struct Line<'a> {
	pub line_number: usize,
	pub source_code: &'a str,
	pub errors: Vec<LineError<'a>>,
}

/// Returns the compiler errors reported in `glslc` output.
pub fn process_glslc_error<'a>(shader_name: &str, source_code: &'a str, error_string: &'a str) -> Error<'a> {
	// Collect (error_line_number, error) pairs
	let errors = error_string
		.lines()
		.filter(|error| error.starts_with(shader_name))
		.filter_map(|error| {
			// `glslc` can include additional `:` characters (for example a trailing `:`).
			// Split only the fixed prefix fields and keep the remainder as the message.
			let split = error.splitn(5, ':').map(|e| e.trim()).collect::<Vec<_>>();
			if split.len() != 5 {
				return None;
			}

			Some((split[1], (split[3].trim_matches('\''), split[4].trim_end_matches(':').trim())))
		})
		.map(|(error_line_number_string, error)| (error_line_number_string.trim().parse::<usize>().unwrap() - 1, error))
		.collect::<Vec<_>>();

	// Collect errors by line number
	let mut errors_by_line_number = std::collections::HashMap::<usize, Vec<(&'a str, &'a str)>>::new();

	for (error_line_number, error) in errors {
		if let Some(errors) = errors_by_line_number.get_mut(&error_line_number) {
			errors.push(error);
		} else {
			errors_by_line_number.insert(error_line_number, vec![error]);
		}
	}

	// Sort errors by line number
	let mut errors = errors_by_line_number.into_iter().collect::<Vec<_>>();

	errors.sort_by_key(|(line_number_a, _)| *line_number_a);

	errors
		.into_iter()
		.map(|(line_number, errors)| Line {
			line_number,
			source_code: source_code.lines().nth(line_number).unwrap_or("").trim(),
			errors: errors
				.into_iter()
				.map(|(symbol, error)| LineError {
					column: None,
					symbol,
					error,
				})
				.collect(),
		})
		.collect()
}

pub type Error<'a> = Vec<Line<'a>>;

pub fn pretty_format_glsl_error_lines(error_lines: &[Line]) -> String {
	let max_error_line_number = error_lines.iter().map(|error_line| error_line.line_number).max().unwrap_or(0);
	let max_line_number_length = max_error_line_number.to_string().len();

	let mut error_string = String::new();

	for error_line in error_lines {
		let line_errors = error_line
			.errors
			.iter()
			.map(|error| format!("'{}': {}", error.symbol, error.error))
			.collect::<Vec<_>>()
			.join(", ");

		error_string.push_str(&format!(
			"{:>width$}| {} {} {}\n",
			error_line.line_number + 1,
			error_line.source_code.bold(),
			"".red().bold(),
			line_errors.red(),
			width = max_line_number_length
		));
	}

	error_string
}

pub fn pretty_format_glslang_errors(error_lines: &[Line], source_code: &str) -> Option<String> {
	let mut source_code_lines = source_code.lines();

	let mut error_string = String::new();

	for error_line in error_lines {
		// How many lines to show before and after the error line
		let window_size = 2i32;

		let error_line_index = error_line.line_number;
		let line_errors = error_line.errors.iter().map(|e| e.error).collect::<Vec<_>>().join(", ");

		let lines = (-window_size..window_size)
			.filter_map(|delta| {
				let line_index = error_line_index as i32 + delta;
				source_code_lines.nth(line_index as usize).map(|e| (e, line_index == 0))
			})
			.map(|(line, is_error_line)| {
				if is_error_line {
					format!(
						"{}| {} {} {}",
						error_line_index + 1,
						line.bold(),
						"".red().bold(),
						line_errors.red()
					)
				} else {
					format!("{}| {}", error_line_index + 1, line.dimmed())
				}
			});

		error_string.push_str(&format!("{}\n", lines.collect::<Vec<_>>().join("\n")));
	}

	Some(error_string)
}

pub fn pretty_format_glslang_error_string(error_string: &str, shader_name: &str, source_code: &str) -> String {
	let error_lines = process_glslc_error(shader_name, source_code, error_string);

	pretty_format_glslang_errors(&error_lines, source_code).unwrap_or_else(|| error_string.to_string())
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_format_glslang_error() {
		let shader_name = "shaders/fragment.besl";
		let error_string = "shaders/fragment.besl:3: error: 'fresnel_schlick' : no matching overloaded function found
shaders/fragment.besl:3: error: '=' :  cannot convert from ' const float' to ' temp highp 3-component vector of float'
shaders/fragment.besl:3: error: 'distribution_ggx' : no matching overloaded function found
shaders/fragment.besl:3: error: 'geometry_smith' : no matching overloaded function found
shaders/fragment.besl:3: error: 'PI' : undeclared identifier";
		let source_code = "#version 450\nlayout(local_size_x = 1) in;\nvoid main() {}\n";

		let error = process_glslc_error(shader_name, source_code, error_string);

		assert_eq!(error.len(), 1);
		assert_eq!(error[0].line_number, 2);
		assert_eq!(error[0].source_code, "void main() {}");
		assert_eq!(error[0].errors.len(), 5);
		assert_eq!(error[0].errors[0].symbol, "fresnel_schlick");
		assert_eq!(error[0].errors[0].error, "no matching overloaded function found");

		println!("{}", pretty_format_glsl_error_lines(&error));
	}

	#[test]
	fn test_other_glsl_error() {
		let shader_name = "shader";
		let error_string = "2 compilation errors:\nshader:145: error: '>=' : can't read from writeonly object:\nshader:147: error: 'initializer' : can't read from writeonly object:\n";
		let source_code = "";

		let error = process_glslc_error(shader_name, source_code, error_string);

		assert_eq!(error.len(), 2);
		assert_eq!(error[0].line_number, 144);
		assert_eq!(error[0].source_code, "");
		assert_eq!(error[0].errors.len(), 1);
		assert_eq!(error[0].errors[0].symbol, ">=");
		assert_eq!(error[0].errors[0].error, "can't read from writeonly object");

		assert_eq!(error[1].line_number, 146);
		assert_eq!(error[1].source_code, "");
		assert_eq!(error[1].errors.len(), 1);
		assert_eq!(error[1].errors[0].symbol, "initializer");
		assert_eq!(error[1].errors[0].error, "can't read from writeonly object");

		println!("{}", pretty_format_glsl_error_lines(&error));
	}
}