use crate::config::Config;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Vertex,
Fragment,
Compute,
}
impl Stage {
pub fn glslang_stage(self) -> &'static str {
match self {
Stage::Vertex => "vert",
Stage::Fragment => "frag",
Stage::Compute => "comp",
}
}
}
const DEFAULT_VERSION: &str = "#version 300 es";
const DEFAULT_PRECISION: &str = "precision highp float;\nprecision highp int;";
#[derive(Debug, Clone)]
pub struct Loc {
pub path: PathBuf,
pub line: u32,
}
pub struct Assembled {
pub source: String,
pub stage: Stage,
pub map: Vec<Option<Loc>>,
pub target: PathBuf,
#[allow(dead_code)]
pub note: Option<&'static str>,
}
struct Builder {
lines: Vec<String>,
map: Vec<Option<Loc>>,
}
impl Builder {
fn new() -> Self {
Builder {
lines: Vec::new(),
map: Vec::new(),
}
}
fn push(&mut self, line: String, loc: Option<Loc>) {
self.lines.push(line);
self.map.push(loc);
}
fn push_block(&mut self, content: &str, path: &Path) {
for (i, l) in content.lines().enumerate() {
self.push(
l.to_string(),
Some(Loc {
path: path.to_path_buf(),
line: line_no(i),
}),
);
}
}
fn push_synthetic(&mut self, content: &str) {
for l in content.lines() {
self.push(l.to_string(), None);
}
}
fn finish(self, stage: Stage, target: &Path, note: Option<&'static str>) -> Assembled {
let mut source = self.lines.join("\n");
source.push('\n');
Assembled {
source,
stage,
map: self.map,
target: target.to_path_buf(),
note,
}
}
}
pub fn detect_stage(path: &Path) -> Option<Stage> {
let name = path.file_name()?.to_str()?;
let n = name.to_ascii_lowercase();
if n.contains(".vert.")
|| n.contains(".vertex.")
|| n.ends_with(".vert")
|| n.ends_with(".vertex")
|| n.ends_with(".vs")
{
Some(Stage::Vertex)
} else if n.contains(".frag.")
|| n.contains(".fragment.")
|| n.ends_with(".frag")
|| n.ends_with(".fragment")
|| n.ends_with(".fs")
{
Some(Stage::Fragment)
} else if n.contains(".comp.")
|| n.contains(".compute.")
|| n.ends_with(".comp")
|| n.ends_with(".compute")
{
Some(Stage::Compute)
} else {
None
}
}
pub(crate) fn line_no(i: usize) -> u32 {
u32::try_from(i).unwrap_or(u32::MAX).saturating_add(1)
}
pub fn assemble(target: &Path, source: &str, config: &Config) -> Assembled {
match detect_stage(target) {
Some(stage) => assemble_stage(target, source, config, stage),
None => wrap_fragment(target, source),
}
}
pub fn assemble_embedded(
target: &Path,
source: &str,
config: &Config,
stage: Stage,
has_entry: bool,
) -> Assembled {
if has_entry {
assemble_stage(target, source, config, stage)
} else {
wrap_as(target, source, stage)
}
}
fn assemble_stage(target: &Path, source: &str, config: &Config, stage: Stage) -> Assembled {
let mut b = Builder::new();
let lines: Vec<&str> = source.lines().collect();
let dir = target.parent().unwrap_or(Path::new("."));
let dialect = crate::dialect::resolve(&config.dialect, source, dir);
let vidx = lines
.iter()
.position(|l| l.trim_start().starts_with("#version"));
match vidx {
Some(i) => b.push(
lines[i].to_string(),
Some(Loc {
path: target.to_path_buf(),
line: line_no(i),
}),
),
None => b.push_synthetic(DEFAULT_VERSION),
}
b.push_synthetic(DEFAULT_PRECISION);
if let Some(d) = dialect.as_ref().filter(|d| !d.deck)
&& let Some(p) = d.prelude(stage)
{
b.push_synthetic(p);
}
if let Some(d) = &dialect
&& let Some(p) = &d.prelude_extra
{
b.push_synthetic(p);
}
if let Some(d) = dialect.as_ref().filter(|d| !d.deck) {
let mut defines = d.define_names.clone();
if d.discover_defines {
defines.extend(crate::discover::injected_defines(dir));
}
for name in defines {
b.push_synthetic(&format!("#ifndef {name}\n#define {name} 1\n#endif"));
}
}
let uses_include = lines.iter().any(|l| crate::include::parse(l).is_some());
if dialect.as_ref().is_none_or(|d| !d.deck) && config.modules.is_empty() && !uses_include {
let explicit: Vec<PathBuf> = dialect
.as_ref()
.map(|d| d.prelude_files(stage))
.filter(|f| !f.is_empty())
.map(|f| f.iter().map(|n| dir.join(n)).collect())
.unwrap_or_default();
let libraries = if explicit.is_empty() {
crate::discover::shared_libraries(dir, stage, target)
} else {
explicit
};
for path in libraries {
if same_file(&path, target) {
continue;
}
if let Ok(c) = std::fs::read_to_string(&path) {
b.push_block(&c, &path);
}
}
}
let deck_active = match &dialect {
Some(d) => d.deck,
None => config.use_builtin_prelude,
};
if deck_active {
let fns = crate::deck::project_fns(target.parent().unwrap_or(Path::new(".")));
if fns.is_empty() {
b.push_synthetic(crate::preset::deck_prelude());
} else {
b.push_synthetic(&crate::deck::stubs(&fns));
}
}
for p in &config.preludes {
if let Ok(c) = std::fs::read_to_string(p) {
b.push_block(&c, p);
}
}
for m in &config.modules {
if same_file(m, target) {
continue;
}
if let Ok(c) = std::fs::read_to_string(m) {
b.push_block(&c, m);
}
}
let mut included: Vec<PathBuf> = target.canonicalize().ok().into_iter().collect();
for (i, l) in lines.iter().enumerate() {
if Some(i) == vidx {
continue;
}
let loc = Loc {
path: target.to_path_buf(),
line: line_no(i),
};
if let Some(spliced) = crate::include::expand(l, dir, &mut included) {
for (line, iloc) in spliced {
b.push(line, Some(iloc));
}
continue;
}
match dialect.as_ref().and_then(|d| d.expand_line(l, stage)) {
Some(expanded) => {
for e in expanded {
b.push(e, Some(loc.clone()));
}
}
None => b.push(l.to_string(), Some(loc)),
}
}
if let Some(d) = &dialect
&& let Some(ep) = &d.epilogue
&& !crate::dialect::has_main(source)
{
b.push_synthetic(ep);
}
b.finish(stage, target, None)
}
fn wrap_fragment(target: &Path, source: &str) -> Assembled {
wrap_as(target, source, Stage::Fragment)
}
fn wrap_as(target: &Path, source: &str, stage: Stage) -> Assembled {
let mut b = Builder::new();
b.push_synthetic(DEFAULT_VERSION);
b.push_synthetic(DEFAULT_PRECISION);
for (i, l) in source.lines().enumerate() {
b.push(
l.to_string(),
Some(Loc {
path: target.to_path_buf(),
line: line_no(i),
}),
);
}
b.push_synthetic("void main() {}");
b.finish(stage, target, Some("module fragment (syntax-only)"))
}
fn same_file(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn no_config() -> Config {
Config {
preludes: vec![],
modules: vec![],
use_builtin_prelude: false,
dialect: crate::dialect::Preference::default(),
}
}
#[test]
fn detect_stage_reads_the_filename() {
assert_eq!(
detect_stage(Path::new("draw.vert.glsl")),
Some(Stage::Vertex)
);
assert_eq!(
detect_stage(Path::new("draw.frag.glsl")),
Some(Stage::Fragment)
);
assert_eq!(
detect_stage(Path::new("sim.comp.glsl")),
Some(Stage::Compute)
);
assert_eq!(
detect_stage(Path::new("line.vertex.glsl")),
Some(Stage::Vertex)
);
assert_eq!(
detect_stage(Path::new("line.fragment.glsl")),
Some(Stage::Fragment)
);
assert_eq!(detect_stage(Path::new("windUniforms.glsl")), None);
}
#[test]
fn spelled_out_vertex_stage_is_not_wrapped_as_fragment() {
let src = "#version 300 es\nlayout(location = 0) in ivec2 a_pos_normal;\nvoid main() { gl_Position = vec4(vec2(a_pos_normal >> 1), 0.0, 1.0); }\n";
let a = assemble(Path::new("line.vertex.glsl"), src, &no_config());
assert_eq!(a.stage, Stage::Vertex);
}
#[test]
fn line_map_has_exactly_one_entry_per_assembled_line() {
let src =
"#version 300 es\nprecision highp float;\nout vec4 c;\nvoid main(){ c = vec4(1.0); }\n";
let a = assemble(Path::new("draw.frag.glsl"), src, &no_config());
assert_eq!(a.source.lines().count(), a.map.len());
}
#[test]
fn version_is_hoisted_and_mapped_to_its_original_line() {
let src = "#version 300 es\nout vec4 c;\nvoid main(){ c = vec4(1.0); }\n";
let a = assemble(Path::new("draw.frag.glsl"), src, &no_config());
assert!(a.source.starts_with("#version 300 es"));
assert_eq!(a.map[0].as_ref().unwrap().line, 1);
}
#[test]
fn default_precision_is_injected() {
let src = "#version 300 es\nout vec4 c;\nvoid main(){}\n";
let a = assemble(Path::new("draw.frag.glsl"), src, &no_config());
assert!(a.source.contains("precision highp float;"));
}
#[test]
fn maplibre_pragma_is_expanded_and_stays_one_to_one_mapped() {
let src = "#pragma maplibre: define lowp float opacity\n\
void main() {\n\
#pragma maplibre: initialize lowp float opacity\n\
gl_Position = vec4(opacity);\n\
}\n";
let a = assemble(Path::new("line.vertex.glsl"), src, &no_config());
assert_eq!(a.stage, Stage::Vertex);
assert_eq!(a.source.lines().count(), a.map.len());
assert!(a.source.contains("in lowp float a_opacity;"));
assert!(a.source.contains("out lowp float opacity;"));
assert!(!a.source.contains("#pragma maplibre"));
let idx = a
.source
.lines()
.position(|l| l.contains("out lowp float opacity;"))
.unwrap();
assert_eq!(a.map[idx].as_ref().unwrap().line, 1);
}
#[test]
fn no_dialect_leaves_a_plain_shader_untouched() {
let src = "out vec4 c;\nvoid main(){ c = vec4(1.0); }\n";
let a = assemble(Path::new("draw.frag.glsl"), src, &no_config());
assert!(a.source.contains("out vec4 c;"));
assert_eq!(a.source.lines().count(), a.map.len());
}
#[test]
fn bare_module_fragment_is_wrapped_for_syntax_checking() {
let src = "layout(std140) uniform U { float a; } u;\n";
let a = assemble(Path::new("windUniforms.glsl"), src, &no_config());
assert_eq!(a.stage, Stage::Fragment);
assert!(a.source.contains("void main()"));
assert_eq!(a.source.lines().count(), a.map.len());
}
#[test]
fn maplibre_injects_its_sibling_shared_library_mapped_to_its_own_file() {
let dir = std::env::temp_dir().join(format!(
"glslint-asm-{}-{}",
std::process::id(),
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
let lib = "vec4 projectTile(vec2 p) { return vec4(p, 0.0, 1.0); }\n";
std::fs::write(dir.join("_prelude.vertex.glsl"), lib).unwrap();
let shader = dir.join("background.vertex.glsl");
let src = "void main() { gl_Position = projectTile(vec2(0.0)); }\n";
let a = assemble(&shader, src, &no_config());
assert!(
a.source.contains("vec4 projectTile(vec2 p)"),
"shared library was injected: {}",
a.source
);
assert_eq!(a.source.lines().count(), a.map.len());
let idx = a
.source
.lines()
.position(|l| l.contains("projectTile(vec2 p)"))
.unwrap();
assert!(
a.map[idx]
.as_ref()
.unwrap()
.path
.ends_with("_prelude.vertex.glsl")
);
std::fs::remove_dir_all(&dir).ok();
}
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
}