use crate::assemble::Stage;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Rule {
pub ns: String,
pub verb: Option<String>,
pub args: Vec<String>,
pub vertex: Option<String>,
pub fragment: Option<String>,
pub compute: Option<String>,
pub any: Option<String>,
}
impl Rule {
fn template(&self, stage: Stage) -> Option<&str> {
let specific = match stage {
Stage::Vertex => &self.vertex,
Stage::Fragment => &self.fragment,
Stage::Compute => &self.compute,
};
specific.as_deref().or(self.any.as_deref())
}
}
#[derive(Debug, Clone, Default)]
pub struct Dialect {
#[allow(dead_code)]
pub name: String,
pub prelude_vertex: Option<String>,
pub prelude_fragment: Option<String>,
pub prelude_any: Option<String>,
pub prelude_extra: Option<String>,
pub epilogue: Option<String>,
pub prelude_files_vertex: Vec<String>,
pub prelude_files_fragment: Vec<String>,
pub rules: Vec<Rule>,
pub deck: bool,
}
impl Dialect {
pub fn prelude(&self, stage: Stage) -> Option<&str> {
let specific = match stage {
Stage::Vertex => &self.prelude_vertex,
Stage::Fragment => &self.prelude_fragment,
Stage::Compute => &None,
};
specific.as_deref().or(self.prelude_any.as_deref())
}
pub fn prelude_files(&self, stage: Stage) -> &[String] {
match stage {
Stage::Vertex => &self.prelude_files_vertex,
Stage::Fragment => &self.prelude_files_fragment,
Stage::Compute => &[],
}
}
pub fn expand_line(&self, line: &str, stage: Stage) -> Option<Vec<String>> {
let (ns, rest) = parse_pragma(line)?;
for rule in &self.rules {
if rule.ns != ns {
continue;
}
let mut tokens = rest.split_whitespace();
if let Some(verb) = &rule.verb {
match tokens.next() {
Some(t) if t == verb => {}
_ => continue,
}
}
let values: Vec<&str> = tokens.collect();
if values.len() != rule.args.len() {
continue;
}
let template = rule.template(stage)?;
return Some(render(template, &rule.args, &values));
}
None
}
}
fn parse_pragma(line: &str) -> Option<(String, &str)> {
let trimmed = line.trim_start();
let after = trimmed.strip_prefix("#pragma")?;
if !after.starts_with([' ', '\t']) {
return None;
}
let (ns, rest) = after.split_once(':')?;
let ns = ns.trim();
if ns.is_empty() {
return None;
}
Some((ns.to_string(), rest))
}
fn render(template: &str, args: &[String], values: &[&str]) -> Vec<String> {
let mut order: Vec<usize> = (0..args.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(args[i].len()));
let mut out = template.to_string();
for i in order {
out = out.replace(&format!("{{{}}}", args[i]), values[i]);
}
out.lines().map(str::to_string).collect()
}
pub fn has_main(source: &str) -> bool {
source
.lines()
.any(|l| l.replace(char::is_whitespace, "").contains("voidmain("))
}
#[derive(Debug, Clone)]
pub struct Preference {
pub preset: Option<String>,
pub auto: bool,
pub custom_rules: Vec<Rule>,
pub custom_prelude: Option<String>,
}
impl Default for Preference {
fn default() -> Self {
Preference {
preset: None,
auto: true,
custom_rules: Vec::new(),
custom_prelude: None,
}
}
}
pub fn resolve(pref: &Preference, source: &str, dir: &Path) -> Option<Dialect> {
let base = match pref.preset.as_deref() {
Some(name) => by_name(name),
None if pref.auto => autodetect(source, dir),
None => None,
};
let has_custom = !pref.custom_rules.is_empty() || pref.custom_prelude.is_some();
if base.is_none() && !has_custom {
return None;
}
let mut d = base.unwrap_or_else(|| Dialect {
name: "custom".into(),
deck: false,
..Default::default()
});
if !pref.custom_rules.is_empty() {
let mut rules = pref.custom_rules.clone();
rules.extend(std::mem::take(&mut d.rules));
d.rules = rules;
}
if pref.custom_prelude.is_some() {
d.prelude_extra = pref.custom_prelude.clone();
}
Some(d)
}
pub fn by_name(name: &str) -> Option<Dialect> {
match name {
"maplibre" | "mapbox" => Some(maplibre()),
"shadertoy" => Some(shadertoy()),
"none" | "raw" => Some(Dialect {
name: "none".into(),
deck: false,
..Default::default()
}),
_ => None,
}
}
pub fn autodetect(source: &str, dir: &Path) -> Option<Dialect> {
if source.contains("#pragma maplibre:") || source.contains("#pragma mapbox:") {
return Some(maplibre());
}
if source
.replace(char::is_whitespace, "")
.contains("voidmainImage(")
{
return Some(shadertoy());
}
if dir.join("_prelude.vertex.glsl").is_file() || dir.join("_prelude.fragment.glsl").is_file() {
return Some(maplibre());
}
None
}
pub fn maplibre() -> Dialect {
let mut rules = Vec::new();
for ns in ["maplibre", "mapbox"] {
rules.push(Rule {
ns: ns.to_string(),
verb: Some("define".into()),
args: vec!["prec".into(), "type".into(), "name".into()],
vertex: Some(
"#ifndef HAS_UNIFORM_u_{name}\n\
uniform lowp float u_{name}_t;\n\
in {prec} {type} a_{name};\n\
out {prec} {type} {name};\n\
#else\n\
uniform {prec} {type} u_{name};\n\
#endif"
.into(),
),
fragment: Some(
"#ifndef HAS_UNIFORM_u_{name}\n\
uniform lowp float u_{name}_t;\n\
in {prec} {type} {name};\n\
#else\n\
uniform {prec} {type} u_{name};\n\
#endif"
.into(),
),
compute: None,
any: None,
});
rules.push(Rule {
ns: ns.to_string(),
verb: Some("initialize".into()),
args: vec!["prec".into(), "type".into(), "name".into()],
vertex: Some(
"#ifndef HAS_UNIFORM_u_{name}\n\
{name} = a_{name};\n\
#else\n\
{prec} {type} {name} = u_{name};\n\
#endif"
.into(),
),
fragment: Some(
"#ifdef HAS_UNIFORM_u_{name}\n\
{prec} {type} {name} = u_{name};\n\
#endif"
.into(),
),
compute: None,
any: None,
});
}
Dialect {
name: "maplibre".into(),
deck: false,
rules,
prelude_files_vertex: vec![
"_prelude.vertex.glsl".into(),
"_projection_mercator.vertex.glsl".into(),
],
prelude_files_fragment: vec![
"_prelude.fragment.glsl".into(),
"_projection_mercator.fragment.glsl".into(),
],
..Default::default()
}
}
pub fn shadertoy() -> Dialect {
Dialect {
name: "shadertoy".into(),
deck: false,
prelude_fragment: Some(
"// glslint ShaderToy prelude: implicit uniforms\n\
uniform vec3 iResolution;\n\
uniform float iTime;\n\
uniform float iTimeDelta;\n\
uniform float iFrameRate;\n\
uniform int iFrame;\n\
uniform vec4 iMouse;\n\
uniform vec4 iDate;\n\
uniform float iSampleRate;\n\
uniform vec3 iChannelResolution[4];\n\
uniform float iChannelTime[4];\n\
uniform sampler2D iChannel0;\n\
uniform sampler2D iChannel1;\n\
uniform sampler2D iChannel2;\n\
uniform sampler2D iChannel3;\n\
out vec4 glslint_fragColor;"
.into(),
),
epilogue: Some("void main() { mainImage(glslint_fragColor, gl_FragCoord.xy); }".into()),
..Default::default()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn parse_pragma_splits_namespace_and_body() {
assert_eq!(
parse_pragma("#pragma maplibre: define lowp float opacity"),
Some(("maplibre".to_string(), " define lowp float opacity"))
);
assert_eq!(
parse_pragma(" #pragma glslify : foo").map(|(n, _)| n),
Some("glslify".to_string())
);
assert_eq!(parse_pragma("#version 300 es"), None);
assert_eq!(parse_pragma("#pragma optimize(on)"), None);
assert_eq!(parse_pragma("#pragmatic: x"), None);
}
#[test]
fn maplibre_define_expands_per_stage() {
let d = maplibre();
let v = d
.expand_line("#pragma maplibre: define lowp float opacity", Stage::Vertex)
.unwrap();
assert!(v.iter().any(|l| l.contains("in lowp float a_opacity;")));
assert!(v.iter().any(|l| l.contains("out lowp float opacity;")));
let f = d
.expand_line(
"#pragma maplibre: define lowp float opacity",
Stage::Fragment,
)
.unwrap();
assert!(f.iter().any(|l| l.contains("in lowp float opacity;")));
assert!(!f.iter().any(|l| l.contains("a_opacity")));
}
#[test]
fn maplibre_initialize_binds_the_name() {
let d = maplibre();
let v = d
.expand_line(
" #pragma maplibre: initialize lowp float opacity",
Stage::Vertex,
)
.unwrap();
assert!(v.iter().any(|l| l.contains("opacity = a_opacity;")));
}
#[test]
fn mapbox_namespace_uses_the_same_rules() {
let v = maplibre()
.expand_line("#pragma mapbox: define highp vec4 color", Stage::Vertex)
.unwrap();
assert!(v.iter().any(|l| l.contains("out highp vec4 color;")));
}
#[test]
fn non_matching_lines_are_left_verbatim() {
let d = maplibre();
assert!(
d.expand_line("uniform vec2 u_translation;", Stage::Vertex)
.is_none()
);
assert!(
d.expand_line("#pragma maplibre: whoknows x", Stage::Vertex)
.is_none()
);
assert!(
d.expand_line("#pragma maplibre: define float x", Stage::Vertex)
.is_none()
);
}
#[test]
fn render_substitutes_all_occurrences() {
let out = render(
"uniform lowp float u_{name}_t;\nin {prec} {type} a_{name};",
&["prec".into(), "type".into(), "name".into()],
&["lowp", "float", "opacity"],
);
assert_eq!(out[0], "uniform lowp float u_opacity_t;");
assert_eq!(out[1], "in lowp float a_opacity;");
}
#[test]
fn autodetect_recognizes_maplibre_and_shadertoy() {
let bare = Path::new("/glslint-nonexistent-test-dir");
assert_eq!(
autodetect("#pragma maplibre: define lowp float opacity\n", bare).map(|d| d.name),
Some("maplibre".to_string())
);
assert_eq!(
autodetect("void mainImage( out vec4 c, in vec2 p ) {}\n", bare).map(|d| d.name),
Some("shadertoy".to_string())
);
assert_eq!(
autodetect("out vec4 c;\nvoid main(){}\n", bare).map(|d| d.name),
None
);
}
#[test]
fn autodetect_fires_on_a_sibling_maplibre_library() {
let dir = std::env::temp_dir().join(format!(
"glslint-detect-{}-{}",
std::process::id(),
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
let src = "void main() { gl_Position = projectTile(vec2(0.0)); }\n";
assert_eq!(autodetect(src, &dir).map(|d| d.name), None);
std::fs::write(dir.join("_prelude.vertex.glsl"), "// lib\n").unwrap();
assert_eq!(
autodetect(src, &dir).map(|d| d.name),
Some("maplibre".into())
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn has_main_detects_the_entry_point() {
assert!(has_main("void main() { }"));
assert!(has_main("void main(){}"));
assert!(!has_main("void mainImage(out vec4 c, in vec2 p){}"));
}
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
}