shader_sense/validator/
glslang.rs

1use super::validator::ValidatorImpl;
2use crate::{
3    include::IncludeHandler,
4    position::{ShaderFileRange, ShaderPosition},
5    shader::{GlslSpirvVersion, GlslTargetClient, ShaderParams, ShaderStage},
6    shader_error::{ShaderDiagnostic, ShaderDiagnosticList, ShaderDiagnosticSeverity, ShaderError},
7};
8use glslang::{
9    error::GlslangError,
10    include::{IncludeResult, IncludeType},
11    Compiler, CompilerOptions, ShaderInput, ShaderSource,
12};
13use std::{
14    collections::HashMap,
15    path::{Path, PathBuf},
16};
17
18impl Into<glslang::ShaderStage> for ShaderStage {
19    fn into(self) -> glslang::ShaderStage {
20        match self {
21            ShaderStage::Vertex => glslang::ShaderStage::Vertex,
22            ShaderStage::Fragment => glslang::ShaderStage::Fragment,
23            ShaderStage::Compute => glslang::ShaderStage::Compute,
24            ShaderStage::TesselationControl => glslang::ShaderStage::TesselationControl,
25            ShaderStage::TesselationEvaluation => glslang::ShaderStage::TesselationEvaluation,
26            ShaderStage::Mesh => glslang::ShaderStage::Mesh,
27            ShaderStage::Task => glslang::ShaderStage::Task,
28            ShaderStage::Geometry => glslang::ShaderStage::Geometry,
29            ShaderStage::RayGeneration => glslang::ShaderStage::RayGeneration,
30            ShaderStage::ClosestHit => glslang::ShaderStage::ClosestHit,
31            ShaderStage::AnyHit => glslang::ShaderStage::AnyHit,
32            ShaderStage::Callable => glslang::ShaderStage::Callable,
33            ShaderStage::Miss => glslang::ShaderStage::Miss,
34            ShaderStage::Intersect => glslang::ShaderStage::Intersect,
35        }
36    }
37}
38
39pub struct Glslang {
40    hlsl: bool,
41    compiler: &'static Compiler,
42
43    // Cache regex for parsing.
44    diagnostic_regex: regex::Regex,
45    internal_diagnostic_regex: regex::Regex,
46}
47
48impl Glslang {
49    #[allow(dead_code)] // Only used for WASI (alternative to DXC)
50    pub fn hlsl() -> Self {
51        Self::new(true)
52    }
53    pub fn glsl() -> Self {
54        Self::new(false)
55    }
56    fn new(hlsl: bool) -> Self {
57        let compiler = Compiler::acquire().expect("Failed to create glslang compiler");
58        Self {
59            hlsl: hlsl,
60            compiler,
61            diagnostic_regex: regex::Regex::new(r"(?m)^(.*?:(?:  \d+:\d+:)?)").unwrap(),
62            internal_diagnostic_regex: regex::Regex::new(
63                r"(?s)^(.*?):(?: ((?:[a-zA-Z]:)?[\d\w\.\/\\\-]+):(\d+):(\d+):)?(.+)",
64            )
65            .unwrap(),
66        }
67    }
68}
69
70struct GlslangIncludeHandler<'a> {
71    include_handler: IncludeHandler,
72    include_callback: &'a mut dyn FnMut(&Path) -> Option<String>,
73}
74
75impl<'a> GlslangIncludeHandler<'a> {
76    pub fn new(
77        file_path: &'a Path,
78        includes: Vec<String>,
79        path_remapping: HashMap<PathBuf, PathBuf>,
80        include_callback: &'a mut dyn FnMut(&Path) -> Option<String>,
81    ) -> Self {
82        Self {
83            include_handler: IncludeHandler::main(file_path, includes, path_remapping),
84            include_callback: include_callback,
85        }
86    }
87}
88
89impl glslang::include::IncludeHandler for GlslangIncludeHandler<'_> {
90    fn include(
91        &mut self,
92        _ty: IncludeType, // TODO: should use them ?
93        header_name: &str,
94        _includer_name: &str,
95        include_depth: usize,
96    ) -> Option<IncludeResult> {
97        // Glslang does not handle stack overflow natively. So put a limit there.
98        if include_depth > IncludeHandler::DEPTH_LIMIT {
99            None
100        } else {
101            match self
102                .include_handler
103                .search_in_includes(Path::new(header_name), self.include_callback)
104            {
105                Some((content, path)) => {
106                    self.include_handler.push_directory_stack(&path);
107                    Some(IncludeResult {
108                        name: String::from(header_name),
109                        data: content,
110                    })
111                }
112                None => None,
113            }
114        }
115    }
116}
117
118impl Glslang {
119    fn parse_errors(
120        &self,
121        errors: &String,
122        file_path: &Path,
123        params: &ShaderParams,
124        offset_first_line: bool,
125    ) -> Result<ShaderDiagnosticList, ShaderError> {
126        let mut shader_error_list = ShaderDiagnosticList::empty();
127
128        let mut starts = Vec::new();
129        for capture in self.diagnostic_regex.captures_iter(errors.as_str()) {
130            if let Some(pos) = capture.get(0) {
131                starts.push(pos.start());
132            }
133        }
134        starts.push(errors.len());
135        let mut include_handler = IncludeHandler::main(
136            file_path,
137            params.context.includes.clone(),
138            params.context.path_remapping.clone(),
139        );
140        // Cache includes as its a heavy operation.
141        let mut include_cache: HashMap<String, PathBuf> = HashMap::new();
142        for start in 0..starts.len() - 1 {
143            let begin = starts[start];
144            let end = starts[start + 1];
145            let block = &errors[begin..end];
146            if block.contains("compilation errors.  No code generated.") {
147                continue; // Skip this useless string.
148            }
149            if let Some(capture) = self.internal_diagnostic_regex.captures(block) {
150                let level = capture.get(1).map_or("", |m| m.as_str());
151                let relative_path = capture.get(2).map_or("", |m| m.as_str());
152                let line = capture.get(3).map_or("", |m| m.as_str());
153                let pos = capture.get(4).map_or("", |m| m.as_str());
154                let msg = capture.get(5).map_or("", |m| m.as_str());
155                let file_path: PathBuf = match relative_path.parse::<u32>() {
156                    Ok(_) => file_path.into(), // Main file
157                    Err(_) => {
158                        if relative_path.is_empty() {
159                            file_path.into()
160                        } else {
161                            include_cache
162                                .entry(relative_path.into())
163                                .or_insert_with(|| {
164                                    include_handler
165                                        .search_path_in_includes(Path::new(&relative_path))
166                                        .unwrap_or(file_path.into())
167                                })
168                                .clone()
169                        }
170                    }
171                };
172                let line = {
173                    // Line is indexed from 1 in glslang, so remove one line (and another one if we offset from first line).
174                    // But sometimes, it return a line of zero (probably some non initialized position) so check this aswell.
175                    let offset = 1 + offset_first_line as u32;
176                    let line = line.parse::<u32>().unwrap_or(offset);
177                    if line < offset {
178                        0
179                    } else {
180                        line - offset
181                    }
182                };
183                let pos = pos.parse::<u32>().unwrap_or(0);
184                shader_error_list.push(ShaderDiagnostic {
185                    severity: match level {
186                        "ERROR" => ShaderDiagnosticSeverity::Error,
187                        "WARNING" => ShaderDiagnosticSeverity::Warning,
188                        "NOTE" => ShaderDiagnosticSeverity::Information,
189                        "HINT" => ShaderDiagnosticSeverity::Hint,
190                        _ => ShaderDiagnosticSeverity::Error,
191                    },
192                    error: String::from(msg),
193                    range: ShaderFileRange::new(
194                        file_path.clone(),
195                        ShaderPosition::new(line, pos),
196                        ShaderPosition::new(line, pos),
197                    ),
198                });
199            } else {
200                return Err(ShaderError::InternalErr(format!(
201                    "Failed to parse regex: {}",
202                    block
203                )));
204            }
205        }
206
207        if shader_error_list.is_empty() {
208            return Err(ShaderError::InternalErr(format!(
209                "Failed to parse errors: {}",
210                errors
211            )));
212        }
213        return Ok(shader_error_list);
214    }
215
216    fn from_glslang_error(
217        &self,
218        err: GlslangError,
219        file_path: &Path,
220        params: &ShaderParams,
221        offset_first_line: bool,
222    ) -> Result<ShaderDiagnosticList, ShaderError> {
223        match err {
224            GlslangError::PreprocessError(error) => {
225                self.parse_errors(&error, file_path, &params, offset_first_line)
226            }
227            GlslangError::ParseError(error) => {
228                self.parse_errors(&error, file_path, &params, offset_first_line)
229            }
230            GlslangError::LinkError(error) => {
231                self.parse_errors(&error, file_path, &params, offset_first_line)
232            }
233            GlslangError::ShaderStageNotFound(stage) => Err(ShaderError::InternalErr(format!(
234                "Shader stage not found: {:#?}",
235                stage
236            ))),
237            GlslangError::InvalidProfile(target, value, profile) => {
238                Err(ShaderError::InternalErr(format!(
239                    "Invalid profile {} for target {:#?}: {:#?}",
240                    value, target, profile
241                )))
242            }
243            GlslangError::VersionUnsupported(value, profile) => Err(ShaderError::InternalErr(
244                format!("Unsupported profile {}: {:#?}", value, profile),
245            )),
246            err => Err(ShaderError::InternalErr(format!(
247                "Internal error: {:#?}",
248                err
249            ))),
250        }
251    }
252}
253impl ValidatorImpl for Glslang {
254    fn validate_shader(
255        &self,
256        content: &str,
257        file_path: &Path,
258        params: &ShaderParams,
259        include_callback: &mut dyn FnMut(&Path) -> Option<String>,
260    ) -> Result<ShaderDiagnosticList, ShaderError> {
261        let file_name = self.get_file_name(file_path);
262
263        let (shader_stage, shader_source, offset_first_line) =
264            if let Some(variant_stage) = params.compilation.shader_stage {
265                (variant_stage, content.into(), false)
266            } else if let Some(shader_stage) = ShaderStage::from_file_name(&file_name) {
267                (shader_stage, content.into(), false)
268            } else {
269                // If we dont have a stage, might require some preprocess to avoid errors.
270                // glslang **REQUIRES** to have stage for linting.
271                let default_stage = ShaderStage::Fragment;
272                if self.hlsl {
273                    // HLSL does not require version, simply assume stage.
274                    (default_stage, content.into(), false)
275                } else {
276                    // glslang does not support linting header file, so to lint them,
277                    // Assume Fragment & add default #version if missing
278                    if content.contains("#version ") {
279                        // Main file with missing stage.
280                        (default_stage, content.into(), false)
281                    } else {
282                        // Header file with missing stage & missing version.
283                        // WARN: Assumed this string is one line offset only.
284                        let version_header = String::from("#version 450\n");
285                        (default_stage, version_header + content, true)
286                    }
287                }
288            };
289
290        let source = ShaderSource::try_from(shader_source).expect("Failed to read from source");
291
292        let defines_copy = params.context.defines.clone();
293        let defines: Vec<(&str, Option<&str>)> = defines_copy
294            .iter()
295            .map(|v| (&v.0 as &str, Some(&v.1 as &str)))
296            .collect();
297        let mut include_handler = GlslangIncludeHandler::new(
298            file_path,
299            params.context.includes.clone(),
300            params.context.path_remapping.clone(),
301            include_callback,
302        );
303
304        let lang_version = match params.compilation.glsl.spirv {
305            GlslSpirvVersion::SPIRV1_0 => glslang::SpirvVersion::SPIRV1_0,
306            GlslSpirvVersion::SPIRV1_1 => glslang::SpirvVersion::SPIRV1_1,
307            GlslSpirvVersion::SPIRV1_2 => glslang::SpirvVersion::SPIRV1_2,
308            GlslSpirvVersion::SPIRV1_3 => glslang::SpirvVersion::SPIRV1_3,
309            GlslSpirvVersion::SPIRV1_4 => glslang::SpirvVersion::SPIRV1_4,
310            GlslSpirvVersion::SPIRV1_5 => glslang::SpirvVersion::SPIRV1_5,
311            GlslSpirvVersion::SPIRV1_6 => glslang::SpirvVersion::SPIRV1_6,
312        };
313        let input = match ShaderInput::new(
314            &source,
315            shader_stage.into(),
316            &CompilerOptions {
317                source_language: if self.hlsl {
318                    glslang::SourceLanguage::HLSL
319                } else {
320                    glslang::SourceLanguage::GLSL
321                },
322                // Should have some settings to select these.
323                target: if self.hlsl {
324                    glslang::Target::None(Some(lang_version))
325                } else {
326                    if params.compilation.glsl.client.is_opengl() {
327                        glslang::Target::OpenGL {
328                            version: glslang::OpenGlVersion::OpenGL4_5,
329                            spirv_version: None, // TODO ?
330                        }
331                    } else {
332                        let client_version = match params.compilation.glsl.client {
333                            GlslTargetClient::Vulkan1_0 => glslang::VulkanVersion::Vulkan1_0,
334                            GlslTargetClient::Vulkan1_1 => glslang::VulkanVersion::Vulkan1_1,
335                            GlslTargetClient::Vulkan1_2 => glslang::VulkanVersion::Vulkan1_2,
336                            GlslTargetClient::Vulkan1_3 => glslang::VulkanVersion::Vulkan1_3,
337                            _ => unreachable!(),
338                        };
339                        glslang::Target::Vulkan {
340                            version: client_version,
341                            spirv_version: lang_version,
342                        }
343                    }
344                },
345                messages: glslang::ShaderMessage::CASCADING_ERRORS
346                    | glslang::ShaderMessage::DEBUG_INFO
347                    | glslang::ShaderMessage::DISPLAY_ERROR_COLUMN
348                    | if self.hlsl && params.compilation.hlsl.enable16bit_types {
349                        glslang::ShaderMessage::HLSL_ENABLE_16BIT_TYPES
350                    } else {
351                        glslang::ShaderMessage::DEFAULT
352                    },
353                ..Default::default()
354            },
355            Some(&defines),
356            Some(&mut include_handler),
357        )
358        .map_err(|e| self.from_glslang_error(e, file_path, &params, offset_first_line))
359        {
360            Ok(value) => value,
361            Err(error) => match error {
362                Err(error) => return Err(error),
363                Ok(diag) => return Ok(diag),
364            },
365        };
366        let _shader = match glslang::Shader::new(&self.compiler, input)
367            .map_err(|e| self.from_glslang_error(e, file_path, &params, offset_first_line))
368        {
369            Ok(value) => value,
370            Err(error) => match error {
371                Err(error) => return Err(error),
372                Ok(diag) => return Ok(diag),
373            },
374        };
375        // Linking require main entry point.
376        // For now, glslang is expecting main entry point, no way to change this via C api.
377        /*if params.entry_point.is_some() {
378            let _spirv = match shader
379                .compile()
380                .map_err(|e| self.from_glslang_error(e, file_path, &params, offset_first_line))
381            {
382                Ok(value) => value,
383                Err(error) => match error {
384                    Err(error) => return Err(error),
385                    Ok(diag) => return Ok(diag),
386                },
387            };
388        }*/
389
390        Ok(ShaderDiagnosticList::empty()) // No error detected.
391    }
392    fn support(&self, shader_stage: ShaderStage) -> bool {
393        if self.hlsl {
394            match shader_stage {
395                ShaderStage::Vertex
396                | ShaderStage::Fragment
397                | ShaderStage::Compute
398                | ShaderStage::Geometry
399                | ShaderStage::TesselationControl
400                | ShaderStage::TesselationEvaluation => true,
401                _ => false,
402            }
403        } else {
404            true // All stages supported.
405        }
406    }
407}