use core::fmt::Write as _;
use wgslender::{Error, MinifyOptions, Strictness, compile, minify_with, validate};
const DEMO: &str = include_str!("../tests/fixtures/demo.wgsl");
const UNPARSEABLE: &str = "fn main( { let ; }";
const UNDECLARED: &str = "\
@compute @workgroup_size(1)
fn main() {
let x = undeclared_variable;
}
";
fn main() -> Result<(), Error> {
mechanics()?;
when_it_pays()?;
failure()
}
fn mechanics() -> Result<(), Error> {
heading("1. what comes out");
let compiled = compile(DEMO, &MinifyOptions::default())?;
println!("{compiled:?}");
let head: Vec<String> = compiled
.wasm
.iter()
.take(8)
.map(|byte| format!("{byte:02x}"))
.collect();
println!("\nthe first eight bytes: {}", head.join(" "));
println!(" 00 61 73 6d the magic number — \\0asm");
println!(" 01 00 00 00 the binary format version, 1");
assert!(
compiled.wasm.starts_with(b"\0asm"),
"compile did not produce a wasm module"
);
println!(
" starts_with(b\"\\0asm\"): {}",
compiled.wasm.starts_with(b"\0asm")
);
let expanded = minify_with(DEMO, &compressible())?;
println!(
"\n{:<16}{:>6} the source that went in",
"original_size", compiled.original_size
);
println!(
"{:<16}{:>6} the module that came out",
"wasm.len()",
compiled.wasm.len()
);
println!(
"{:<16}{:>6} the minified text it will write",
"generate()",
expanded.len()
);
println!(
"\nThe module imports nothing and exports two things: `memory`, and a\n\
`generate() -> i32` that writes the minified WGSL at offset 0 and returns\n\
how many bytes it wrote. That is the whole runtime:\n\
\n\
\x20 const {{ instance }} = await WebAssembly.instantiate(wasm);\n\
\x20 const length = instance.exports.generate();\n\
\x20 const wgsl = new TextDecoder().decode(\n\
\x20 new Uint8Array(instance.exports.memory.buffer, 0, length),\n\
\x20 );\n\
\x20 device.createShaderModule({{ code: wgsl }});\n\
\n\
It is a compressed shader, not a compiled pipeline — the WGSL still goes to\n\
`createShaderModule` on the other side. Note which number `original_size`\n\
is: the input, not the output of `generate`, which is smaller because it is\n\
minified."
);
Ok(())
}
fn when_it_pays() -> Result<(), Error> {
heading("2. when it is worth doing");
println!(
"{:<28}{:>8}{:>10}{:>8}{:>12}",
"shader", "source", "minified", "wasm", "wasm ÷ text"
);
let mut rows = vec![
measure("nothing but an entry point", &synthetic(0))?,
measure("demo.wgsl", DEMO)?,
];
for helpers in [4, 8, 16, 48] {
let source = synthetic(helpers);
rows.push(measure(&format!("{helpers} generated helpers"), &source)?);
}
let floor = rows.iter().map(|(_, wasm)| *wasm).min().unwrap_or_default();
let largest_loss = rows.iter().filter(|(t, w)| w >= t).map(|(t, _)| *t).max();
let smallest_win = rows.iter().filter(|(t, w)| w < t).map(|(t, _)| *t).min();
let found = match (largest_loss, smallest_win) {
(Some(loss), Some(win)) => format!("between {loss} and {win} bytes of minified text"),
(None, Some(win)) => format!("below {win} bytes — every row here won"),
(Some(loss), None) => format!("above {loss} bytes — no row here reached it"),
(None, None) => "nothing was measured".to_string(),
};
println!(
"\nA percentage over 100 means the module is the larger thing to ship. The\n\
smallest module in that table is {floor} bytes, for a shader with nothing in\n\
it: most of that is the decoder and the wasm framing, and it is paid whether\n\
the shader is two lines or two thousand. Byte-pair encoding only starts to\n\
outrun the fixed cost once there is enough repeated text to encode, and the\n\
crossover in this run fell {found}.\n\
\n\
Take the winning rows as an upper bound rather than a forecast: even with\n\
six different bodies, generated helpers repeat more than hand-written code\n\
does, and repetition is what the encoder is paid in. On two real shaders\n\
from this repository — which live outside the published package, so this\n\
example cannot run them for you — the same curve is gentler: 4 292 bytes of\n\
source minify to 1 116 and compile to 998 (89%), and 28 855 minify to 7 722\n\
and compile to 4 676 (61%).\n\
\n\
So: a large shader — not a small one. For a small one the minified text is\n\
the smaller artifact, and `include_wgsl!` already ships it."
);
Ok(())
}
fn failure() -> Result<(), Error> {
heading("3. what counts as failure");
println!("compile({UNPARSEABLE:?})");
match compile(UNPARSEABLE, &MinifyOptions::default()) {
Err(Error::Compile(diagnostics)) => {
println!(
" -> Err(Compile), carrying {} diagnostics:",
diagnostics.len()
);
for diagnostic in &diagnostics {
let position = format!("{}:{}", diagnostic.line, diagnostic.column);
println!(
" {position:<7}{:<7}{}",
diagnostic.code.as_deref().unwrap_or("-"),
diagnostic.message,
);
}
}
Ok(compiled) => println!(" -> Ok, {} bytes", compiled.wasm.len()),
Err(other) => return Err(other),
}
println!("\ncompile(a shader that parses and then means nothing)");
let verdict = validate(UNDECLARED, Strictness::Default)?;
let compiled = compile(UNDECLARED, &MinifyOptions::default())?;
println!(
" validate -> valid: {}, errors: {}, the first of them: {}",
verdict.valid,
verdict.error_count,
verdict
.diagnostics
.first()
.map_or("none", |diagnostic| diagnostic.message.as_str()),
);
println!(" compile -> Ok, {} bytes of wasm", compiled.wasm.len());
println!(
"\nOnly the parser stops `compile`. It never type-checks, so a shader that\n\
is nonsense in every way except syntactically becomes a module that\n\
faithfully expands back into that nonsense — and the failure surfaces at\n\
`createShaderModule`, at run time, on someone else's machine. Call\n\
`validate` first if the source is not already known good; `include_wgsl!`\n\
does exactly that before it embeds anything."
);
Ok(())
}
fn measure(label: &str, source: &str) -> Result<(usize, usize), Error> {
let minified = minify_with(source, &compressible())?.len();
let wasm = compile(source, &MinifyOptions::default())?.wasm.len();
println!(
"{label:<28}{:>8}{:>10}{:>8}{:>11}%",
source.len(),
minified,
wasm,
percent(wasm, minified),
);
Ok((minified, wasm))
}
fn compressible() -> MinifyOptions {
MinifyOptions::default()
.sort_declarations(true)
.scope_local_rename(true)
}
fn synthetic(helpers: usize) -> String {
let declarations = (0..helpers).fold(String::new(), |mut out, n| {
let _ = write!(
out,
"fn stage_{n}(value: f32, weight: f32) -> f32 {{\n{}\n}}\n\n",
body(n)
);
out
});
let calls = (0..helpers).fold(String::new(), |mut out, n| {
let _ = writeln!(out, " value = stage_{n}(value, {}.0);", n + 1);
out
});
format!(
"@group(0) @binding(0) var<storage, read_write> data: array<f32>;\n\n\
{declarations}\
@compute @workgroup_size(64)\n\
fn main(@builtin(global_invocation_id) id: vec3u) {{\n\
\x20 var value = data[id.x];\n\
{calls}\
\x20 data[id.x] = value;\n\
}}\n"
)
}
fn body(n: usize) -> String {
match n % 6 {
0 => format!(
" let scaled = value * weight + {n}.0;\n return scaled - floor(scaled / 8.0) * 8.0;"
),
1 => format!(" return mix(value, weight, fract(value * {n}.5));"),
2 => format!(
" let angle = value * {n}.0 + weight;\n return sin(angle) * cos(angle * 0.5);"
),
3 => format!(" return clamp(value + weight * {n}.25, -{n}.0, {n}.0);"),
4 => format!(
" let ramp = smoothstep(0.0, {n}.0, value);\n return ramp * weight + value;"
),
_ => format!(
" var acc = value;\n for (var i = 0u; i < {n}u; i = i + 1u) {{\n acc = acc * 0.5 + weight;\n }}\n return acc;"
),
}
}
fn percent(part: usize, whole: usize) -> usize {
if whole == 0 { 0 } else { part * 100 / whole }
}
fn heading(title: &str) {
println!("\n{title}");
println!("{}", "-".repeat(title.chars().count()));
}