blob: e4c17ef7340404daa62d138b44c288ea5cdf3b62 [file] [edit]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Build-time shader pipeline.
//!
//! Every top-level file in `shaders` is treated as a root module. WESL links each root with its
//! imports. By default, the linked WGSL is parsed into Naga IR, named declarations are
//! deterministically shortened while renderer-facing entry-point names remain unchanged, and
//! redundant comments and whitespace are removed from the serialized WGSL. The `unminified`
//! feature preserves the linked source for local inspection.
//!
//! The generated module embeds WGSL and, with the `glsl` feature, vertex and fragment GLSL plus
//! reflection metadata. Global names are recorded before default-mode renaming so that this
//! metadata continues to expose the resource names authored in WESL.
//!
//! With the `wgpu_external_texture` feature, roots in `TEXTURE_EXTERNAL_SHADERS` are linked a
//! second time with the `texture_external` WESL feature into an extra `<NAME>_TEXTURE_EXTERNAL`
//! WGSL constant. GLSL is always generated from the default link.
//!
//! ```text
//! |-> [default] renamed/minified WGSL -|
//! shaders/*.wesl -> linked WGSL -------| |-> WGSL constants
//! |-> [unminified] linked WGSL --------|
//! |-> [glsl] GLSL
//! | + reflection
//! |
//! OUT_DIR/compiled_shaders.rs <------------|
//! ```
use std::env;
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use wesl::Wesl;
/// Root modules that need a `texture_external` variant.
#[cfg(feature = "wgpu_external_texture")]
const TEXTURE_EXTERNAL_SHADERS: &[&str] = &["render"];
#[allow(warnings)]
#[cfg(feature = "glsl")]
#[path = "src/compile.rs"]
mod compile;
#[allow(warnings)]
#[cfg(feature = "glsl")]
#[path = "src/lint/mod.rs"]
mod lint;
#[path = "src/minify.rs"]
mod minify;
#[allow(warnings)]
#[cfg(feature = "glsl")]
#[path = "src/types.rs"]
mod types;
struct ShaderInfo {
name: String,
wgsl_source: String,
#[cfg(feature = "glsl")]
original_global_names: std::collections::BTreeMap<String, String>,
/// WGSL linked with the `texture_external` WESL feature, if this root has such a variant.
#[cfg(feature = "wgpu_external_texture")]
texture_external_wgsl_source: Option<String>,
}
// TODO: Format the generated code via `rustfmt`.
// TODO: Use `quote` instead of string concatenation to generate code.
fn main() {
// Rerun build if the shaders directory changes
println!("cargo:rerun-if-changed=shaders");
#[cfg(feature = "glsl")]
println!("cargo:rerun-if-changed=src/webgl_bootstrap.js");
#[cfg(feature = "glsl")]
println!("cargo:rerun-if-changed=src/webgl_bootstrap.d.ts");
let out_dir = env::var_os("OUT_DIR").unwrap();
// Build outputs a `compiled_shaders.rs` module containing the GLSL source and reflection
// metadata.
let dest_path = Path::new(&out_dir).join("compiled_shaders.rs");
// Link each WESL root module to WGSL.
let shader_dir = PathBuf::from("shaders");
let shader_infos = load_shader_infos(&shader_dir);
fs::write(dest_path, generate_compiled_shaders_module(&shader_infos)).unwrap();
}
fn load_shader_infos(shader_dir: &Path) -> Vec<ShaderInfo> {
let shader_names = load_shader_names(shader_dir);
let mut compiler = Wesl::new(shader_dir);
compiler.use_stripping(true);
compiler.set_feature(
"blurred_rounded_rect",
cfg!(feature = "blurred_rounded_rect"),
);
compiler.set_feature("full_image_support", cfg!(feature = "full_image_support"));
compiler.set_feature("gradient_sweep", cfg!(feature = "gradient_sweep"));
compiler.set_feature("texture_external", false);
#[cfg_attr(
not(feature = "wgpu_external_texture"),
expect(unused_mut, reason = "only extended with `texture_external` variants")
)]
let mut shader_infos: Vec<_> = shader_names
.into_iter()
.map(|name| compile_shader(&compiler, name))
.collect();
#[cfg(feature = "wgpu_external_texture")]
{
compiler.set_feature("texture_external", true);
for shader_info in &mut shader_infos {
if TEXTURE_EXTERNAL_SHADERS.contains(&shader_info.name.as_str()) {
let variant = compile_shader(&compiler, shader_info.name.clone());
shader_info.texture_external_wgsl_source = Some(variant.wgsl_source);
}
}
}
shader_infos
}
fn load_shader_names(shader_dir: &Path) -> Vec<String> {
let mut shader_names = fs::read_dir(shader_dir)
.expect("Unable to discover WESL shaders")
.filter_map(|entry| {
let path = entry.ok()?.path();
if path.extension()?.to_str()? == "wesl" {
Some(path.file_stem()?.to_str()?.to_owned())
} else {
None
}
})
.collect::<Vec<_>>();
shader_names.sort();
shader_names
}
fn compile_shader<R: wesl::Resolver>(compiler: &Wesl<R>, name: String) -> ShaderInfo {
let module_path = format!("package::{name}")
.parse()
.expect("generated WESL module path should be valid");
let linked_wgsl = compiler
.compile(&module_path)
.unwrap_or_else(|error| panic!("Unable to compile `{name}.wesl`: {error}"))
.to_string();
#[cfg(feature = "unminified")]
let wgsl_source = linked_wgsl;
#[cfg(all(feature = "unminified", feature = "glsl"))]
let original_global_names = std::collections::BTreeMap::new();
#[cfg(not(feature = "unminified"))]
let minified = minify::minify_wgsl(&linked_wgsl);
#[cfg(not(feature = "unminified"))]
let wgsl_source = minified.source;
#[cfg(all(not(feature = "unminified"), feature = "glsl"))]
let original_global_names = minified.original_global_names;
ShaderInfo {
name,
wgsl_source,
#[cfg(feature = "glsl")]
original_global_names,
#[cfg(feature = "wgpu_external_texture")]
texture_external_wgsl_source: None,
}
}
fn generate_compiled_shaders_module(shader_infos: &[ShaderInfo]) -> String {
let mut buf = String::new();
writeln!(
buf,
"// Generated code by `vello_gpu_shaders` - DO NOT EDIT"
)
.unwrap();
writeln!(buf, "/// WGSL shader sources linked from WESL modules.").unwrap();
writeln!(buf, "pub mod wgsl {{").unwrap();
for shader_info in shader_infos {
generate_wgsl_shader_module(&mut buf, shader_info).unwrap();
}
writeln!(
buf,
" /// All linked WGSL shader sources, keyed by WESL root module name."
)
.unwrap();
writeln!(buf, " pub const ALL: &[(&str, &str)] = &[").unwrap();
for shader_info in shader_infos {
let const_name = shader_info.name.to_uppercase();
writeln!(buf, " (\"{}\", {const_name}),", shader_info.name).unwrap();
}
writeln!(buf, " ];").unwrap();
writeln!(buf, "}}").unwrap();
// Implementation for creating a CompiledGlsl struct per shader assuming the standard entry
// names of `vs_main` and `fs_main`.
#[cfg(feature = "glsl")]
{
writeln!(
buf,
"/// Build-time GLSL shaders derived from linked WESL modules."
)
.unwrap();
let mut webgl_programs = Vec::new();
for shader_info in shader_infos {
let shader = compile::compile_wgsl_shader(
&shader_info.wgsl_source,
&shader_info.name,
"vs_main",
"fs_main",
&shader_info.original_global_names,
);
let generated_code = shader.to_generated_code(&shader_info.name);
writeln!(buf, "{generated_code}").unwrap();
if WEBGL_PROGRAMS.contains(&shader_info.name.as_str()) {
webgl_programs.push((
shader_info.name.clone(),
shader.vertex.source,
shader.fragment.source,
));
}
}
webgl_programs.sort_by_key(|(name, ..)| {
WEBGL_PROGRAMS
.iter()
.position(|program| program == name)
.unwrap()
});
assert_eq!(
webgl_programs.len(),
WEBGL_PROGRAMS.len(),
"every WebGL program must have a WESL root module"
);
generate_webgl_bootstrap_module(&mut buf, &webgl_programs).unwrap();
}
buf
}
/// WESL root modules compiled into programs by the WebGL renderer, in the renderer's order.
#[cfg(feature = "glsl")]
const WEBGL_PROGRAMS: [&str; 4] = ["render", "filter", "blend", "copy"];
/// Generates the JavaScript that compiles the WebGL programs before the WebAssembly module loads.
#[cfg(feature = "glsl")]
fn generate_webgl_bootstrap_module(
buf: &mut String,
programs: &[(String, String, String)],
) -> std::fmt::Result {
let mut body = String::new();
writeln!(body, "const PROGRAMS = {{")?;
for (name, vertex_source, fragment_source) in programs {
writeln!(
body,
" \"{name}\": [{}, {}],",
js_string_literal(vertex_source),
js_string_literal(fragment_source)
)?;
}
writeln!(body, "}};")?;
body.push_str(include_str!("src/webgl_bootstrap.js"));
let module_hash = fnv1a_hash(&body);
let mut js = String::new();
writeln!(js, "// Generated by `vello_gpu_shaders` - DO NOT EDIT")?;
writeln!(js, "const MODULE_HASH = \"{module_hash}\";")?;
js.push_str(&body);
writeln!(
buf,
"/// JavaScript for compiling the WebGL programs before the WebAssembly module loads."
)?;
writeln!(buf, "pub mod webgl_bootstrap {{")?;
writeln!(
buf,
" /// Identifies [`JS_MODULE`], covering both its GLSL sources and its JavaScript."
)?;
writeln!(buf, " pub const MODULE_HASH: &str = \"{module_hash}\";")?;
writeln!(
buf,
" /// ES module exporting `compileVelloShaders(canvas, contextAttributes = {{}})`."
)?;
writeln!(buf, " ///")?;
writeln!(
buf,
" /// It must be generated with the same shader features as the WebAssembly module."
)?;
writeln!(buf, " pub const JS_MODULE: &str = r###\"{js}\"###;")?;
writeln!(buf, " /// TypeScript declarations for [`JS_MODULE`].")?;
writeln!(buf, " ///")?;
writeln!(
buf,
" /// They aren't covered by [`MODULE_HASH`], since they don't affect which programs can be"
)?;
writeln!(buf, " /// adopted.")?;
writeln!(
buf,
" pub const DTS_MODULE: &str = r###\"// Generated by `vello_gpu_shaders` - DO NOT EDIT\n{}\"###;",
include_str!("src/webgl_bootstrap.d.ts")
)?;
writeln!(buf, "}}")?;
Ok(())
}
/// 64-bit FNV-1a, formatted as 16 hexadecimal digits.
#[cfg(feature = "glsl")]
fn fnv1a_hash(text: &str) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in text.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
format!("{hash:016x}")
}
#[cfg(feature = "glsl")]
fn js_string_literal(source: &str) -> String {
let mut literal = String::with_capacity(source.len() + 2);
literal.push('"');
for c in source.chars() {
match c {
'"' => literal.push_str("\\\""),
'\\' => literal.push_str("\\\\"),
'\n' => literal.push_str("\\n"),
'\r' => literal.push_str("\\r"),
'\t' => literal.push_str("\\t"),
c if c < ' ' || c == '\u{2028}' || c == '\u{2029}' => {
write!(literal, "\\u{:04x}", u32::from(c)).unwrap();
}
c => literal.push(c),
}
}
literal.push('"');
literal
}
fn generate_wgsl_shader_module(buf: &mut String, shader_info: &ShaderInfo) -> std::fmt::Result {
let const_name = shader_info.name.to_uppercase();
writeln!(
buf,
" /// Linked WGSL source for `{}.wesl`.",
shader_info.name
)?;
writeln!(
buf,
" pub const {const_name}: &str = r###\"{}\"###;",
shader_info.wgsl_source
)?;
#[cfg(feature = "wgpu_external_texture")]
if let Some(source) = &shader_info.texture_external_wgsl_source {
writeln!(
buf,
" /// Linked WGSL source for `{}.wesl` with a `texture_external` binding.",
shader_info.name
)?;
writeln!(
buf,
" pub const {const_name}_TEXTURE_EXTERNAL: &str = r###\"{source}\"###;"
)?;
}
Ok(())
}