shader_sense/validator/
dxc.rs

1//! Validation for hlsl with DXC
2
3use hassle_rs::*;
4use std::{
5    collections::HashMap,
6    path::{Path, PathBuf},
7};
8
9use crate::{
10    include::IncludeHandler,
11    position::{ShaderFileRange, ShaderPosition},
12    shader::{HlslShaderModel, HlslVersion, ShaderParams, ShaderStage},
13    shader_error::{ShaderDiagnostic, ShaderDiagnosticList, ShaderDiagnosticSeverity, ShaderError},
14};
15
16use super::validator::ValidatorImpl;
17
18pub struct Dxc {
19    compiler: hassle_rs::DxcCompiler,
20    library: hassle_rs::DxcLibrary,
21
22    validator: Option<hassle_rs::DxcValidator>,
23    dxil: Option<hassle_rs::wrapper::Dxil>,
24
25    #[allow(dead_code)] // Need to keep dxc alive while dependencies created
26    dxc: hassle_rs::wrapper::Dxc,
27
28    // Cache regex for parsing.
29    diagnostic_regex: regex::Regex,
30    internal_diagnostic_regex: regex::Regex,
31}
32
33struct DxcIncludeHandler<'a> {
34    include_handler: IncludeHandler,
35    include_callback: &'a mut dyn FnMut(&Path) -> Option<String>,
36}
37
38impl<'a> DxcIncludeHandler<'a> {
39    pub fn new(
40        file: &Path,
41        includes: Vec<PathBuf>,
42        path_remapping: HashMap<PathBuf, PathBuf>,
43        include_callback: &'a mut dyn FnMut(&Path) -> Option<String>,
44    ) -> Self {
45        Self {
46            include_handler: IncludeHandler::main(file, includes, path_remapping),
47            include_callback: include_callback,
48        }
49    }
50}
51
52impl hassle_rs::wrapper::DxcIncludeHandler for DxcIncludeHandler<'_> {
53    fn load_source(&mut self, filename: String) -> Option<String> {
54        // DXC include handler kinda bad.
55        // First path are already preprocessed by dxc before calling this
56        // (adding ./ in front of relative path & convert slash to backslash)
57        // Tricky to solve virtual path. Done in include handler.
58        // Second, we dont have any knowledge about the parent includer here.
59        // And its not something they are going to solve:
60        // https://github.com/microsoft/DirectXShaderCompiler/issues/6093
61        // So includes can behave weirdly with dxc if too many subfolders.
62        let path = Path::new(filename.as_str());
63        match self
64            .include_handler
65            .search_in_includes(&path, self.include_callback)
66        {
67            Some((content, include)) => {
68                self.include_handler.push_directory_stack(&include);
69                Some(content)
70            }
71            None => None,
72        }
73    }
74}
75
76impl Dxc {
77    // This is the version bundled with DXC and which is expected.
78    // TODO: Find a way to get these values dynamically.
79    // Could preprocess them in small shader file and read them instead ?
80    pub const DXC_VERSION_MAJOR: u32 = 1;
81    pub const DXC_VERSION_MINOR: u32 = 8;
82    pub const DXC_VERSION_RELEASE: u32 = 2405;
83    pub const DXC_VERSION_COMMIT: u32 = 0;
84    pub const DXC_SPIRV_VERSION_MAJOR: u32 = 1;
85    pub const DXC_SPIRV_VERSION_MINOR: u32 = 6;
86
87    // Look for dxc library near the executable
88    pub fn find_dxc_library() -> Option<PathBuf> {
89        // Assume dxil at same folder to avoid version mismatch.
90        let dxc_compiler_lib_name = libloading::library_filename("dxcompiler");
91        // Pick the dxc dll next to executable if available.
92        // Rely on current_exe as current_dir might be changed by process.
93        // Else return dll path and hope that they are accessible in path.
94        match std::env::current_exe() {
95            Ok(executable_path) => {
96                if let Some(parent_path) = executable_path.parent() {
97                    let dll_path = parent_path.join(&dxc_compiler_lib_name);
98                    if dll_path.is_file() {
99                        dll_path.parent().map(|p| p.into())
100                    } else {
101                        None // Not found at executable path
102                    }
103                } else {
104                    None // Executable path does not have parent
105                }
106            }
107            Err(_) => None, // No executable path
108        }
109    }
110
111    pub fn new(library_path: Option<PathBuf>) -> Result<Self, hassle_rs::HassleError> {
112        let dxc_compiler_lib_name = libloading::library_filename("dxcompiler");
113        let dxil_lib_name = libloading::library_filename("dxil");
114        let dxc = hassle_rs::Dxc::new(
115            library_path
116                .clone()
117                .map(|path| path.join(dxc_compiler_lib_name)),
118        )?;
119        let library = dxc.create_library()?;
120        let compiler = dxc.create_compiler()?;
121        // For some reason, there is a sneaky LoadLibrary call to dxil.dll from dxcompiler.dll that forces it to be in global path on Linux.
122        // This might get mismatch version between dxc compiler and dxil validation.
123        let (dxil, validator) = match Dxil::new(library_path.map(|path| path.join(dxil_lib_name))) {
124            Ok(dxil) => {
125                let validator_option = match dxil.create_validator() {
126                    Ok(validator) => Some(validator),
127                    Err(_) => None,
128                };
129                (Some(dxil), validator_option)
130            }
131            Err(_) => (None, None),
132        };
133        Ok(Self {
134            dxc,
135            compiler,
136            library,
137            dxil,
138            validator,
139            diagnostic_regex: regex::Regex::new(r"(?m)^(.*?:\d+:\d+: .*:.*?)$").unwrap(),
140            internal_diagnostic_regex: regex::Regex::new(r"(?s)^(.*?):(\d+):(\d+): (.*?):(.*)")
141                .unwrap(),
142        })
143    }
144    pub fn is_dxil_validation_available(&self) -> bool {
145        self.dxil.is_some() && self.validator.is_some()
146    }
147    fn parse_dxc_errors(
148        &self,
149        errors: &String,
150        file_path: &Path,
151        params: &ShaderParams,
152    ) -> Result<ShaderDiagnosticList, ShaderError> {
153        // Check empty string.
154        if errors.len() == 0 {
155            return Ok(ShaderDiagnosticList::empty());
156        }
157        let mut shader_error_list = ShaderDiagnosticList::empty();
158        let mut starts = Vec::new();
159        for capture in self.diagnostic_regex.captures_iter(errors.as_str()) {
160            if let Some(pos) = capture.get(0) {
161                starts.push(pos.start());
162            }
163        }
164        starts.push(errors.len()); // Push the end.
165        let mut include_handler = IncludeHandler::main(
166            file_path,
167            params.context.includes.clone(),
168            params.context.path_remapping.clone(),
169        );
170        // Cache includes as its a heavy operation.
171        let mut include_cache: HashMap<String, PathBuf> = HashMap::new();
172        for start in 0..starts.len() - 1 {
173            let begin = starts[start];
174            let end = starts[start + 1];
175            let block = &errors[begin..end];
176            if let Some(capture) = self.internal_diagnostic_regex.captures(block) {
177                let relative_path = capture.get(1).map_or("", |m| m.as_str());
178                let line = capture.get(2).map_or("", |m| m.as_str());
179                let pos = capture.get(3).map_or("", |m| m.as_str());
180                let level = capture.get(4).map_or("", |m| m.as_str());
181                let msg = capture.get(5).map_or("", |m| m.as_str());
182                let file_path = include_cache
183                    .entry(relative_path.into())
184                    .or_insert_with(|| {
185                        include_handler
186                            .search_path_in_includes(Path::new(&relative_path))
187                            .unwrap_or(file_path.into())
188                    });
189                shader_error_list.push(ShaderDiagnostic {
190                    severity: match level {
191                        "error" => ShaderDiagnosticSeverity::Error,
192                        "warning" => ShaderDiagnosticSeverity::Warning,
193                        "note" => ShaderDiagnosticSeverity::Information,
194                        "hint" => ShaderDiagnosticSeverity::Hint,
195                        _ => ShaderDiagnosticSeverity::Error,
196                    },
197                    error: String::from(msg),
198                    range: ShaderFileRange::new(
199                        file_path.clone(),
200                        ShaderPosition::new(
201                            line.parse::<u32>().unwrap_or(1) - 1,
202                            pos.parse::<u32>().unwrap_or(0),
203                        ),
204                        ShaderPosition::new(
205                            line.parse::<u32>().unwrap_or(1) - 1,
206                            pos.parse::<u32>().unwrap_or(0),
207                        ),
208                    ),
209                });
210            }
211        }
212
213        if shader_error_list.is_empty() {
214            let errors_to_ignore = vec![
215                // Anoying error that seems to be coming from dxc doing a sneaky call to LoadLibrary
216                // for loading DXIL even though we loaded the DLL explicitely already from a
217                // specific path. Only on Linux though...
218                "warning: DXIL signing library (dxil.dll,libdxil.so) not found.",
219            ];
220            for error_to_ignore in errors_to_ignore {
221                if errors.starts_with(error_to_ignore) {
222                    return Ok(ShaderDiagnosticList::default());
223                }
224            }
225            Ok(ShaderDiagnosticList {
226                diagnostics: vec![ShaderDiagnostic {
227                    severity: ShaderDiagnosticSeverity::Error,
228                    error: format!("Failed to parse errors: {}", &errors),
229                    // Minimize impact of error by showing it only at beginning.
230                    range: ShaderFileRange::zero(file_path.into()),
231                }],
232            })
233        } else {
234            Ok(shader_error_list)
235        }
236    }
237    fn from_hassle_error(
238        &self,
239        error: HassleError,
240        file_path: &Path,
241        params: &ShaderParams,
242    ) -> Result<ShaderDiagnosticList, ShaderError> {
243        match error {
244            HassleError::CompileError(err) => self.parse_dxc_errors(&err, file_path, &params),
245            HassleError::ValidationError(err) => Ok(ShaderDiagnosticList::from(ShaderDiagnostic {
246                severity: ShaderDiagnosticSeverity::Error,
247                error: err.to_string(),
248                range: ShaderFileRange::new(
249                    file_path.into(),
250                    ShaderPosition::new(0, 0),
251                    ShaderPosition::new(0, 0),
252                ),
253            })),
254            HassleError::LibLoadingError(err) => Err(ShaderError::InternalErr(err.to_string())),
255            HassleError::LoadLibraryError { filename, inner } => {
256                Err(ShaderError::InternalErr(format!(
257                    "Failed to load library {}: {}",
258                    filename.display(),
259                    inner.to_string()
260                )))
261            }
262            HassleError::Win32Error(err) => Err(ShaderError::InternalErr(format!(
263                "Win32 error: HRESULT={}",
264                err
265            ))),
266            HassleError::WindowsOnly(err) => Err(ShaderError::InternalErr(format!(
267                "Windows only error: {}",
268                err
269            ))),
270        }
271    }
272}
273
274fn get_profile(shader_stage: Option<ShaderStage>) -> &'static str {
275    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-models
276    match shader_stage {
277        Some(shader_stage) => match shader_stage {
278            ShaderStage::Vertex => "vs",
279            ShaderStage::Fragment => "ps",
280            ShaderStage::Compute => "cs",
281            ShaderStage::TesselationControl => "hs",
282            ShaderStage::TesselationEvaluation => "ds",
283            ShaderStage::Geometry => "gs",
284            // Mesh shader not in spec. but seems to be it
285            ShaderStage::Mesh => "ms", // Check these
286            ShaderStage::Task => "as", // Check these
287            // All RT seems to use lib profile.
288            ShaderStage::RayGeneration
289            | ShaderStage::ClosestHit
290            | ShaderStage::AnyHit
291            | ShaderStage::Callable
292            | ShaderStage::Miss
293            | ShaderStage::Intersect => "lib",
294        },
295        // Use lib profile if no stage.
296        None => "lib",
297    }
298}
299
300impl ValidatorImpl for Dxc {
301    fn validate_shader(
302        &self,
303        shader_source: &str,
304        file_path: &Path,
305        params: &ShaderParams,
306        include_callback: &mut dyn FnMut(&Path) -> Option<String>,
307    ) -> Result<ShaderDiagnosticList, ShaderError> {
308        let file_name = self.get_file_name(file_path);
309
310        let blob = match self
311            .library
312            .create_blob_with_encoding_from_str(shader_source)
313        {
314            Ok(blob) => blob,
315            Err(err) => match self.from_hassle_error(err, file_path, &params) {
316                Ok(diagnostics) => return Ok(diagnostics),
317                Err(error) => return Err(error),
318            },
319        };
320
321        let defines_copy = params.context.defines.clone();
322        let defines: Vec<(&str, Option<&str>)> = defines_copy
323            .iter()
324            .map(|v| (&v.0 as &str, Some(&v.1 as &str)))
325            .collect();
326        let mut include_handler = DxcIncludeHandler::new(
327            file_path,
328            params.context.includes.clone(),
329            params.context.path_remapping.clone(),
330            include_callback,
331        );
332        let dxc_options = {
333            let mut options = Vec::new();
334            options.push(format!(
335                "-HV {}",
336                match params.compilation.hlsl.version {
337                    HlslVersion::V2016 => "2016",
338                    HlslVersion::V2017 => "2017",
339                    HlslVersion::V2018 => "2018",
340                    HlslVersion::V2021 => "2021",
341                }
342            ));
343
344            if params.compilation.hlsl.enable16bit_types {
345                options.push("-enable-16bit-types".into());
346            }
347            if params.compilation.hlsl.spirv {
348                options.push("-spirv".into());
349                // Default target does not support lib profile, so this is required.
350                options.push("-fspv-target-env=vulkan1.3".into());
351            }
352            options
353        };
354        let dxc_options_str: Vec<&str> = dxc_options.iter().map(|s| s.as_str()).collect();
355        let result = self.compiler.compile(
356            &blob,
357            file_name.as_str(),
358            match &params.compilation.entry_point {
359                Some(entry_point) => entry_point.as_str(),
360                None => "",
361            },
362            format!(
363                "{}_{}",
364                get_profile(params.compilation.shader_stage),
365                match params.compilation.hlsl.shader_model {
366                    HlslShaderModel::ShaderModel6 => "6_0",
367                    HlslShaderModel::ShaderModel6_1 => "6_1",
368                    HlslShaderModel::ShaderModel6_2 => "6_2",
369                    HlslShaderModel::ShaderModel6_3 => "6_3",
370                    HlslShaderModel::ShaderModel6_4 => "6_4",
371                    HlslShaderModel::ShaderModel6_5 => "6_5",
372                    HlslShaderModel::ShaderModel6_6 => "6_6",
373                    HlslShaderModel::ShaderModel6_7 => "6_7",
374                    HlslShaderModel::ShaderModel6_8 => "6_8",
375                    sm =>
376                        return Err(ShaderError::ValidationError(format!(
377                            "Shader model {:?} not supported by DXC.",
378                            sm
379                        ))),
380                }
381            )
382            .as_str(),
383            &dxc_options_str,
384            Some(&mut include_handler),
385            &defines,
386        );
387
388        match result {
389            Ok(dxc_result) => {
390                // Read error buffer as they might have warnings.
391                let error_blob = match dxc_result.get_error_buffer() {
392                    Ok(blob) => blob,
393                    Err(err) => match self.from_hassle_error(err, file_path, &params) {
394                        Ok(diagnostics) => return Ok(diagnostics),
395                        Err(error) => return Err(error),
396                    },
397                };
398                let warning_emitted = match self.library.get_blob_as_string(&error_blob.into()) {
399                    Ok(string) => string,
400                    Err(err) => match self.from_hassle_error(err, file_path, &params) {
401                        Ok(diagnostics) => return Ok(diagnostics),
402                        Err(error) => return Err(error),
403                    },
404                };
405                let warning_diagnostics = match self.from_hassle_error(
406                    HassleError::CompileError(warning_emitted),
407                    file_path,
408                    &params,
409                ) {
410                    Ok(diag) => diag,
411                    Err(error) => return Err(error),
412                };
413                // Get other diagnostics from result
414                let result_blob = match dxc_result.get_result() {
415                    Ok(blob) => blob,
416                    Err(err) => match self.from_hassle_error(err, file_path, &params) {
417                        Ok(diagnostics) => {
418                            return Ok(ShaderDiagnosticList::join(warning_diagnostics, diagnostics))
419                        }
420                        Err(error) => return Err(error),
421                    },
422                };
423                // Dxil validation not supported for spirv.
424                if !params.compilation.hlsl.spirv {
425                    // Skip validation if dxil.dll does not exist.
426                    if let (Some(_dxil), Some(validator)) = (&self.dxil, &self.validator) {
427                        let data = result_blob.to_vec();
428                        let blob_encoding =
429                            match self.library.create_blob_with_encoding(data.as_ref()) {
430                                Ok(blob) => blob,
431                                Err(err) => match self.from_hassle_error(err, file_path, &params) {
432                                    Ok(diagnostics) => {
433                                        return Ok(ShaderDiagnosticList::join(
434                                            warning_diagnostics,
435                                            diagnostics,
436                                        ))
437                                    }
438                                    Err(error) => return Err(error),
439                                },
440                            };
441                        match validator.validate(blob_encoding.into()) {
442                            Ok(_) => Ok(warning_diagnostics),
443                            Err((_dxc_res, hassle_err)) => {
444                                //let error_blob = dxc_err.0.get_error_buffer().map_err(|e| self.from_hassle_error(e))?;
445                                //let error_emitted = self.library.get_blob_as_string(&error_blob.into()).map_err(|e| self.from_hassle_error(e))?;
446                                match self.from_hassle_error(hassle_err, file_path, &params) {
447                                    Ok(diagnostics) => Ok(ShaderDiagnosticList::join(
448                                        warning_diagnostics,
449                                        diagnostics,
450                                    )),
451                                    Err(err) => Err(err),
452                                }
453                            }
454                        }
455                    } else {
456                        Ok(warning_diagnostics)
457                    }
458                } else {
459                    Ok(warning_diagnostics)
460                }
461            }
462            Err((dxc_result, _hresult)) => {
463                let error_blob = match dxc_result.get_error_buffer() {
464                    Ok(blob) => blob,
465                    Err(err) => match self.from_hassle_error(err, file_path, &params) {
466                        Ok(diagnostics) => return Ok(diagnostics),
467                        Err(error) => return Err(error),
468                    },
469                };
470                let error_emitted = match self.library.get_blob_as_string(&error_blob.into()) {
471                    Ok(string) => string,
472                    Err(err) => match self.from_hassle_error(err, file_path, &params) {
473                        Ok(diagnostics) => return Ok(diagnostics),
474                        Err(error) => return Err(error),
475                    },
476                };
477                match self.from_hassle_error(
478                    HassleError::CompileError(error_emitted),
479                    file_path,
480                    &params,
481                ) {
482                    Ok(diag) => Ok(diag),
483                    Err(error) => Err(error),
484                }
485            }
486        }
487    }
488    fn support(&self, _shader_stage: ShaderStage) -> bool {
489        true // Support all shader stage
490    }
491}