Skip to main content

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, Value};
22use cubecl_opt::Function;
23use hashbrown::HashMap;
24use rspirv::spirv::{DebugInfoFlags, FunctionControl, Word};
25use rspirv::sr::{
26    nonsemantic_debugprintf::DebugPrintfBuilder, nonsemantic_shader_debuginfo_100::DebugInfoBuilder,
27};
28
29use crate::{SpirvCompiler, SpirvTarget, lookups::FuncDefinition};
30
31pub const SIGNATURE: &str = concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION"));
32
33#[derive(Clone, Copy, Debug, Default)]
34pub struct FunctionDefinition {
35    id: Word,
36    source: SourceFile,
37    line: u32,
38    col: u32,
39}
40
41#[derive(Clone, Debug, Default)]
42pub struct FunctionCall {
43    definition: FunctionDefinition,
44    inlined_at: Option<Word>,
45}
46
47#[derive(Clone, Debug)]
48pub struct DebugInfo {
49    function_ty: Word,
50
51    stack: Vec<FunctionCall>,
52    definitions: Definitions,
53    previous_loc: Option<SourceLoc>,
54}
55
56#[derive(Clone, Copy, Debug, Default)]
57struct SourceFile {
58    /// Id of the `DebugSource` instruction
59    id: Word,
60    /// Id of the compilation unit for this file
61    compilation_unit: Word,
62}
63
64#[derive(Clone, Debug, Default)]
65struct Definitions {
66    /// source files
67    source_files: HashMap<String, SourceFile>,
68    /// map of call names to definitions
69    functions: HashMap<CubeFnSource, FunctionDefinition>,
70}
71
72impl<T: SpirvTarget> SpirvCompiler<T> {
73    pub fn init_debug(&mut self) {
74        if self.debug_enabled() {
75            let return_ty = self.type_void();
76            let function_ty = self.debug_type_function(DebugInfoFlags::NONE, return_ty, []);
77            let entry_loc = self
78                .opt
79                .global_state
80                .root_scope
81                .debug
82                .entry_loc
83                .borrow()
84                .clone()
85                .unwrap();
86
87            self.debug_info = Some(DebugInfo {
88                function_ty,
89                stack: Default::default(),
90                definitions: Default::default(),
91                previous_loc: Some(entry_loc.clone()),
92            });
93
94            self.collect_sources();
95
96            let entry_def = self.definitions().functions[&entry_loc.source];
97            self.debug_entry_point(
98                entry_def.id,
99                entry_def.source.compilation_unit,
100                SIGNATURE,
101                "",
102            );
103        } else if self.debug_symbols {
104            let return_ty = self.type_void();
105            let function_ty = self.debug_type_function(DebugInfoFlags::NONE, return_ty, []);
106            self.debug_info = Some(DebugInfo {
107                function_ty,
108                stack: Default::default(),
109                definitions: Default::default(),
110                previous_loc: None,
111            });
112        }
113    }
114
115    /// Collect sources ahead of time so line numbers and source file names are correct
116    fn collect_sources(&mut self) {
117        let cube_fns = self
118            .opt
119            .global_state
120            .root_scope
121            .debug
122            .sources
123            .borrow()
124            .clone();
125        let mut sources = HashMap::new();
126        for cube_fn in cube_fns.iter() {
127            // If source is missing, don't override since it might exist from another function in the
128            // same file. If it's not empty, just override since they're identical.
129            if cube_fn.source_text.is_empty() {
130                sources
131                    .entry(cube_fn.file.clone())
132                    .or_insert(cube_fn.source_text.clone());
133            } else {
134                sources.insert(cube_fn.file.clone(), cube_fn.source_text.clone());
135            }
136        }
137
138        for (file, source_text) in sources {
139            self.debug_source_dedup(file, source_text);
140        }
141
142        for cube_fn in cube_fns.into_iter() {
143            let source = self.definitions().source_files[cube_fn.file.as_ref()];
144            let name = cube_fn.function_name.as_ref();
145            let mut function = FunctionDefinition {
146                id: 0,
147                source,
148                line: cube_fn.line,
149                col: cube_fn.column,
150            };
151            self.declare_debug_function(name, &mut function);
152            self.definitions().functions.insert(cube_fn, function);
153        }
154    }
155
156    pub fn declare_function(&mut self, func: &Function) -> FuncDefinition {
157        let return_ty = func
158            .return_value
159            .map(|it| self.compile_type(it.ty).id(self))
160            .unwrap_or_else(|| self.type_void());
161        let param_types = func
162            .all_params()
163            .map(|it| self.compile_function_param_type(it))
164            .collect::<Vec<_>>();
165        let func_ty = self.type_function(return_ty, param_types.iter().copied());
166
167        let definition = self
168            .debug_info
169            .as_ref()
170            .and_then(|info| info.previous_loc.clone())
171            .map(|loc| self.definitions().functions[&loc.source]);
172
173        if let Some(definition) = definition {
174            let func_call = FunctionCall {
175                definition,
176                inlined_at: None,
177            };
178
179            self.stack().push(func_call);
180        }
181
182        let id = self
183            .begin_function(return_ty, None, FunctionControl::NONE, func_ty)
184            .unwrap();
185
186        for (param_ty, param) in param_types.into_iter().zip(func.all_params()) {
187            let param_id = self.function_parameter(param_ty).unwrap();
188            self.init_function_param(param, param_id);
189        }
190
191        FuncDefinition {
192            type_id: func_ty,
193            id,
194        }
195    }
196
197    pub fn declare_main(&mut self, kernel_name: &str) -> (Word, impl Fn(&mut Self) + 'static) {
198        let void = self.type_void();
199        let voidf = self.type_function(void, vec![]);
200
201        let definition = self
202            .debug_info
203            .as_ref()
204            .and_then(|info| info.previous_loc.clone())
205            .map(|loc| self.definitions().functions[&loc.source]);
206
207        if let Some(definition) = definition {
208            let main_call = FunctionCall {
209                definition,
210                inlined_at: None,
211            };
212
213            self.stack().push(main_call);
214        }
215
216        let main = self
217            .begin_function(void, None, FunctionControl::NONE, voidf)
218            .unwrap();
219        self.debug_name(main, kernel_name);
220
221        let func_id = definition.map(|it| it.id);
222
223        let setup = move |b: &mut Self| {
224            if let Some(func_id) = func_id {
225                b.debug_start_block();
226                b.debug_function_definition(func_id, main).unwrap();
227            }
228        };
229
230        (main, setup)
231    }
232
233    pub fn set_source_loc(&mut self, loc: &Option<SourceLoc>) {
234        if let Some(loc) = loc {
235            match self.debug_info().previous_loc.clone() {
236                Some(prev) if &prev != loc => {
237                    self.debug_info().previous_loc = Some(loc.clone());
238                    if prev.source != loc.source {
239                        self.update_call(loc.clone(), prev);
240                        self.debug_start_block();
241                    } else {
242                        let source = self.stack_top().definition.source.id;
243                        self.debug_line(source, loc.line, loc.line, loc.column, loc.column)
244                            .unwrap();
245                    }
246                }
247                _ => {}
248            }
249        }
250    }
251
252    fn update_call(&mut self, loc: SourceLoc, prev: SourceLoc) {
253        let is_inlined = self.stack().len() > 1;
254        let call_fn = self.definitions().functions[&loc.source];
255        let inlined_at = is_inlined.then(|| {
256            let parent = self.stack_prev().clone();
257            self.debug_inlined_at(parent.definition.id, prev.line, parent.inlined_at)
258        });
259        self.stack_top().definition = call_fn;
260        self.stack_top().inlined_at = inlined_at;
261    }
262
263    pub fn compile_debug(&mut self, debug: core::NonSemantic) {
264        if let core::NonSemantic::Print {
265            format_string,
266            args,
267        } = &debug
268        {
269            let args = args
270                .iter()
271                .map(|arg| {
272                    let val = self.compile_value(*arg);
273                    self.read(&val)
274                })
275                .collect::<Vec<_>>();
276            DebugPrintfBuilder::debug_printf(&mut self.builder, format_string, args).unwrap();
277            return;
278        }
279        if self.debug_enabled() {
280            match debug {
281                core::NonSemantic::Print { .. } => panic!("Should already be handled"),
282                core::NonSemantic::Comment { .. } => {
283                    // Comments not supported for SPIR-V
284                }
285                core::NonSemantic::EnterDebugScope => {
286                    let new_top = self.stack_top().clone();
287                    self.stack().push(new_top);
288                }
289                core::NonSemantic::ExitDebugScope => {
290                    self.stack().pop();
291                }
292            };
293        }
294    }
295
296    fn definitions(&mut self) -> &mut Definitions {
297        &mut self.debug_info().definitions
298    }
299
300    fn stack(&mut self) -> &mut Vec<FunctionCall> {
301        &mut self.debug_info().stack
302    }
303
304    fn stack_top(&mut self) -> &mut FunctionCall {
305        self.debug_info().stack.last_mut().unwrap()
306    }
307
308    fn stack_prev(&mut self) -> &mut FunctionCall {
309        self.debug_info().stack.iter_mut().nth_back(1).unwrap()
310    }
311
312    // Deduplicated debug_source
313    fn debug_source_dedup(
314        &mut self,
315        file: impl AsRef<str>,
316        source_text: impl AsRef<str>,
317    ) -> SourceFile {
318        let existing = self.definitions().source_files.get(file.as_ref());
319        if let Some(existing) = existing {
320            *existing
321        } else {
322            let source = self.debug_source(file.as_ref(), Some(source_text.as_ref()));
323
324            let comp_unit = {
325                let version = self.const_u32(1);
326                let dwarf_version = self.const_u32(5);
327                let language = self.const_u32(13);
328                self.shader_debug_compilation_unit_id(
329                    None,
330                    version,
331                    dwarf_version,
332                    source,
333                    language,
334                )
335            };
336
337            let source_file = SourceFile {
338                id: source,
339                compilation_unit: comp_unit,
340            };
341            self.definitions()
342                .source_files
343                .insert(file.as_ref().into(), source_file);
344            source_file
345        }
346    }
347
348    fn declare_debug_function(&mut self, name: &str, function: &mut FunctionDefinition) {
349        let debug_type = self.debug_info().function_ty;
350
351        function.id = self.debug_function(
352            name,
353            debug_type,
354            function.source.id,
355            function.line,
356            function.col,
357            function.source.compilation_unit,
358            name,
359            DebugInfoFlags::NONE,
360            function.line,
361            None,
362        );
363    }
364
365    pub fn debug_start_block(&mut self) {
366        if self.debug_enabled() {
367            let loc = self.debug_info().previous_loc.clone().unwrap();
368            let func = self.stack_top().definition;
369            let inlined = self.stack_top().inlined_at;
370
371            if let Some(inlined) = inlined {
372                self.debug_scope(func.id, Some(inlined)).unwrap()
373            } else {
374                self.debug_scope(func.id, None).unwrap()
375            };
376            self.debug_line(func.source.id, loc.line, loc.line, loc.column, loc.column)
377                .unwrap();
378        }
379    }
380
381    fn debug_enabled(&self) -> bool {
382        self.debug_symbols
383            && self
384                .opt
385                .global_state
386                .root_scope
387                .debug
388                .entry_loc
389                .borrow()
390                .is_some()
391    }
392
393    #[track_caller]
394    pub fn debug_info(&mut self) -> &mut DebugInfo {
395        self.debug_info.as_mut().unwrap()
396    }
397
398    pub fn debug_name(&mut self, val: Word, name: impl Into<String>) {
399        if self.debug_symbols {
400            self.name(val, name);
401        }
402    }
403
404    pub fn debug_val_name(&mut self, id: Word, val: Id) {
405        if self.debug_symbols {
406            self.debug_name(id, format!("%{val}"));
407        }
408    }
409
410    pub fn debug_shared(&mut self, id: Word, index: Id) {
411        if self.debug_symbols {
412            let name = format!("shared({index})");
413            self.debug_name(id, name);
414        }
415    }
416
417    pub fn name_of_val(&mut self, val: Value) -> Cow<'static, str> {
418        let val_names = self.opt.global_state.root_scope.debug.value_names.clone();
419        let debug_name = val_names.borrow().get(&val).cloned();
420        debug_name.unwrap_or_else(|| val.to_string().into())
421    }
422}