use alloc::{borrow::Cow, format, string::String, vec::Vec};
use core::fmt::Write as _;
use hashbrown::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GlslMacroKind {
Bool,
Sint,
Uint,
Float,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlslMacro {
pub constant: &'static str,
pub name: &'static str,
pub kind: GlslMacroKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlslBlock {
pub name: &'static str,
pub storage: bool,
pub binding: naga::ResourceBinding,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlslTexture {
pub name: &'static str,
pub storage: bool,
pub binding: naga::ResourceBinding,
pub sampler: Option<naga::ResourceBinding>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlslTemplate {
pub entry_point: &'static str,
pub stage: naga::ShaderStage,
pub glsl: &'static [&'static str],
pub macros: &'static [GlslMacro],
pub blocks: &'static [GlslBlock],
pub textures: &'static [GlslTexture],
pub varyings: &'static [(&'static str, u32)],
pub clip_distance_count: u32,
}
impl GlslTemplate {
pub fn expand(&self, constants: &naga::back::PipelineConstants) -> Result<String, String> {
let mut macros = Macros::default();
for item in self.macros {
if let Some(&value) = constants.get(item.constant) {
macros.define(item.name, Cow::Owned(macro_value(item, value)?));
}
}
let length = self.glsl.iter().map(|piece| piece.len()).sum::<usize>();
let mut code = String::with_capacity(length);
let mut version = "";
let mut kept: Vec<bool> = Vec::new();
for line in self.glsl.iter().flat_map(|piece| piece.split_inclusive('\n')) {
let keeping = kept.last().copied().unwrap_or(true);
let Some(directive) = line.trim_start().strip_prefix('#') else {
if keeping {
code.push_str(line);
}
continue;
};
let directive = directive.trim_end();
let (name, rest) = directive.split_once(' ').unwrap_or((directive, ""));
match name {
"version" => version = line,
"ifndef" => kept.push(keeping && !macros.defined(rest.trim())),
"if" => {
let holds = keeping && macros.evaluate(rest)? != 0;
kept.push(holds);
}
"endif" => {
kept.pop().ok_or("#endif without #if")?;
}
"define" if keeping => {
let (macro_name, value) = rest.split_once(' ').unwrap_or((rest, ""));
macros.define(macro_name, Cow::Borrowed(value));
}
"define" => {}
other => return Err(format!("#{other} in a GLSL template")),
}
}
if !kept.is_empty() {
return Err("a GLSL template leaves a conditional open".into());
}
let mut glsl = String::with_capacity(version.len() + 48 * self.macros.len() + code.len());
glsl.push_str(version);
for item in self.macros {
if let Some(value) = macros.values.get(item.name) {
let _ = writeln!(glsl, "#define {} {value}", item.name);
}
}
glsl.push_str(&code);
Ok(glsl)
}
}
fn macro_value(item: &GlslMacro, value: f64) -> Result<String, String> {
let unfit = || format!("{}: {value} is no {:?}", item.constant, item.kind);
Ok(match item.kind {
GlslMacroKind::Bool => format!("{}", u8::from(value != 0.0)),
GlslMacroKind::Sint => {
if value % 1.0 != 0.0
|| !(f64::from(i32::MIN)..=f64::from(i32::MAX)).contains(&value)
{
return Err(unfit());
}
format!("{}", value as i32)
}
GlslMacroKind::Uint => {
if value % 1.0 != 0.0 || !(0.0..=f64::from(u32::MAX)).contains(&value) {
return Err(unfit());
}
let value = value as u32;
if value > i32::MAX as u32 {
format!("{value:#X}")
} else {
format!("{value}")
}
}
GlslMacroKind::Float => {
if !value.is_finite() {
return Err(unfit());
}
format!("{:?}", value as f32)
}
})
}
#[derive(Default)]
struct Macros<'t> {
values: HashMap<&'t str, Cow<'t, str>>,
evaluated: HashMap<&'t str, i64>,
}
impl<'t> Macros<'t> {
fn define(&mut self, name: &'t str, value: Cow<'t, str>) {
self.values.entry(name).or_insert(value);
}
fn defined(&self, name: &str) -> bool {
self.values.contains_key(name)
}
fn evaluate(&mut self, text: &str) -> Result<i64, String> {
Evaluator {
values: &self.values,
evaluated: &mut self.evaluated,
}
.evaluate(text)
}
}
struct Evaluator<'a, 't> {
values: &'a HashMap<&'t str, Cow<'t, str>>,
evaluated: &'a mut HashMap<&'t str, i64>,
}
impl Evaluator<'_, '_> {
fn evaluate(&mut self, text: &str) -> Result<i64, String> {
let mut parser = Parser { text, at: 0 };
let value = parser.binary(self, 0)?;
parser.skip_space();
if parser.at != text.len() {
return Err(format!("`{text}` does not end at {}", parser.at));
}
Ok(value)
}
fn value(&mut self, name: &str) -> Result<i64, String> {
if let Some(&value) = self.evaluated.get(name) {
return Ok(value);
}
let values = self.values;
let (&key, text) = values
.get_key_value(name)
.ok_or_else(|| format!("`{name}` is not defined"))?;
let value = self.evaluate(text)?;
self.evaluated.insert(key, value);
Ok(value)
}
}
const LEVELS: [&[&str]; 10] = [
&["||"],
&["&&"],
&["|"],
&["^"],
&["&"],
&["==", "!="],
&["<=", ">=", "<", ">"],
&["<<", ">>"],
&["+", "-"],
&["*", "/", "%"],
];
struct Parser<'s> {
text: &'s str,
at: usize,
}
impl Parser<'_> {
fn skip_space(&mut self) {
while self.text[self.at..].starts_with([' ', '\t']) {
self.at += 1;
}
}
fn operator(&mut self, level: usize) -> Option<&'static str> {
self.skip_space();
let rest = &self.text[self.at..];
let found = LEVELS[level].iter().copied().find(|operator| {
rest.starts_with(operator)
&& !(operator.len() == 1 && rest[1..].starts_with(*operator))
})?;
self.at += found.len();
Some(found)
}
fn binary(&mut self, macros: &mut Evaluator<'_, '_>, level: usize) -> Result<i64, String> {
if level == LEVELS.len() {
return self.unary(macros);
}
let mut value = self.binary(macros, level + 1)?;
while let Some(operator) = self.operator(level) {
let right = self.binary(macros, level + 1)?;
value = match operator {
"||" => i64::from(value != 0 || right != 0),
"&&" => i64::from(value != 0 && right != 0),
"|" => value | right,
"^" => value ^ right,
"&" => value & right,
"==" => i64::from(value == right),
"!=" => i64::from(value != right),
"<" => i64::from(value < right),
">" => i64::from(value > right),
"<=" => i64::from(value <= right),
">=" => i64::from(value >= right),
"<<" => value.wrapping_shl(right as u32),
">>" => value.wrapping_shr(right as u32),
"+" => value.wrapping_add(right),
"-" => value.wrapping_sub(right),
"*" => value.wrapping_mul(right),
"/" => value.checked_div(right).ok_or("division by zero")?,
_ => value.checked_rem(right).ok_or("division by zero")?,
};
}
Ok(value)
}
fn unary(&mut self, macros: &mut Evaluator<'_, '_>) -> Result<i64, String> {
self.skip_space();
let rest = &self.text[self.at..];
let Some(first) = rest.chars().next() else {
return Err(format!("`{}` ends where a value goes", self.text));
};
match first {
'!' | '~' | '-' | '+' => {
self.at += 1;
let value = self.unary(macros)?;
Ok(match first {
'!' => i64::from(value == 0),
'~' => !value,
'-' => value.wrapping_neg(),
_ => value,
})
}
'(' => {
self.at += 1;
let value = self.binary(macros, 0)?;
self.skip_space();
if !self.text[self.at..].starts_with(')') {
return Err(format!("`{}` leaves a parenthesis open", self.text));
}
self.at += 1;
Ok(value)
}
_ => {
let length = rest
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.unwrap_or(rest.len());
let word = &rest[..length];
self.at += length;
if first.is_ascii_digit() {
let (digits, radix) = match word.strip_prefix("0x").or_else(|| word.strip_prefix("0X")) {
Some(hex) => (hex, 16),
None => (word, 10),
};
i64::from_str_radix(digits.trim_end_matches(['u', 'U']), radix)
.map_err(|error| format!("`{word}`: {error}"))
} else if word.is_empty() {
Err(format!("`{}` has no value at {}", self.text, self.at))
} else {
macros.value(word)
}
}
}
}
}