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",
}
}
}
pub const BUILTIN_PRELUDE: &str = r#"// glslint built-in prelude: deck.gl project32
vec4 project_position_to_clipspace(vec3 position, vec3 position64Low, vec3 offset) {
return vec4(position + position64Low + offset, 1.0);
}
vec2 project_pixel_size_to_clipspace(vec2 pixels) { return pixels; }
vec3 project_position(vec3 position) { return position; }
vec4 project_common_position_to_clipspace(vec4 position) { return position; }
"#;
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.ends_with(".vert") || n.ends_with(".vs") {
Some(Stage::Vertex)
} else if n.contains(".frag.") || n.ends_with(".frag") || n.ends_with(".fs") {
Some(Stage::Fragment)
} else if n.contains(".comp.") || n.ends_with(".comp") {
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 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 config.use_builtin_prelude {
let fns = crate::deck::project_fns(target.parent().unwrap_or(Path::new(".")));
if fns.is_empty() {
b.push_synthetic(BUILTIN_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);
}
}
for (i, l) in lines.iter().enumerate() {
if Some(i) == vidx {
continue;
}
b.push(
l.to_string(),
Some(Loc {
path: target.to_path_buf(),
line: line_no(i),
}),
);
}
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,
}
}
#[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("windUniforms.glsl")), None);
}
#[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 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());
}
}