Skip to main content

wgpu_hal/
glsl_template.rs

1//! GLSL ES written ahead of time for one entry point, its pipeline constants
2//! preprocessor macros. For a pipeline, the GL backend works out the
3//! template's directives from the pipeline's constants and hands the browser
4//! only the code they keep, as [`crate::auxil::prune`] would: no naga runs
5//! when the pipeline is made.
6
7use alloc::{borrow::Cow, format, string::String, vec::Vec};
8use core::fmt::Write as _;
9
10use hashbrown::HashMap;
11
12/// How a pipeline constant's value is written as its macro.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum GlslMacroKind {
15    Bool,
16    Sint,
17    Uint,
18    Float,
19}
20
21/// A pipeline constant a template reads as a macro.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct GlslMacro {
24    /// The constant's name among the pipeline's constants.
25    pub constant: &'static str,
26    /// The macro's name in the template.
27    pub name: &'static str,
28    pub kind: GlslMacroKind,
29}
30
31/// A uniform or storage block, by the name the template gives it.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct GlslBlock {
34    pub name: &'static str,
35    pub storage: bool,
36    pub binding: naga::ResourceBinding,
37}
38
39/// A texture, by the name the template gives it, with the sampler GLSL ES
40/// combines it with.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct GlslTexture {
43    pub name: &'static str,
44    pub storage: bool,
45    pub binding: naga::ResourceBinding,
46    pub sampler: Option<naga::ResourceBinding>,
47}
48
49/// GLSL ES of one entry point whose pipeline constants are macros, and the
50/// names the backend binds its resources by.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct GlslTemplate {
53    pub entry_point: &'static str,
54    pub stage: naga::ShaderStage,
55    /// The GLSL, in pieces that templates share: the first is the
56    /// `#version` line. Each macro's default is defined under `#ifndef`, so
57    /// a definition in front of it wins.
58    pub glsl: &'static [&'static str],
59    pub macros: &'static [GlslMacro],
60    pub blocks: &'static [GlslBlock],
61    pub textures: &'static [GlslTexture],
62    /// Each vertex input or fragment output, by name, with its location.
63    pub varyings: &'static [(&'static str, u32)],
64    pub clip_distance_count: u32,
65}
66
67impl GlslTemplate {
68    /// The template's GLSL for a pipeline whose constants are `constants`:
69    /// its directives worked out here, so the browser gets only the code the
70    /// constants keep, with each constant the code reads defined after the
71    /// `#version` line. Errors on a value a macro cannot hold and on a
72    /// directive the template should not have.
73    pub fn expand(&self, constants: &naga::back::PipelineConstants) -> Result<String, String> {
74        let mut macros = Macros::default();
75        for item in self.macros {
76            if let Some(&value) = constants.get(item.constant) {
77                macros.define(item.name, Cow::Owned(macro_value(item, value)?));
78            }
79        }
80        let length = self.glsl.iter().map(|piece| piece.len()).sum::<usize>();
81        let mut code = String::with_capacity(length);
82        let mut version = "";
83        // Whether the lines of each open conditional are kept.
84        let mut kept: Vec<bool> = Vec::new();
85        for line in self.glsl.iter().flat_map(|piece| piece.split_inclusive('\n')) {
86            let keeping = kept.last().copied().unwrap_or(true);
87            let Some(directive) = line.trim_start().strip_prefix('#') else {
88                if keeping {
89                    code.push_str(line);
90                }
91                continue;
92            };
93            let directive = directive.trim_end();
94            let (name, rest) = directive.split_once(' ').unwrap_or((directive, ""));
95            match name {
96                "version" => version = line,
97                "ifndef" => kept.push(keeping && !macros.defined(rest.trim())),
98                "if" => {
99                    let holds = keeping && macros.evaluate(rest)? != 0;
100                    kept.push(holds);
101                }
102                "endif" => {
103                    kept.pop().ok_or("#endif without #if")?;
104                }
105                "define" if keeping => {
106                    let (macro_name, value) = rest.split_once(' ').unwrap_or((rest, ""));
107                    macros.define(macro_name, Cow::Borrowed(value));
108                }
109                "define" => {}
110                other => return Err(format!("#{other} in a GLSL template")),
111            }
112        }
113        if !kept.is_empty() {
114            return Err("a GLSL template leaves a conditional open".into());
115        }
116        let mut glsl = String::with_capacity(version.len() + 48 * self.macros.len() + code.len());
117        glsl.push_str(version);
118        for item in self.macros {
119            if let Some(value) = macros.values.get(item.name) {
120                let _ = writeln!(glsl, "#define {} {value}", item.name);
121            }
122        }
123        glsl.push_str(&code);
124        Ok(glsl)
125    }
126}
127
128/// A pipeline constant's value as its macro: a bool as 1 or 0, an integer
129/// without a suffix, which the preprocessor reads, and a float with its
130/// point.
131fn macro_value(item: &GlslMacro, value: f64) -> Result<String, String> {
132    let unfit = || format!("{}: {value} is no {:?}", item.constant, item.kind);
133    Ok(match item.kind {
134        GlslMacroKind::Bool => format!("{}", u8::from(value != 0.0)),
135        GlslMacroKind::Sint => {
136            if value % 1.0 != 0.0
137                || !(f64::from(i32::MIN)..=f64::from(i32::MAX)).contains(&value)
138            {
139                return Err(unfit());
140            }
141            format!("{}", value as i32)
142        }
143        // An integer literal is signed: a value past `i32::MAX` is written
144        // as its bit pattern.
145        GlslMacroKind::Uint => {
146            if value % 1.0 != 0.0 || !(0.0..=f64::from(u32::MAX)).contains(&value) {
147                return Err(unfit());
148            }
149            let value = value as u32;
150            if value > i32::MAX as u32 {
151                format!("{value:#X}")
152            } else {
153                format!("{value}")
154            }
155        }
156        GlslMacroKind::Float => {
157            if !value.is_finite() {
158                return Err(unfit());
159            }
160            format!("{:?}", value as f32)
161        }
162    })
163}
164
165/// The macros a template defines, and the integer value of each its
166/// directives read.
167#[derive(Default)]
168struct Macros<'t> {
169    values: HashMap<&'t str, Cow<'t, str>>,
170    evaluated: HashMap<&'t str, i64>,
171}
172
173impl<'t> Macros<'t> {
174    /// Defines `name` unless it is: a pipeline's value comes first.
175    fn define(&mut self, name: &'t str, value: Cow<'t, str>) {
176        self.values.entry(name).or_insert(value);
177    }
178
179    fn defined(&self, name: &str) -> bool {
180        self.values.contains_key(name)
181    }
182
183    /// The value of the `#if` expression `text`.
184    fn evaluate(&mut self, text: &str) -> Result<i64, String> {
185        Evaluator {
186            values: &self.values,
187            evaluated: &mut self.evaluated,
188        }
189        .evaluate(text)
190    }
191}
192
193/// Works out expressions over macros, each macro's value once.
194struct Evaluator<'a, 't> {
195    values: &'a HashMap<&'t str, Cow<'t, str>>,
196    evaluated: &'a mut HashMap<&'t str, i64>,
197}
198
199impl Evaluator<'_, '_> {
200    fn evaluate(&mut self, text: &str) -> Result<i64, String> {
201        let mut parser = Parser { text, at: 0 };
202        let value = parser.binary(self, 0)?;
203        parser.skip_space();
204        if parser.at != text.len() {
205            return Err(format!("`{text}` does not end at {}", parser.at));
206        }
207        Ok(value)
208    }
209
210    fn value(&mut self, name: &str) -> Result<i64, String> {
211        if let Some(&value) = self.evaluated.get(name) {
212            return Ok(value);
213        }
214        let values = self.values;
215        let (&key, text) = values
216            .get_key_value(name)
217            .ok_or_else(|| format!("`{name}` is not defined"))?;
218        let value = self.evaluate(text)?;
219        self.evaluated.insert(key, value);
220        Ok(value)
221    }
222}
223
224/// Binary operators by precedence, loosest first, as C's.
225const LEVELS: [&[&str]; 10] = [
226    &["||"],
227    &["&&"],
228    &["|"],
229    &["^"],
230    &["&"],
231    &["==", "!="],
232    &["<=", ">=", "<", ">"],
233    &["<<", ">>"],
234    &["+", "-"],
235    &["*", "/", "%"],
236];
237
238struct Parser<'s> {
239    text: &'s str,
240    at: usize,
241}
242
243impl Parser<'_> {
244    fn skip_space(&mut self) {
245        while self.text[self.at..].starts_with([' ', '\t']) {
246            self.at += 1;
247        }
248    }
249
250    /// The operator of `level` at the cursor, taken.
251    fn operator(&mut self, level: usize) -> Option<&'static str> {
252        self.skip_space();
253        let rest = &self.text[self.at..];
254        let found = LEVELS[level].iter().copied().find(|operator| {
255            rest.starts_with(operator)
256                // `&` and `|` are not the first half of `&&` and `||`, nor
257                // `<` of `<<`.
258                && !(operator.len() == 1 && rest[1..].starts_with(*operator))
259        })?;
260        self.at += found.len();
261        Some(found)
262    }
263
264    fn binary(&mut self, macros: &mut Evaluator<'_, '_>, level: usize) -> Result<i64, String> {
265        if level == LEVELS.len() {
266            return self.unary(macros);
267        }
268        let mut value = self.binary(macros, level + 1)?;
269        while let Some(operator) = self.operator(level) {
270            let right = self.binary(macros, level + 1)?;
271            value = match operator {
272                "||" => i64::from(value != 0 || right != 0),
273                "&&" => i64::from(value != 0 && right != 0),
274                "|" => value | right,
275                "^" => value ^ right,
276                "&" => value & right,
277                "==" => i64::from(value == right),
278                "!=" => i64::from(value != right),
279                "<" => i64::from(value < right),
280                ">" => i64::from(value > right),
281                "<=" => i64::from(value <= right),
282                ">=" => i64::from(value >= right),
283                "<<" => value.wrapping_shl(right as u32),
284                ">>" => value.wrapping_shr(right as u32),
285                "+" => value.wrapping_add(right),
286                "-" => value.wrapping_sub(right),
287                "*" => value.wrapping_mul(right),
288                "/" => value.checked_div(right).ok_or("division by zero")?,
289                _ => value.checked_rem(right).ok_or("division by zero")?,
290            };
291        }
292        Ok(value)
293    }
294
295    fn unary(&mut self, macros: &mut Evaluator<'_, '_>) -> Result<i64, String> {
296        self.skip_space();
297        let rest = &self.text[self.at..];
298        let Some(first) = rest.chars().next() else {
299            return Err(format!("`{}` ends where a value goes", self.text));
300        };
301        match first {
302            '!' | '~' | '-' | '+' => {
303                self.at += 1;
304                let value = self.unary(macros)?;
305                Ok(match first {
306                    '!' => i64::from(value == 0),
307                    '~' => !value,
308                    '-' => value.wrapping_neg(),
309                    _ => value,
310                })
311            }
312            '(' => {
313                self.at += 1;
314                let value = self.binary(macros, 0)?;
315                self.skip_space();
316                if !self.text[self.at..].starts_with(')') {
317                    return Err(format!("`{}` leaves a parenthesis open", self.text));
318                }
319                self.at += 1;
320                Ok(value)
321            }
322            _ => {
323                let length = rest
324                    .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
325                    .unwrap_or(rest.len());
326                let word = &rest[..length];
327                self.at += length;
328                if first.is_ascii_digit() {
329                    let (digits, radix) = match word.strip_prefix("0x").or_else(|| word.strip_prefix("0X")) {
330                        Some(hex) => (hex, 16),
331                        None => (word, 10),
332                    };
333                    i64::from_str_radix(digits.trim_end_matches(['u', 'U']), radix)
334                        .map_err(|error| format!("`{word}`: {error}"))
335                } else if word.is_empty() {
336                    Err(format!("`{}` has no value at {}", self.text, self.at))
337                } else {
338                    macros.value(word)
339                }
340            }
341        }
342    }
343}