webgl-lint

WebGL Lint

WebGL lint is a script you can throw into your WebGL project to check for common WebGL errors.

Example

Open the JavaScript console. You’ll see the first example prints fewer errors and less info where as the second prints much more info.

Usage

<script src="https://greggman.github.io/webgl-lint/webgl-lint.js" crossorigin></script>

or

import 'https://greggman.github.io/webgl-lint/webgl-lint.js';

WebGL Lint throws a JavaScript exception when there is an issue so if you are using try/catch to catch errors you might need to print the exceptions inside your catch block. You can also turn on “pause on exception” on your JavaScript debugger.

Throwing seemed more a appropriate than just printing an error because if you get an error you should fix it! I tried the script out with all the three.js examples. It found 1 real bug and several half bugs. By half bugs I mean there were several examples that functioned but were actually passing NaN or null in the wrong places for a few frames or they were not setting uniforms that probably should have been set. Arguably it’s better to fix those so that you can continue to use the helper to find real errors. In any case most of examples ran without error so you can do it too! 😉

GMAN_debug_helper extension

WebGL Lint adds a special extension GMAN_debug_helper with these functions

Configuration

You don’t need to configure anything to use in general but there are some settings for special needs.

There 2 ways to configure

  1. Via the extension and JavaScript.

    Example:

    const gl = someCanvas.getContext('webgl');
    const ext = gl.getExtension('GMAN_debug_helper');
    if (ext) {
      ext.setConfiguration({
        maxDrawCalls: 2000,
        failUnsetSamplerUniforms: true,
      });
    }
    
  2. Via an HTML dataset attribute

    Example:

    <script
      src="https://greggman.github.io/webgl-lint/webgl-lint.js"
      data-gman-debug-helper='
        {
          "maxDrawCalls": 2000, 
          "failUnsetSamplerUniforms": true
        }
      '>
    </script>
    

    Note: (1) the setting string must be valid JSON. (2) any tag will do, <div>, <span>, etc. as the script just applies all tags it finds with querySelectorAll('[data-gman-debug-helper]') and applies the options in the order found.

Naming your WebGL objects (buffers, textures, programs, etc..)

Using the extension you can name your objects. This way when an error is printed the names will be inserted where appropriate.

const ext = gl.getExtension('GMAN_debug_helper');
const tex = gl.createTexture();
ext.tagObject(tex, 'background-tex');

Now if you get an error related to tex you might get an told it’s related to ‘background-tex’ instead of just that you got an error.

4 suggestions for using naming

  1. make some helpers

    const ext = gl.getExtension('GMAN_debug_helper');
    const tagObject = ext ? ext.tagObject.bind(ext) : () => ();
    

    now you can just unconditionally tag things and if the extension does not exist it will just be a no-op.

    const tex = gl.createTexture();
    tagObject(tex, 'checkerboard');
    
  2. wrap the creations functions

    const ext = gl.getExtension('GMAN_debug_helper');
    if (ext) {
      Object.keys(gl.__proto__)
        .filter(name => name.startsWith('create'))
        .forEach(name => {
          const origFn = gl[name];
          if (origFn) {
            gl[name] = function(...args) {
              const obj = origFn.call(this, ...args);
              if (obj) {
                ext.tagObject(obj, args[args.length - 1] || '*unknown*');
              }
              return obj;
            }
          }
        });
    }
    

    Which you use like this

    const shader = gl.createShader(gl.VERTEX_SHADER, 'phongVertexShader');
    const tex = gl.createTexture('tree-texture');
    

    and they’ll still work in normal WebGL as it will ignore the extra parameter.

  3. Same as above but not wrapped

     const ext = gl.getExtension('GMAN_debug_helper');
     const api = Object.fromEntries(
         Object.keys(gl.__proto__)
           .filter(name => name.startsWith('create'))
           .map(name => {
             const func = (ext && gl[name])
               ? function(...args) {
                   const obj = gl[name](...args);
                   if (obj) {
                     ext.tagObject(obj, args[args.length - 1] || '*unknown*');
                   }
                   return obj;
                 }
               : function(...args) {
                   return gl[name](...args);
                 };
             return [name, func];
           }));
    

    Which you use like this

     const shader = api.createShader(gl.VERTEX_SHADER, 'phongVertexShader');
     const tex = api.createTexture('tree-texture');
    

    If you’re allergic to hacking native APIs this is better but you have to remember to use api.createXXX instead of gl.createXXX

  4. Use your own API.

    Lots of people have wrapped WebGL themselves with things like class Texture and class Framebuffer or other functions. Those would be a good place to integrate tagging.

As a simple example, naming buffers after the attributes they’ll be used with (eg. ‘position’, ‘normal’), naming textures by the URL of the img where they get their data. Naming vertex array objects by the model (‘tree’, ‘car’, ‘house’), naming framebuffers by their usage (‘shadow-depth’, ‘post-processing’), naming programs by what they do (‘phong-shading’, ‘sky-box’)…

You can also untag an object with

ext.untagObject(someObj);

Arguably for debugging you probably don’t want to untag. The problem with untagging is if you untag and then have a bug where you reuse an untagged object, for example using an object you deleted, there will be no tag for the object, so your error will just say something like “error: tried to use Texture(unknown)” instead of “error: tried to use Texture(yourTag)”. At the same time, if you’re running WebGL-lint indefinitely (see maxDrawCalls) and creating and deleting lots of objects, for example sync objects, there is a tiny bit of memory involved keeping the label of each one so manually untagging should mean you are not leaking memory. Honestly I wouldn’t worry about untagging but it’s here just in case you’ve got special needs. Also see makeDefaultTags above.

The extension also includes getTagForObject if you want to look up what string you tagged an object with

const buf = gl.createBuffer();
ext.tagObject(buf, 'normals');
console.log(ext.getTagForObject(buf));  // prints 'normals'

Checking for redundant calls

An example of a redundant call is calling gl.useProgram with the same program or calling gl.vertexAttribPointer with the same parameters and the same buffer. You can get a count to date of the redundant state setting WebGL-Lint has detected by calling ext.getAndResetRedundantCallInfo()

Example:

function render() {
  // .. do stuff with webgl ..

  const info = ext.getAndResetRedundantCallInfo();
  console.log(JSON.stringify(info));
  requestAnimationFrame(render);
}

Alternatively you can try adding this script to your page (instead of webgl-lint.js) and it will attempt to print redundant call info for you.

<script type="module" src="https://greggman.github.io/webgl-lint/webgl-lint-check-redundant-state-setting.js"></script>

or

import 'https://greggman.github.io/webgl-lint/webgl-lint-check-redundant-state-setting.js';

Note: WebGL-Lint does not check every possible redundant set setting. At the moment it checks

It’s not important to avoid 100% of redundant state setting. Rather, you can use this feature to see if you have too much redundant state setting. For example if you’re making 2000 draw calls and you see 500-5000 redundant state setting counts then you should probably look into adding some state tracking in your own code or organizing the way calls happen so state is not set. It’s not so much that setting state more than once is bad, it’s rather than it’s just proof your code is doing extra work. If it’s a lot of extra work then you probably want to look into it. If it’s only a small amount of extra work then don’t worry about it.

Suggestions?

https://github.com/greggman/webgl-lint/issues

Development

You can run the tests with the un-merged code with http://localhost:8080/test/?src=true. You can also filter the tests with grep= as in http://localhost:8080/test/?grep=shader or both http://localhost:8080/test/?src=true&grep=shader.

Repo

https://github.com/greggman/webgl-lint/

License

MIT