cubecl_spirv/
debug.rs

1//! Uses non-semantic extensions to add debug info to the generated SPIR-V.
2//!
3//! Adds a dummy source with the kernel name as the file name and a dummy compilation unit.
4//! Then marks the top level function, and any inlined functions marked by debug instructions with
5//! their corresponding `OpDebugFunction`s and `OpDebugInlinedAt` instructions, and marks the extent
6//! with `OpDebugScope`.
7//!
8//! Also adds dummy `OpLine` instructions to allow Nsight to see the file name. Might add real
9//! line numbers in the future if possible.
10//!
11//! To get proper debugging, every instruction must be inside an `OpDebugScope` referencing the
12//! appropriate function.
13//!
14//! # Deduplication
15//!
16//! All debug instructions are deduplicated to ensure minimal binary size when functions are called
17//! in a loop or other similar situations.
18
19use std::borrow::Cow;
20
21use cubecl_core::ir::{self as core, CubeFnSource, Id, SourceLoc, Variable};
22use hashbrown::HashMap;
23use rspirv::spirv::{DebugInfoFlags, FunctionControl, Word};
24use rspirv::sr::{
25    nonsemantic_debugprintf::DebugPrintfBuilder, nonsemantic_shader_debuginfo_100::DebugInfoBuilder,
26};
27
28use crate::{SpirvCompiler, SpirvTarget};
29
30pub const SIGNATURE: &str = concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION"));
31
32#[derive(Clone, Copy, Debug, Default)]
33pub struct FunctionDefinition {
34    id: Word,
35    source: SourceFile,
36    line: u32,
37    col: u32,
38}
39
40#[derive(Clone, Debug, Default)]
41pub struct FunctionCall {
42    definition: FunctionDefinition,
43    inlined_at: Option<Word>,
44}
45
46#[derive(Clone, Debug)]
47pub struct DebugInfo {
48    function_ty: Word,
49
50    stack: Vec<FunctionCall>,
51    definitions: Definitions,
52    previous_loc: Option<SourceLoc>,
53}
54
55#[derive(Clone, Copy, Debug, Default)]
56struct SourceFile {
57    /// Id of the `DebugSource` instruction
58    id: Word,
59    /// Id of the compilation unit for this file
60    compilation_unit: Word,
61}
62
63#[derive(Clone, Debug, Default)]
64struct Definitions {
65    /// source files
66    source_files: HashMap<String, SourceFile>,
67    /// map of call names to definitions
68    functions: HashMap<CubeFnSource, FunctionDefinition>,
69}
70
71impl<T: SpirvTarget> SpirvCompiler<T> {
72    pub fn init_debug(&mut self) {
73        if self.debug_enabled() {
74            let return_ty = self.type_void();
75            let function_ty = self.debug_type_function(DebugInfoFlags::NONE, return_ty, []);
76            let entry_loc = self.opt.root_scope.debug.entry_loc.clone().unwrap();
77
78            self.debug_info = Some(DebugInfo {
79                function_ty,
80                stack: Default::default(),
81                definitions: Default::default(),
82                previous_loc: Some(entry_loc.clone()),
83            });
84
85            self.collect_sources();
86
87            let entry_def = self.definitions().functions[&entry_loc.source];
88            self.debug_entry_point(
89                entry_def.id,
90                entry_def.source.compilation_unit,
91                SIGNATURE,
92                "",
93            );
94        } else if self.debug_symbols {
95            let return_ty = self.type_void();
96            let function_ty = self.debug_type_function(DebugInfoFlags::NONE, return_ty, []);
97            self.debug_info = Some(DebugInfo {
98                function_ty,
99                stack: Default::default(),
100                definitions: Default::default(),
101                previous_loc: None,
102            });
103        }
104    }
105
106    /// Collect sources ahead of time so line numbers and source file names are correct
107    fn collect_sources(&mut self) {
108        let cube_fns = self.opt.root_scope.debug.sources.borrow().clone();
109        let mut sources = HashMap::new();
110        for cube_fn in cube_fns.iter() {
111            // If source is missing, don't override since it might exist from another function in the
112            // same file. If it's not empty, just override since they're identical.
113            if cube_fn.source_text.is_empty() {
114                sources
115                    .entry(cube_fn.file.clone())
116                    .or_insert(cube_fn.source_text.clone());
117            } else {
118                sources.insert(cube_fn.file.clone(), cube_fn.source_text.clone());
119            }
120        }
121
122        for (file, source_text) in sources {
123            self.debug_source_dedup(file, source_text);
124        }
125
126        for cube_fn in cube_fns.into_iter() {
127            let source = self.definitions().source_files[cube_fn.file.as_ref()];
128            let name = cube_fn.function_name.as_ref();
129            let mut function = FunctionDefinition {
130                id: 0,
131                source,
132                line: cube_fn.line,
133                col: cube_fn.column,
134            };
135            self.declare_debug_function(name, &mut function);
136            self.definitions().functions.insert(cube_fn, function);
137        }
138    }
139
140    pub fn declare_main(&mut self, kernel_name: &str) -> (Word, impl Fn(&mut Self) + 'static) {
141        let void = self.type_void();
142        let voidf = self.type_function(void, vec![]);
143
144        let definition = self
145            .debug_info
146            .as_ref()
147            .and_then(|info| info.previous_loc.clone())
148            .map(|loc| self.definitions().functions[&loc.source]);
149
150        if let Some(definition) = definition {
151            let main_call = FunctionCall {
152                definition,
153                inlined_at: None,
154            };
155
156            self.stack().push(main_call);
157        }
158
159        let main = self
160            .begin_function(void, None, FunctionControl::NONE, voidf)
161            .unwrap();
162        self.debug_name(main, kernel_name);
163
164        let func_id = definition.map(|it| it.id);
165
166        let setup = move |b: &mut Self| {
167            if let Some(func_id) = func_id {
168                b.debug_start_block();
169                b.debug_function_definition(func_id, main).unwrap();
170            }
171        };
172
173        (main, setup)
174    }
175
176    pub fn set_source_loc(&mut self, loc: &Option<SourceLoc>) {
177        if let Some(loc) = loc {
178            match self.debug_info().previous_loc.clone() {
179                Some(prev) if &prev != loc => {
180                    self.debug_info().previous_loc = Some(loc.clone());
181                    if prev.source != loc.source {
182                        self.update_call(loc.clone(), prev);
183                        self.debug_start_block();
184                    } else {
185                        let source = self.stack_top().definition.source.id;
186                        self.debug_line(source, loc.line, loc.line, loc.column, loc.column)
187                            .unwrap();
188                    }
189                }
190                _ => {}
191            }
192        }
193    }
194
195    fn update_call(&mut self, loc: SourceLoc, prev: SourceLoc) {
196        let is_inlined = self.stack().len() > 1;
197        let call_fn = self.definitions().functions[&loc.source];
198        let inlined_at = is_inlined.then(|| {
199            let parent = self.stack_prev().clone();
200            self.debug_inlined_at(parent.definition.id, prev.line, parent.inlined_at)
201        });
202        self.stack_top().definition = call_fn;
203        self.stack_top().inlined_at = inlined_at;
204    }
205
206    pub fn compile_debug(&mut self, debug: core::NonSemantic) {
207        if let core::NonSemantic::Print {
208            format_string,
209            args,
210        } = &debug
211        {
212            let args = args
213                .iter()
214                .map(|arg| {
215                    let var = self.compile_variable(*arg);
216                    self.read(&var)
217                })
218                .collect::<Vec<_>>();
219            DebugPrintfBuilder::debug_printf(&mut self.builder, format_string, args).unwrap();
220            return;
221        }
222        if self.debug_enabled() {
223            match debug {
224                core::NonSemantic::Print { .. } => panic!("Should already be handled"),
225                core::NonSemantic::Comment { .. } => {
226                    // Comments not supported for SPIR-V
227                }
228                core::NonSemantic::EnterDebugScope => {
229                    let new_top = self.stack_top().clone();
230                    self.stack().push(new_top);
231                }
232                core::NonSemantic::ExitDebugScope => {
233                    self.stack().pop();
234                }
235            };
236        }
237    }
238
239    fn definitions(&mut self) -> &mut Definitions {
240        &mut self.debug_info().definitions
241    }
242
243    fn stack(&mut self) -> &mut Vec<FunctionCall> {
244        &mut self.debug_info().stack
245    }
246
247    fn stack_top(&mut self) -> &mut FunctionCall {
248        self.debug_info().stack.last_mut().unwrap()
249    }
250
251    fn stack_prev(&mut self) -> &mut FunctionCall {
252        self.debug_info().stack.iter_mut().nth_back(1).unwrap()
253    }
254
255    // Deduplicated debug_source
256    fn debug_source_dedup(
257        &mut self,
258        file: impl AsRef<str>,
259        source_text: impl AsRef<str>,
260    ) -> SourceFile {
261        let existing = self.definitions().source_files.get(file.as_ref());
262        if let Some(existing) = existing {
263            *existing
264        } else {
265            let source = self.debug_source(file.as_ref(), Some(source_text.as_ref()));
266
267            let comp_unit = {
268                let version = self.const_u32(1);
269                let dwarf_version = self.const_u32(5);
270                let language = self.const_u32(13);
271                self.shader_debug_compilation_unit_id(
272                    None,
273                    version,
274                    dwarf_version,
275                    source,
276                    language,
277                )
278            };
279
280            let source_file = SourceFile {
281                id: source,
282                compilation_unit: comp_unit,
283            };
284            self.definitions()
285                .source_files
286                .insert(file.as_ref().into(), source_file);
287            source_file
288        }
289    }
290
291    fn declare_debug_function(&mut self, name: &str, function: &mut FunctionDefinition) {
292        let debug_type = self.debug_info().function_ty;
293
294        function.id = self.debug_function(
295            name,
296            debug_type,
297            function.source.id,
298            function.line,
299            function.col,
300            function.source.compilation_unit,
301            name,
302            DebugInfoFlags::NONE,
303            function.line,
304            None,
305        );
306    }
307
308    pub fn debug_start_block(&mut self) {
309        if self.debug_enabled() {
310            let loc = self.debug_info().previous_loc.clone().unwrap();
311            let func = self.stack_top().definition;
312            let inlined = self.stack_top().inlined_at;
313
314            if let Some(inlined) = inlined {
315                self.debug_scope(func.id, Some(inlined)).unwrap()
316            } else {
317                self.debug_scope(func.id, None).unwrap()
318            };
319            self.debug_line(func.source.id, loc.line, loc.line, loc.column, loc.column)
320                .unwrap();
321        }
322    }
323
324    fn debug_enabled(&self) -> bool {
325        self.debug_symbols && self.opt.root_scope.debug.entry_loc.is_some()
326    }
327
328    #[track_caller]
329    pub fn debug_info(&mut self) -> &mut DebugInfo {
330        self.debug_info.as_mut().unwrap()
331    }
332
333    pub fn debug_name(&mut self, var: Word, name: impl Into<String>) {
334        if self.debug_symbols {
335            self.name(var, name);
336        }
337    }
338
339    pub fn debug_var_name(&mut self, id: Word, var: Variable) {
340        if self.debug_symbols {
341            let name = self.name_of_var(var);
342            self.debug_name(id, name);
343        }
344    }
345
346    pub fn debug_shared(&mut self, id: Word, index: Id) {
347        if self.debug_symbols {
348            let name = format!("shared({index})");
349            self.debug_name(id, name);
350        }
351    }
352
353    pub fn name_of_var(&mut self, var: Variable) -> Cow<'static, str> {
354        let var_names = self.opt.root_scope.debug.variable_names.clone();
355        let debug_name = var_names.borrow().get(&var).cloned();
356        debug_name.unwrap_or_else(|| var.to_string().into())
357    }
358}