Search code examples
c++webgpuwgpu-rs

How can multiple WGSL sources be combined such that line numbers are properly reported?


I have organized my project so that some shared functionality is kept in separate .wgsl files. I can load them and dynamically compose them into a single string myself, but then the line reporting for WGSL errors is relative to the beginning of the composed string instead of to the beginning of the respective files.

How is it intended for this to be handled? I found some mention of "source maps" by searching online, but I'm using the C/C++ bindings for wgpu and I can't find any reference to this concept in the Rust docs or the structs exposed by the bindings.

How do I get correct line numbers?


Solution

  • It's handled by you checking for errors, getting the actual line numbers, and translating to your line numbers.

    To get the errors you'd write some code like this (sorry, don't know the C++ api well)

    device.pushErrorScope('validation');
    module = device.createShaderModule(....);
    device.popErrorScope().then(async (err) => {
      if (!err) return;
    
      const info = await module.getComplilationInfo();
      // now walk through the info.messages list and translate lines
      // and print them
    

    Here's a working example

    const insertSnippets = (wgsl, snippets) => {
      const includeRE = /^\s*#include\s+"(.*?)"/
      const lines = wgsl.split('\n');
      let numLines = lines.length;
      const newLines = [];
      for (let l = 0; l < numLines; ++l) {
        const line = lines[l];
        const m = includeRE.exec(line);
        if (m) {
          const name = m[1];
          const snippetLines = snippets[name].split('\n');
          newLines.push(`// #snippet: '${name}', #line: 0`, ...snippetLines, `// #snippet: '', #line: ${l + 1}`)
        } else {
          newLines.push(line);
        }
      }
      return newLines.join('\n');
    }
    
    const snippetRE = /\/\/ #snippet: '([^']*)', #line: (\d+)/
    function getSnippetAndLineNum(lines, lineNum) {
      for (let l = lineNum; l > 0; --l) {
        const m = snippetRE.exec(lines[l]);
        if (m) {
          return {snippet: m[1], lineNum: lineNum - l + parseInt(m[2] - 1)};
        }
      }
      return {snippet: '', lineNum};
    }
    
    const snippets = {
      mult2: `             // 1
        fn mult2(          // 2
            v: f32         // 3
        ) -> f32           // 4
        {                  // 5
          return v * 2.0;  // 6
        }                  // 7
      `,
      div2: `                  // 1
        fn div2(               // 2
            v: f32             // 3
        ) -> f32 {             // 4
          return v / i32(2);   // 5  < -- error here
        }                      // 6
      `,
    };
    
    
    const wgsl = `             // 1
    // Checking                // 2
    // We                      // 3
    // Can                     // 4
    // Report                  // 5
    // The                     // 6
    // snippet                 // 7
    // and                     // 8
    // line                    // 9
                               // 10
    //foo bar                  // 11
                               // 12
    #include "mult2"           // 13
    #include "div2"            // 14
                               // 15
    //foo bar                  // 16
    
    `;
    
    (async function() {
      const adapter = await navigator.gpu.requestAdapter();
      const device = await adapter.requestDevice();
      device.pushErrorScope('validation');
      const code = insertSnippets(wgsl, snippets);
      const module = device.createShaderModule({code});
      device.popErrorScope().then(async err => {
        if (err) {
          const info = await module.getCompilationInfo();
          const lines = code.split('\n');
          for (const msg of info.messages) {
            const {snippet, lineNum} = getSnippetAndLineNum(lines, msg.lineNum);
            console.error(`${snippet}:${lineNum}:${msg.linePos} error: ${msg.message}\n\n${lines[msg.lineNum - 1]}\n${''.padStart(msg.linePos - 1)}${''.padStart(msg.length, '^')}`)
          }
        }
      });
    })();
    You should see an error messages in the console. Notice it says the error is in `div2:5:14` as in the snippet called 'div2', line 5, column 14. Even though the actual WGSL passed to WebGPU 

    If you run the snippet above, you should see an error messages in the console. Notice it says the error is in div2:5:14 as in the snippet called 'div2', line 5, column 14. Even though in the actual WGSL passed to WebGPU in was at line 28

    Here's a wrapped version (JS) as well

    const insertSnippets = (wgsl, snippets) => {
      const includeRE = /^\s*#include\s+"(.*?)"/
      const lines = wgsl.split('\n');
      let numLines = lines.length;
      const newLines = [];
      for (let l = 0; l < numLines; ++l) {
        const line = lines[l];
        const m = includeRE.exec(line);
        if (m) {
          const name = m[1];
          const snippetLines = snippets[name].split('\n');
          newLines.push(`// #snippet: '${name}', #line: 0`, ...snippetLines, `// #snippet: '', #line: ${l + 1}`)
        } else {
          newLines.push(line);
        }
      }
      return newLines.join('\n');
    }
    
    const snippetRE = /\/\/ #snippet: '([^']*)', #line: (\d+)/
    function getSnippetAndLineNum(lines, lineNum) {
      for (let l = lineNum; l > 0; --l) {
        const m = snippetRE.exec(lines[l]);
        if (m) {
          return {snippet: m[1], lineNum: lineNum - l + parseInt(m[2] - 1)};
        }
      }
      return {snippet: '', lineNum};
    }
    
    const snippets = {
      mult2: `             // 1
        fn mult2(          // 2
            v: f32         // 3
        ) -> f32           // 4
        {                  // 5
          return v * 2.0;  // 6
        }                  // 7
      `,
      div2: `                  // 1
        fn div2(               // 2
            v: f32             // 3
        ) -> f32 {             // 4
          return v / i32(2);   // 5  < -- error here
        }                      // 6
      `,
    };
    
    
    const wgsl = `             // 1
    // Checking                // 2
    // We                      // 3
    // Can                     // 4
    // Report                  // 5
    // The                     // 6
    // snippet                 // 7
    // and                     // 8
    // line                    // 9
                               // 10
    //foo bar                    // 11
                               // 12
    #include "mult2"           // 13
    #include "div2"            // 14
                               // 15
    //foo bar                    // 16
    
    `;
    
    GPUDevice.prototype.createShaderModule = (function(origFn) {
      return function(desc) {
        const code = desc.code;
        this.pushErrorScope('validation')
        const module = origFn.call(this, desc); 
        this.popErrorScope().then(async err => {
          if (err) {
            const info = await module.getCompilationInfo();
            const lines = code.split('\n');
            for (const msg of info.messages) {
              const {snippet, lineNum} = getSnippetAndLineNum(lines, msg.lineNum);
              console.error(`${snippet}:${lineNum}:${msg.linePos} error: ${msg.message}\n\n${lines[msg.lineNum - 1]}\n${''.padStart(msg.linePos - 1)}${''.padStart(msg.length, '^')}`)
            }
          }
        });
        return module;
      }
    })(GPUDevice.prototype.createShaderModule);
    
    
    (async function() {
      const adapter = await navigator.gpu.requestAdapter();
      const device = await adapter.requestDevice();
      const code = insertSnippets(wgsl, snippets);
      const module = device.createShaderModule({code});
    })();

    The wrapped version just means it wraps the WebGPU API so it happens automatically. Whether that's good or bad is up to you. It means exposing your preprocessor at a low-level.