shader_sense/validator/
glslang.rs

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