├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .vscode
└── settings.json
├── LICENSE.md
├── README.md
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── augment-api.js
├── check-attributes-buffer-overflow.js
├── check-framebuffer-feedback.js
├── parse-stack.js
├── samplers.js
├── texture-manager.js
├── utils.js
├── vertex-array-manager.js
├── webgl-lint.js
└── wrap.js
├── test
├── assert.js
├── index.html
├── index.js
├── js
│ └── twgl-full.module.js
├── mocha-support.js
├── mocha.css
├── mocha.js
├── tests
│ ├── arrays-with-offsets-tests.js
│ ├── bad-data-tests.js
│ ├── buffer-overflow-tests.js
│ ├── buffer-tests.js
│ ├── data-view-tests.js
│ ├── disable-tests.js
│ ├── draw-reports-program-and-vao-tests.js
│ ├── drawing-test.js
│ ├── enum-tests.js
│ ├── extension-enum-tests.js
│ ├── framebuffer-feedback-tests.js
│ ├── ignore-uniforms-tests.js
│ ├── naming-tests.js
│ ├── program-delete-tests.js
│ ├── recommended-extensions.js
│ ├── redundant-state-tests.js
│ ├── report-vao-tests.js
│ ├── shader-fail-tests.js
│ ├── undefined-uniform-tests.js
│ ├── uniform-buffer-tests.js
│ ├── uniform-mismatch-tests.js
│ ├── uniform-type-tests.js
│ ├── uniformXXv-tests.js
│ ├── unrenderable-texture-tests.js
│ ├── unset-uniform-tests.js
│ ├── untagged-objects-tests.js
│ ├── wrong-number-of-arguments-tests.js
│ └── zero-matrix-tests.js
└── webgl.js
├── webgl-lint-check-redundant-state-setting.js
├── webgl-lint.js
└── webgl-lint.png
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/js
2 | test/mocha.js
3 | /webgl-lint.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* global module */
2 |
3 | module.exports = {
4 | env: {
5 | 'es6': true,
6 | },
7 | parserOptions: {
8 | 'sourceType': 'module',
9 | 'ecmaVersion': 9,
10 | },
11 |
12 | plugins: [
13 | 'eslint-plugin-html',
14 | 'eslint-plugin-optional-comma-spacing',
15 | // "eslint-plugin-one-variable-per-var",
16 | 'eslint-plugin-require-trailing-comma',
17 | ],
18 | extends: 'eslint:recommended',
19 | rules: {
20 | 'one-var': ['error', 'never'],
21 | 'no-eval': 2,
22 | 'no-array-constructor': 2,
23 | 'no-caller': 2,
24 | 'no-catch-shadow': 2,
25 | 'no-extend-native': 2,
26 | 'no-extra-bind': 2,
27 | 'no-implied-eval': 2,
28 | 'no-inner-declarations': 0,
29 | 'no-iterator': 2,
30 | 'no-label-var': 2,
31 | 'no-labels': 2,
32 | 'no-lone-blocks': 0,
33 | 'no-multi-str': 2,
34 | 'no-native-reassign': 2,
35 | 'no-new': 2,
36 | 'no-new-func': 2,
37 | 'no-new-object': 2,
38 | 'no-new-wrappers': 2,
39 | 'no-octal-escape': 2,
40 | 'no-process-exit': 2,
41 | 'no-proto': 2,
42 | 'no-return-assign': 2,
43 | 'no-script-url': 2,
44 | 'no-sequences': 2,
45 | 'no-shadow-restricted-names': 2,
46 | 'no-spaced-func': 2,
47 | 'no-trailing-spaces': 2,
48 | 'no-undef-init': 2,
49 | 'no-unused-expressions': 2,
50 | 'no-use-before-define': 0,
51 | 'no-var': 2,
52 | 'no-with': 2,
53 | 'prefer-const': 2,
54 | 'consistent-return': 2,
55 | 'curly': [2, 'all'],
56 | 'no-extra-parens': [2, 'functions'],
57 | 'eqeqeq': 2,
58 | 'new-cap': 2,
59 | 'new-parens': 2,
60 | 'semi-spacing': [2, {'before': false, 'after': true}],
61 | 'space-infix-ops': 2,
62 | 'space-unary-ops': [2, { 'words': true, 'nonwords': false }],
63 | 'yoda': [2, 'never'],
64 |
65 | 'brace-style': [2, '1tbs', { 'allowSingleLine': false }],
66 | 'camelcase': [0],
67 | 'comma-spacing': 0,
68 | 'comma-dangle': 0,
69 | 'comma-style': [2, 'last'],
70 | 'optional-comma-spacing/optional-comma-spacing': [2, {'after': true}],
71 | 'dot-notation': 0,
72 | 'eol-last': [0],
73 | 'global-strict': [0],
74 | 'key-spacing': [0],
75 | 'no-comma-dangle': [0],
76 | 'no-irregular-whitespace': 2,
77 | 'no-multi-spaces': [0],
78 | 'no-loop-func': 0,
79 | 'no-obj-calls': 2,
80 | 'no-redeclare': [0],
81 | 'no-shadow': [0],
82 | 'no-undef': [2],
83 | 'no-unreachable': 2,
84 | //"one-variable-per-var/one-variable-per-var": [2],
85 | 'quotes': [2, 'single'],
86 | 'require-atomic-updates': 0,
87 | 'require-trailing-comma/require-trailing-comma': [2],
88 | 'require-yield': 0,
89 | 'semi': [2, 'always'],
90 | 'strict': [2, 'global'],
91 | 'space-before-function-paren': [2, 'never'],
92 | 'keyword-spacing': [1, {'before': true, 'after': true, 'overrides': {}} ],
93 | },
94 | };
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .DS_Store
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | src
3 | rollup.config.js
4 | .*
5 | *.png
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Bufferfi",
4 | "Bufferfv",
5 | "Bufferiv",
6 | "Bufferuiv",
7 | "bvec",
8 | "camelcase",
9 | "eqeqeq",
10 | "framebuffer",
11 | "gman",
12 | "highp",
13 | "isampler",
14 | "mediump",
15 | "minmax",
16 | "Multisample",
17 | "nonwords",
18 | "phong",
19 | "proto",
20 | "Renderability",
21 | "Renderbuffers",
22 | "SNORM",
23 | "SRGB",
24 | "Unrenderability",
25 | "unrenderable",
26 | "untagging",
27 | "usampler",
28 | "uvec",
29 | "webgl"
30 | ]
31 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Gregg Tavares
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 | --------------------------------------------------------------------------------
23 |
24 | Copyright (c) 2012 The Khronos Group Inc.
25 |
26 | Permission is hereby granted, free of charge, to any person obtaining a
27 | copy of this software and/or associated documentation files (the
28 | "Materials"), to deal in the Materials without restriction, including
29 | without limitation the rights to use, copy, modify, merge, publish,
30 | distribute, sublicense, and/or sell copies of the Materials, and to
31 | permit persons to whom the Materials are furnished to do so, subject to
32 | the following conditions:
33 |
34 | The above copyright notice and this permission notice shall be included
35 | in all copies or substantial portions of the Materials.
36 |
37 | THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
38 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
39 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
40 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
41 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
42 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
43 | MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebGL Lint
2 |
3 |
4 |
5 | WebGL lint is a script you can throw into your WebGL project
6 | to check for common WebGL errors.
7 |
8 | * Calls `getError` after every function and throws if there was an error.
9 |
10 | * Checks that no arguments to any functions are `undefined`.
11 |
12 | Pass `0` or `false` or `null` where you mean `0` or `false` or `null`.
13 |
14 | * Checks that no numbers or values in arrays of numbers are `NaN`.
15 |
16 | * Checks that all non-sampler uniforms are set. (see configuration below)
17 |
18 | * Checks that uniform matrices are not all zero.
19 |
20 | * Warns if you try to access an undefined uniform.
21 |
22 | * Checks for out of range access issues and will tell you which attribute/s are out of range
23 |
24 | * Checks that shaders compile. On failure prints the shader and tries to highlight errors.
25 |
26 | * Checks that programs link. On failure prints the attached shaders.
27 |
28 | * If there is a WebGL error it tries to provide more info about why
29 |
30 | * for framebuffer feedback it will tell you which textures assigned to which uniforms and which attachments
31 |
32 | * for other errors it will try print extra info where possible.
33 |
34 | * it lets you name webgl objects so those names can be shown in the error messages.
35 |
36 | ## Example
37 |
38 | **Open the JavaScript console**. You'll see the first example prints fewer
39 | errors and less info where as the second prints much more info.
40 |
41 | * [without script](https://greggman.github.io/webgl-lint/test/?lint=false)
42 | * [with script](https://greggman.github.io/webgl-lint/test/)
43 |
44 | # Usage
45 |
46 | ```
47 |
48 | ```
49 |
50 | or
51 |
52 | ```
53 | import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
54 | ```
55 |
56 | WebGL Lint `throw`s a JavaScript exception when there is an issue so if you are
57 | using `try`/`catch` to catch errors you might need to print the exceptions
58 | inside your catch block. You can also turn on "pause on exception" on your
59 | JavaScript debugger.
60 |
61 | Throwing seemed more a appropriate than just printing an error because if you
62 | get an error you should fix it! I tried the script out with all the
63 | [three.js examples](https://threejs.org/examples). It found 1 real bug and several half bugs.
64 | By half bugs I mean there were several examples that functioned but were actually
65 | passing `NaN` or `null` in the wrong places for a few frames or they were not setting
66 | uniforms that probably should have been set. Arguably it's
67 | better to fix those so that you can continue to use the helper to find real
68 | errors. In any case most of examples ran without error so you can do it
69 | too! 😉
70 |
71 | ### `GMAN_debug_helper` extension
72 |
73 | WebGL Lint adds a special extension `GMAN_debug_helper` with these functions
74 |
75 | * `tagObject(obj: WebGLObject, name: string): void` - see naming below
76 | * `untagObject(obj: WebGLObject): void` - see naming below
77 | * `getTagForObject(obj: WebGLObject): string` - see naming below
78 | * `setConfiguration(settings): void` - see configuration below
79 | * `disable(): void` - turns off the checking
80 | * `getAndResetRedundantCallInfo(): RedundantCallInfo` - see below
81 |
82 | ### Configuration
83 |
84 | You don't need to configure anything to use in general but there are some settings
85 | for special needs.
86 |
87 | * `maxDrawCalls` (default: 1000)
88 |
89 | Turns off the checking after this many draw calls. Set to 0 to check forever.
90 |
91 | * `failUnsetUniforms`: (default: true)
92 |
93 | Checks that you set uniforms except for samplers and fails if you didn't.
94 | It's a common error to forget to set a uniform or to mis-spell the name of
95 | a uniform and therefore not set the real one. The common exception is
96 | samplers because uniforms default to 0 so not setting a sampler means use
97 | texture unit 0 so samplers are not checked.
98 |
99 | Of course maybe you're not initializing some uniforms on purpose
100 | so you can turn off this check. I'd recommend setting them so you get the
101 | benefit of this check finding errors.
102 |
103 | Note: uniform blocks are not checked directly. They are checked by WebGL itself
104 | in the sense that if you fail to provide uniform buffers for your uniform blocks
105 | you'll get an error but there is no easy way to check that you set them.
106 |
107 | * `failUnsetSamplerUniforms`: (default: false)
108 |
109 | See above why sampler uniforms are not checked by default. You can force them
110 | to be checked by this setting.
111 |
112 | * `failZeroMatrixUniforms`: (default: true)
113 |
114 | Checks that a uniform matrix is not all zeros. It's a common source of errors to
115 | forget to set a matrix to the identity and it seems uncommon to have an all
116 | zero matrix. If you have a reason a matrix needs to be all zeros you may want
117 | to turn this off.
118 |
119 | * `failUnrenderableTextures`: (default: true)
120 |
121 | Unrenderable textures are not an error in WebGL, they just don't render.
122 | WebGL itself usually print's a warning but it's usually fairly cryptic
123 | just telling you an unrenderable texture exists but not much else.
124 |
125 | Examples of unrenderable textures are non-power of 2 textures in WebGL1
126 | with filtering set to need mips and wrap not set to `CLAMP_TO_EDGE` or
127 | in both WebGL and WebGL2 would be mips of different internal formats
128 | or the wrong size.
129 |
130 | * `failUndefinedUniforms`: (default: false)
131 |
132 | WebGL by default returns `null` when you call `gl.getUniformLocation` for
133 | a uniform that does not exist. It then silently ignores calling `gl.uniformXXX`
134 | if the location is `null`. This is great when you're editing a shader in that
135 | if you remove a uniform from the shader your code that is still setting
136 | the old uniform will keep working.
137 |
138 | For example if you are debugging and you go to the bottom of your fragment
139 | shader and add `gl_FragColor = vec4(1, 0, 0, 1);` all the uniforms in your
140 | fragment shader will be optimized out. If WebGL suddenly issues errors trying
141 | to set those it would be much more frustrating to debug. Conversely though, if
142 | you have a typo, for example you want to look up the location of `'u_color'` and
143 | you type `gl.getUniformLocation(prg, 'uColor')` you'll get no error and it
144 | will likely take you a while to find your typo.
145 |
146 | So, by default webgl-lint only prints a warning for undefined uniforms.
147 | You can make throw by setting `failUndefinedUniforms` to `true`.
148 |
149 | * `failBadShadersAndPrograms`: (default: true)
150 |
151 | Most WebGL programs expect all shaders to compile and all programs
152 | to link but often programmers don't check for errors. While it's likely
153 | they'd get an error about a bad program further in their code, at that point
154 | it's likely too late to tell them it's because the program didn't compile or
155 | link. Instead the message will just be something like "no valid program in use".
156 |
157 | If you're working on a project that expects shaders to fail to compile
158 | and/or programs to link you can set this to `false`.
159 |
160 | * `warnUndefinedUniforms`: (default: true)
161 |
162 | See `failUndefinedUniforms`. Setting this to false turns off warnings
163 | about undefined uniforms.
164 |
165 | * `ignoreUniforms`: (default: [])
166 |
167 | Lets you configure certain uniforms not to be checked. This way you can turn
168 | off checking for certain uniforms if they don't obey the rules above and still
169 | keep the rules on for other uniforms. This configuration is additive. In other words
170 |
171 | ```js
172 | ext.setConfiguration({ignoreUniforms: ['foo', 'bar']});
173 | ext.setConfiguration({ignoreUniforms: ['baz']});
174 | ```
175 |
176 | Ignores uniforms called 'foo', 'bar', and 'baz'.
177 |
178 | * `throwOnError`: (default: true)
179 |
180 | The default is to throw an exception on error. This has several benefits.
181 |
182 | 1. It encourages you to fix the bug.
183 |
184 | 2. You'll get a stack trace which you can drill down to find the bug.
185 |
186 | 3. If you use "pause on exception" in your browser's dev tools you'll
187 | get a live stack trace where you can explore all the local variables
188 | and state of your program.
189 |
190 | But, there might be times when you can't avoid the error, say you're
191 | running a 3rd party library that gets errors. You should go politely
192 | ask them to fix the bug or better, fix it yourself and send them a pull request.
193 | In any case, if you just want it to print an error instead of throw then
194 | you can set `throwOnError` to false.
195 |
196 | * `makeDefaultTags`: (default: true)
197 |
198 | If true, all objects get a default tag, Example `*UNTAGGED:Buffer1`,
199 | `*UNTAGGED:Buffer2` etc. This is a minor convenience to have something
200 | to distinguish one object from another though it's highly recommended
201 | you tag your objects. (See naming).
202 |
203 | The only reason to turn this off is if you're creating and deleting
204 | lots of objects and you want to make sure tags are not leaking memory
205 | since tags are never deleted automatically. (See "naming).
206 |
207 | There 2 ways to configure
208 |
209 | 1. Via the extension and JavaScript.
210 |
211 | Example:
212 |
213 | ```js
214 | const gl = someCanvas.getContext('webgl');
215 | const ext = gl.getExtension('GMAN_debug_helper');
216 | if (ext) {
217 | ext.setConfiguration({
218 | maxDrawCalls: 2000,
219 | failUnsetSamplerUniforms: true,
220 | });
221 | }
222 | ```
223 |
224 | 2. Via an HTML dataset attribute
225 |
226 | Example:
227 |
228 | ```html
229 |
238 | ```
239 |
240 | Note: (1) the setting string must be valid JSON. (2) any tag will do, `
`, ``, etc. as the
241 | script just applies all tags it finds with `querySelectorAll('[data-gman-debug-helper]')` and applies
242 | the options in the order found.
243 |
244 | ### Naming your WebGL objects (buffers, textures, programs, etc..)
245 |
246 | Using the extension you can name your objects. This way when an error is printed
247 | the names will be inserted where appropriate.
248 |
249 | ```js
250 | const ext = gl.getExtension('GMAN_debug_helper');
251 | const tex = gl.createTexture();
252 | ext.tagObject(tex, 'background-tex');
253 | ```
254 |
255 | Now if you get an error related to `tex` you might get an told it's related to 'background-tex'
256 | instead of just that you got an error.
257 |
258 | 4 suggestions for using naming
259 |
260 | 1. make some helpers
261 |
262 | ```js
263 | const ext = gl.getExtension('GMAN_debug_helper');
264 | const tagObject = ext ? ext.tagObject.bind(ext) : () => ();
265 | ```
266 |
267 | now you can just unconditionally tag things and if the extension does
268 | not exist it will just be a no-op.
269 |
270 | ```js
271 | const tex = gl.createTexture();
272 | tagObject(tex, 'checkerboard');
273 | ```
274 |
275 | 2. wrap the creations functions
276 |
277 | ```js
278 | const ext = gl.getExtension('GMAN_debug_helper');
279 | if (ext) {
280 | Object.keys(gl.__proto__)
281 | .filter(name => name.startsWith('create'))
282 | .forEach(name => {
283 | const origFn = gl[name];
284 | if (origFn) {
285 | gl[name] = function(...args) {
286 | const obj = origFn.call(this, ...args);
287 | if (obj) {
288 | ext.tagObject(obj, args[args.length - 1] || '*unknown*');
289 | }
290 | return obj;
291 | }
292 | }
293 | });
294 | }
295 | ```
296 |
297 | Which you use like this
298 |
299 | ```js
300 | const shader = gl.createShader(gl.VERTEX_SHADER, 'phongVertexShader');
301 | const tex = gl.createTexture('tree-texture');
302 | ```
303 |
304 | and they'll still work in normal WebGL as it will ignore
305 | the extra parameter.
306 |
307 | 3. Same as above but not wrapped
308 |
309 | ```js
310 | const ext = gl.getExtension('GMAN_debug_helper');
311 | const api = Object.fromEntries(
312 | Object.keys(gl.__proto__)
313 | .filter(name => name.startsWith('create'))
314 | .map(name => {
315 | const func = (ext && gl[name])
316 | ? function(...args) {
317 | const obj = gl[name](...args);
318 | if (obj) {
319 | ext.tagObject(obj, args[args.length - 1] || '*unknown*');
320 | }
321 | return obj;
322 | }
323 | : function(...args) {
324 | return gl[name](...args);
325 | };
326 | return [name, func];
327 | }));
328 | ```
329 |
330 | Which you use like this
331 |
332 | ```js
333 | const shader = api.createShader(gl.VERTEX_SHADER, 'phongVertexShader');
334 | const tex = api.createTexture('tree-texture');
335 | ```
336 |
337 | If you're allergic to hacking native APIs this is better but you have to
338 | remember to use `api.createXXX` instead of `gl.createXXX`
339 |
340 | 4. Use your own API.
341 |
342 | Lots of people have wrapped WebGL themselves with things like `class Texture` and
343 | `class Framebuffer` or other functions. Those would be a good place to integrate
344 | tagging.
345 |
346 | As a simple example, naming buffers after the attributes they'll
347 | be used with (eg. 'position', 'normal'), naming textures by the URL of the img where they
348 | get their data. Naming vertex array objects by the model ('tree', 'car', 'house'), naming
349 | framebuffers by their usage ('shadow-depth', 'post-processing'), naming programs by what they do ('phong-shading', 'sky-box')...
350 |
351 | You can also untag an object with
352 |
353 | ```js
354 | ext.untagObject(someObj);
355 | ```
356 |
357 | Arguably for debugging you probably don't want to untag. The problem with
358 | untagging is if you untag and then have a bug where you reuse an untagged
359 | object, for example using an object you deleted, there will be no tag for the
360 | object, so your error will just say something like "error: tried to use
361 | Texture(unknown)" instead of "error: tried to use Texture(yourTag)". At the same
362 | time, if you're running WebGL-lint indefinitely (see `maxDrawCalls`) and
363 | creating and deleting lots of objects, for example sync objects, there is a tiny
364 | bit of memory involved keeping the label of each one so manually untagging
365 | should mean you are not leaking memory. Honestly I wouldn't worry about
366 | untagging but it's here just in case you've got special needs. Also see
367 | `makeDefaultTags` above.
368 |
369 | The extension also includes `getTagForObject` if you want to look up
370 | what string you tagged an object with
371 |
372 | ```js
373 | const buf = gl.createBuffer();
374 | ext.tagObject(buf, 'normals');
375 | console.log(ext.getTagForObject(buf)); // prints 'normals'
376 | ```
377 |
378 | ## Checking for redundant calls
379 |
380 | An example of a redundant call is calling `gl.useProgram` with the same program
381 | or calling `gl.vertexAttribPointer` with the same parameters and the same buffer.
382 | You can get a count to date of the redundant state setting WebGL-Lint has detected
383 | by calling `ext.getAndResetRedundantCallInfo()`
384 |
385 | Example:
386 |
387 | ```js
388 | function render() {
389 | // .. do stuff with webgl ..
390 |
391 | const info = ext.getAndResetRedundantCallInfo();
392 | console.log(JSON.stringify(info));
393 | requestAnimationFrame(render);
394 | }
395 | ```
396 |
397 | Alternatively you can try adding this script to your page (**instead of `webgl-lint.js`**) and it will
398 | attempt to print redundant call info for you.
399 |
400 | ```html
401 |
402 | ```
403 |
404 | or
405 |
406 | ```js
407 | import 'https://greggman.github.io/webgl-lint/webgl-lint-check-redundant-state-setting.js';
408 | ```
409 |
410 | Note: WebGL-Lint does not check every possible redundant set setting. At the moment it checks
411 |
412 | * `bindBuffer`
413 | * `bindFramebuffer`
414 | * `bindRenderbuffer`
415 | * `bindSampler`
416 | * `bindTexture`
417 | * `bindVertexArray`
418 | * `enable`
419 | * `disable`
420 | * `vertexAttribPointer`
421 | * `vertexAttribIPointer`
422 |
423 | It's not important to avoid 100% of redundant state setting. Rather, you can use
424 | this feature to see if you have too much redundant state setting. For example if
425 | you're making 2000 draw calls and you see 500-5000 redundant state setting
426 | counts then you should probably look into adding some state tracking in your own
427 | code or organizing the way calls happen so state is not set. It's not so much
428 | that setting state more than once is bad, it's rather than it's just proof your
429 | code is doing extra work. If it's a lot of extra work then you probably want to
430 | look into it. If it's only a small amount of extra work then don't worry about
431 | it.
432 |
433 | # Suggestions?
434 |
435 | [https://github.com/greggman/webgl-lint/issues](https://github.com/greggman/webgl-lint/issues)
436 |
437 | # Development
438 |
439 | 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
440 | `http://localhost:8080/test/?grep=shader` or both
441 | `http://localhost:8080/test/?src=true&grep=shader`.
442 |
443 | # Repo
444 |
445 | [https://github.com/greggman/webgl-lint/](https://github.com/greggman/webgl-lint/)
446 |
447 | # License
448 |
449 | [MIT](LICENSE.md)
450 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webgl-lint",
3 | "version": "1.11.4",
4 | "description": "A semi strict WebGL checker",
5 | "main": "webgl-lint.js",
6 | "scripts": {
7 | "build": "rollup -c",
8 | "eslint": "eslint \"**/*.js\"",
9 | "pre-push": "npm run eslint",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/greggman/webgl-lint.git"
15 | },
16 | "keywords": [
17 | "webgl"
18 | ],
19 | "author": "Gregg Tavares",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/greggman/webgl-lint/issues"
23 | },
24 | "homepage": "https://github.com/greggman/webgl-lint#readme",
25 | "devDependencies": {
26 | "eslint": "^7.5.0",
27 | "eslint-plugin-html": "^6.0.2",
28 | "eslint-plugin-optional-comma-spacing": "0.0.4",
29 | "eslint-plugin-require-trailing-comma": "0.0.1",
30 | "rollup": "^2.23.0",
31 | "rollup-plugin-node-resolve": "^5.2.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import fs from 'fs';
3 |
4 | const pkg = JSON.parse(fs.readFileSync('package.json', {encoding: 'utf8'}));
5 | const banner = `/* webgl-lint@${pkg.version}, license MIT */`;
6 |
7 | export default [
8 | {
9 | input: 'src/webgl-lint.js',
10 | plugins: [
11 | resolve({
12 | modulesOnly: true,
13 | }),
14 | ],
15 | output: [
16 | {
17 | format: 'umd',
18 | file: 'webgl-lint.js',
19 | indent: ' ',
20 | banner,
21 | },
22 | ],
23 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/src/check-attributes-buffer-overflow.js:
--------------------------------------------------------------------------------
1 | import {
2 | getAttributeTypeInfo,
3 | getBytesPerValueForGLType,
4 | getDrawFunctionArgs,
5 | glEnumToString,
6 | glTypeToTypedArray,
7 | isWebGL2,
8 | isBuiltIn,
9 | } from './utils.js';
10 |
11 | const VERTEX_ATTRIB_ARRAY_DIVISOR = 0x88FE;
12 |
13 | function computeLastUseIndexForDrawArrays(startOffset, vertCount/*, instances, errors*/) {
14 | return startOffset + vertCount - 1;
15 | }
16 |
17 | function getLastUsedIndexForDrawElements(gl, funcName, startOffset, vertCount, instances, indexType, getWebGLObjectString, getIndicesForBuffer, errors) {
18 | const elementBuffer = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
19 | if (!elementBuffer) {
20 | errors.push('No ELEMENT_ARRAY_BUFFER bound');
21 | return undefined;
22 | }
23 | const bytesPerIndex = getBytesPerValueForGLType(indexType);
24 | const bufferSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE);
25 | const sizeNeeded = startOffset + vertCount * bytesPerIndex;
26 | if (sizeNeeded > bufferSize) {
27 | errors.push(`offset: ${startOffset} and count: ${vertCount} with index type: ${glEnumToString(indexType)} passed to ${funcName} are out of range for current ELEMENT_ARRAY_BUFFER.
28 | Those parameters require ${sizeNeeded} bytes but the current ELEMENT_ARRAY_BUFFER ${getWebGLObjectString(elementBuffer)} only has ${bufferSize} bytes`);
29 | return undefined;
30 | }
31 | const buffer = getIndicesForBuffer(elementBuffer);
32 | const Type = glTypeToTypedArray(indexType);
33 | const view = new Type(buffer, startOffset);
34 | let maxIndex = view[0];
35 | for (let i = 1; i < vertCount; ++i) {
36 | maxIndex = Math.max(maxIndex, view[i]);
37 | }
38 | return maxIndex;
39 | }
40 |
41 |
42 | export function checkAttributesForBufferOverflow(gl, funcName, args, getWebGLObjectString, getIndicesForBuffer) {
43 | const {vertCount, startOffset, indexType, instances} = getDrawFunctionArgs(funcName, args);
44 | if (vertCount <= 0 || instances <= 0) {
45 | return [];
46 | }
47 | const program = gl.getParameter(gl.CURRENT_PROGRAM);
48 | const errors = [];
49 | const nonInstancedLastIndex = indexType
50 | ? getLastUsedIndexForDrawElements(gl, funcName, startOffset, vertCount, instances, indexType, getWebGLObjectString, getIndicesForBuffer, errors)
51 | : computeLastUseIndexForDrawArrays(startOffset, vertCount, instances, errors);
52 | if (errors.length) {
53 | return errors;
54 | }
55 |
56 | const hasDivisor = isWebGL2(gl) || gl.getExtension('ANGLE_instanced_arrays');
57 |
58 | // get the attributes used by the current program
59 | const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
60 | const oldArrayBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
61 | for (let ii = 0; ii < numAttributes; ++ii) {
62 | const {name, type} = gl.getActiveAttrib(program, ii);
63 | if (isBuiltIn(name)) {
64 | continue;
65 | }
66 | const index = gl.getAttribLocation(program, name);
67 | const {count} = {count: 1, ...getAttributeTypeInfo(type)};
68 | for (let jj = 0; jj < count; ++jj) {
69 | const ndx = index + jj;
70 | const enabled = gl.getVertexAttrib(ndx, gl.VERTEX_ATTRIB_ARRAY_ENABLED);
71 | if (!enabled) {
72 | continue;
73 | }
74 | const buffer = gl.getVertexAttrib(ndx, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING);
75 | if (!buffer) {
76 | errors.push(`no buffer bound to attribute (${name}) location: ${index}`);
77 | continue;
78 | }
79 | const numComponents = gl.getVertexAttrib(ndx, gl.VERTEX_ATTRIB_ARRAY_SIZE);
80 | const type = gl.getVertexAttrib(ndx, gl.VERTEX_ATTRIB_ARRAY_TYPE);
81 | const bytesPerElement = getBytesPerValueForGLType(type) * numComponents;
82 | const offset = gl.getVertexAttribOffset(ndx, gl.VERTEX_ATTRIB_ARRAY_POINTER);
83 | const specifiedStride = gl.getVertexAttrib(ndx, gl.VERTEX_ATTRIB_ARRAY_STRIDE);
84 | const stride = specifiedStride ? specifiedStride : bytesPerElement;
85 | const divisor = hasDivisor
86 | ? gl.getVertexAttrib(ndx, VERTEX_ATTRIB_ARRAY_DIVISOR)
87 | : 0;
88 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
89 | const bufferSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE);
90 | const effectiveLastIndex = divisor > 0
91 | ? ((instances + divisor - 1) / divisor | 0) - 1
92 | : nonInstancedLastIndex;
93 | const sizeNeeded = offset + effectiveLastIndex * stride + bytesPerElement;
94 | if (sizeNeeded > bufferSize) {
95 | errors.push(`${getWebGLObjectString(buffer)} assigned to attribute ${ndx} used as attribute '${name}' in current program is too small for draw parameters.
96 | index of highest vertex accessed: ${effectiveLastIndex}
97 | attribute size: ${numComponents}, type: ${glEnumToString(type)}, stride: ${specifiedStride}, offset: ${offset}, divisor: ${divisor}
98 | needs ${sizeNeeded} bytes for draw but buffer is only ${bufferSize} bytes`);
99 | }
100 | }
101 | }
102 | gl.bindBuffer(gl.ARRAY_BUFFER, oldArrayBuffer);
103 | return errors;
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/src/check-framebuffer-feedback.js:
--------------------------------------------------------------------------------
1 | /* global WebGLTexture */
2 |
3 | import {glEnumToString, isWebGL2, isBuiltIn} from './utils.js';
4 | import {uniformTypeIsSampler, getTextureForUnit} from './samplers.js';
5 |
6 | const MAX_COLOR_ATTACHMENTS = 0x8CDF;
7 |
8 | function getMaxColorAttachments(gl) {
9 | if (!isWebGL2(gl)) {
10 | const ext = gl.getExtension('WEBGL_draw_buffers');
11 | if (!ext) {
12 | return 1;
13 | }
14 | }
15 | return gl.getParameter(MAX_COLOR_ATTACHMENTS);
16 | }
17 |
18 | /**
19 | * slow non-cached version
20 | * @param {WebGLRenderingContext} gl
21 | * @param {number} attachment
22 | * @param {Map} textureAttachments
23 | */
24 | function addTextureAttachment(gl, attachment, textureAttachments) {
25 | const type = gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, attachment, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE);
26 | if (type === gl.NONE) {
27 | return;
28 | }
29 | const obj = gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, attachment, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
30 | if (obj instanceof WebGLTexture) {
31 | if (!textureAttachments.has(obj)) {
32 | textureAttachments.set(obj, []);
33 | }
34 | textureAttachments.get(obj).push(attachment);
35 | }
36 | }
37 |
38 | /**
39 | * slow non-cached version
40 | * @param {WebGLRenderingContext} gl
41 | */
42 | export function checkFramebufferFeedback(gl, getWebGLObjectString) {
43 | const framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
44 | if (!framebuffer) {
45 | // drawing to canvas
46 | return [];
47 | }
48 |
49 | // get framebuffer texture attachments
50 | const maxColorAttachments = getMaxColorAttachments(gl);
51 | const textureAttachments = new Map();
52 | for (let i = 0; i < maxColorAttachments; ++i) {
53 | addTextureAttachment(gl, gl.COLOR_ATTACHMENT0 + i, textureAttachments);
54 | }
55 | addTextureAttachment(gl, gl.DEPTH_ATTACHMENT, textureAttachments);
56 | addTextureAttachment(gl, gl.STENCIL_ATTACHMENT, textureAttachments);
57 |
58 | if (!isWebGL2(gl)) {
59 | addTextureAttachment(gl, gl.DEPTH_STENCIL_ATTACHMENT, textureAttachments);
60 | }
61 |
62 | const oldActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
63 | const program = gl.getParameter(gl.CURRENT_PROGRAM);
64 | // get the texture units used by the current program
65 | const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
66 | const errors = [];
67 | for (let ii = 0; ii < numUniforms; ++ii) {
68 | const {name, type, size} = gl.getActiveUniform(program, ii);
69 | if (isBuiltIn(name) || !uniformTypeIsSampler(type)) {
70 | continue;
71 | }
72 |
73 | if (size > 1) {
74 | const baseName = (name.substr(-3) === '[0]')
75 | ? name.substr(0, name.length - 3)
76 | : name;
77 | for (let t = 0; t < size; ++t) {
78 | errors.push(...checkTextureUsage(gl, framebuffer, textureAttachments, program, `${baseName}[${t}]`, type, getWebGLObjectString));
79 | }
80 | } else {
81 | errors.push(...checkTextureUsage(gl, framebuffer, textureAttachments, program, name, type, getWebGLObjectString));
82 | }
83 | }
84 | gl.activeTexture(oldActiveTexture);
85 |
86 | return errors;
87 | }
88 |
89 | function checkTextureUsage(gl, framebuffer, textureAttachments, program, uniformName, uniformType, getWebGLObjectString) {
90 | const location = gl.getUniformLocation(program, uniformName);
91 | const textureUnit = gl.getUniform(program, location);
92 | const texture = getTextureForUnit(gl, textureUnit, uniformType);
93 | const attachments = textureAttachments.get(texture);
94 | return attachments
95 | ? [`${getWebGLObjectString(texture)} on uniform: ${uniformName} bound to texture unit ${textureUnit} is also attached to ${getWebGLObjectString(framebuffer)} on attachment: ${attachments.map(a => glEnumToString(a)).join(', ')}`]
96 | : [];
97 | }
98 |
--------------------------------------------------------------------------------
/src/parse-stack.js:
--------------------------------------------------------------------------------
1 | /* global navigator */
2 |
3 | // adapted from http://stackoverflow.com/a/2401861/128511
4 | function getBrowser() {
5 | const userAgent = navigator.userAgent;
6 | let m = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
7 | if (/trident/i.test(m[1])) {
8 | m = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
9 | return {
10 | name: 'IE',
11 | version: m[1],
12 | };
13 | }
14 | if (m[1] === 'Chrome') {
15 | const temp = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
16 | if (temp) {
17 | return {
18 | name: temp[1].replace('OPR', 'Opera'),
19 | version: temp[2],
20 | };
21 | }
22 | }
23 | m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?'];
24 | const version = userAgent.match(/version\/(\d+)/i);
25 | if (version) {
26 | m.splice(1, 1, version[1]);
27 | }
28 | return {
29 | name: m[0],
30 | version: m[1],
31 | };
32 | }
33 |
34 | /**
35 | * @typedef {Object} StackInfo
36 | * @property {string} url Url of line
37 | * @property {number} lineNo line number of error
38 | * @property {number} colNo column number of error
39 | * @property {string} [funcName] name of function
40 | */
41 |
42 | /**
43 | * @parameter {string} stack A stack string as in `(new Error()).stack`
44 | * @returns {StackInfo}
45 | */
46 | export const parseStack = function() {
47 | const browser = getBrowser();
48 | let lineNdx;
49 | let matcher;
50 | if ((/chrome|opera/i).test(browser.name)) {
51 | lineNdx = 3;
52 | matcher = function(line) {
53 | const m = /at ([^(]+)*\(*(.*?):(\d+):(\d+)/.exec(line);
54 | if (m) {
55 | let userFnName = m[1];
56 | let url = m[2];
57 | const lineNo = parseInt(m[3]);
58 | const colNo = parseInt(m[4]);
59 | if (url === '') {
60 | url = userFnName;
61 | userFnName = '';
62 | }
63 | return {
64 | url: url,
65 | lineNo: lineNo,
66 | colNo: colNo,
67 | funcName: userFnName,
68 | };
69 | }
70 | return undefined;
71 | };
72 | } else if ((/firefox|safari/i).test(browser.name)) {
73 | lineNdx = 2;
74 | matcher = function(line) {
75 | const m = /@(.*?):(\d+):(\d+)/.exec(line);
76 | if (m) {
77 | const url = m[1];
78 | const lineNo = parseInt(m[2]);
79 | const colNo = parseInt(m[3]);
80 | return {
81 | url: url,
82 | lineNo: lineNo,
83 | colNo: colNo,
84 | };
85 | }
86 | return undefined;
87 | };
88 | }
89 |
90 | return function stackParser(stack) {
91 | if (matcher) {
92 | try {
93 | const lines = stack.split('\n');
94 | // window.fooLines = lines;
95 | // lines.forEach(function(line, ndx) {
96 | // origConsole.log("#", ndx, line);
97 | // });
98 | return matcher(lines[lineNdx]);
99 | } catch (e) {
100 | // do nothing
101 | }
102 | }
103 | return undefined;
104 | };
105 | }();
106 |
107 |
--------------------------------------------------------------------------------
/src/samplers.js:
--------------------------------------------------------------------------------
1 | const SAMPLER_2D = 0x8B5E;
2 | const SAMPLER_CUBE = 0x8B60;
3 | const SAMPLER_3D = 0x8B5F;
4 | const SAMPLER_2D_SHADOW = 0x8B62;
5 | const SAMPLER_2D_ARRAY = 0x8DC1;
6 | const SAMPLER_2D_ARRAY_SHADOW = 0x8DC4;
7 | const SAMPLER_CUBE_SHADOW = 0x8DC5;
8 | const INT_SAMPLER_2D = 0x8DCA;
9 | const INT_SAMPLER_3D = 0x8DCB;
10 | const INT_SAMPLER_CUBE = 0x8DCC;
11 | const INT_SAMPLER_2D_ARRAY = 0x8DCF;
12 | const UNSIGNED_INT_SAMPLER_2D = 0x8DD2;
13 | const UNSIGNED_INT_SAMPLER_3D = 0x8DD3;
14 | const UNSIGNED_INT_SAMPLER_CUBE = 0x8DD4;
15 | const UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8DD7;
16 |
17 | const samplers = new Set([
18 | SAMPLER_2D,
19 | SAMPLER_CUBE,
20 | SAMPLER_3D,
21 | SAMPLER_2D_SHADOW,
22 | SAMPLER_2D_ARRAY,
23 | SAMPLER_2D_ARRAY_SHADOW,
24 | SAMPLER_CUBE_SHADOW,
25 | ]);
26 |
27 | export function isSampler(type) {
28 | return samplers.has(type);
29 | }
30 |
31 | const samplerTypes = new Map([
32 | [SAMPLER_2D, {uniformType: 'sampler2D', numberType: 'float/normalized', bindPoint: '2D'}],
33 | [SAMPLER_CUBE, {uniformType: 'samplerCube', numberType: 'float/normalized', bindPoint: 'CUBE'}],
34 | [SAMPLER_3D, {uniformType: 'sampler3D', numberType: 'float/normalized', bindPoint: '3D'}],
35 | [SAMPLER_2D_SHADOW, {uniformType: 'sampler2D', numberType: 'float/normalized', bindPoint: '2D'}],
36 | [SAMPLER_2D_ARRAY, {uniformType: 'sampler2DArray', numberType: 'float/normalized', bindPoint: '2D_ARRAY'}],
37 | [SAMPLER_2D_ARRAY_SHADOW, {uniformType: 'sampler2DArray', numberType: 'float/normalized', bindPoint: '2D_ARRAY'}],
38 | [SAMPLER_CUBE_SHADOW, {uniformType: 'samplerCube', numberType: 'float/normalized', bindPoint: 'CUBE'}],
39 | [INT_SAMPLER_2D, {uniformType: 'isampler2D', numberType: 'int', bindPoint: '2D'}],
40 | [INT_SAMPLER_3D, {uniformType: 'isampler3D', numberType: 'int', bindPoint: '3D'}],
41 | [INT_SAMPLER_CUBE, {uniformType: 'isamplerCube', numberType: 'int', bindPoint: 'CUBE'}],
42 | [INT_SAMPLER_2D_ARRAY, {uniformType: 'isampler2DArray', numberType: 'int', bindPoint: '2D_ARRAY'}],
43 | [UNSIGNED_INT_SAMPLER_2D, {uniformType: 'usampler2D', numberType: 'unsigned int', bindPoint: '2D'}],
44 | [UNSIGNED_INT_SAMPLER_3D, {uniformType: 'usampler3D', numberType: 'unsigned int', bindPoint: '3D'}],
45 | [UNSIGNED_INT_SAMPLER_CUBE, {uniformType: 'usamplerCube', numberType: 'unsigned int', bindPoint: 'CUBE'}],
46 | [UNSIGNED_INT_SAMPLER_2D_ARRAY, {uniformType: 'usampler2DArray', numberType: 'unsigned int', bindPoint: '2D_ARRAY'}],
47 | ]);
48 |
49 | export function getBindPointForSampler(type) {
50 | return samplerTypes.get(type).bindPoint;
51 | }
52 |
53 | export function uniformTypeIsSampler(type) {
54 | return samplerTypes.has(type);
55 | }
56 |
57 | export function getNumberTypeForUniformSamplerType(type) {
58 | return samplerTypes.get(type).numberType;
59 | }
60 |
61 | export function getUniformTypeForUniformSamplerType(type) {
62 | return samplerTypes.get(type).uniformType;
63 | }
64 |
65 | const TEXTURE_2D = 0x0DE1;
66 | const TEXTURE_3D = 0x806F;
67 | const TEXTURE_2D_ARRAY = 0x8C1A;
68 | const TEXTURE_CUBE_MAP = 0x8513;
69 | const TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515;
70 | const TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516;
71 | const TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517;
72 | const TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;
73 | const TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;
74 | const TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;
75 |
76 | const targetToBindPointMap = new Map([
77 | [TEXTURE_2D, '2D'],
78 | [TEXTURE_3D, '3D'],
79 | [TEXTURE_CUBE_MAP, 'CUBE'],
80 | [TEXTURE_CUBE_MAP_POSITIVE_X, 'CUBE'],
81 | [TEXTURE_CUBE_MAP_NEGATIVE_X, 'CUBE'],
82 | [TEXTURE_CUBE_MAP_POSITIVE_Y, 'CUBE'],
83 | [TEXTURE_CUBE_MAP_NEGATIVE_Y, 'CUBE'],
84 | [TEXTURE_CUBE_MAP_POSITIVE_Z, 'CUBE'],
85 | [TEXTURE_CUBE_MAP_NEGATIVE_Z, 'CUBE'],
86 | [TEXTURE_2D_ARRAY, '2D_ARRAY'],
87 | ]);
88 |
89 | export function getBindPointForTarget(target) {
90 | return targetToBindPointMap.get(target);
91 | }
92 |
93 | const TEXTURE_BINDING_2D = 0x8069;
94 | const TEXTURE_BINDING_CUBE_MAP = 0x8514;
95 | const TEXTURE_BINDING_3D = 0x806A;
96 | const TEXTURE_BINDING_2D_ARRAY = 0x8C1D;
97 |
98 | const samplerTypeToBinding = new Map([
99 | [SAMPLER_2D, TEXTURE_BINDING_2D],
100 | [SAMPLER_2D_SHADOW, TEXTURE_BINDING_2D],
101 | [SAMPLER_3D, TEXTURE_BINDING_3D],
102 | [SAMPLER_2D_ARRAY, TEXTURE_BINDING_2D_ARRAY],
103 | [SAMPLER_2D_ARRAY_SHADOW, TEXTURE_BINDING_2D_ARRAY],
104 | [SAMPLER_CUBE, TEXTURE_BINDING_CUBE_MAP],
105 | [SAMPLER_CUBE_SHADOW, TEXTURE_BINDING_CUBE_MAP],
106 | ]);
107 |
108 | export function getTextureForUnit(gl, unit, type) {
109 | gl.activeTexture(gl.TEXTURE0 + unit);
110 | const binding = samplerTypeToBinding.get(type);
111 | return gl.getParameter(binding);
112 | }
113 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function isBuiltIn(name) {
2 | return name.startsWith('gl_') || name.startsWith('webgl_');
3 | }
4 |
5 | export function isWebGL2(gl) {
6 | // a proxy for if this is webgl
7 | return !!gl.texImage3D;
8 | }
9 |
10 | export function isTypedArray(v) {
11 | return v && v.buffer && v.buffer instanceof ArrayBuffer;
12 | }
13 |
14 | export function isArrayThatCanHaveBadValues(v) {
15 | return Array.isArray(v) ||
16 | v instanceof Float32Array ||
17 | v instanceof Float64Array;
18 | }
19 |
20 | export function quotedStringOrEmpty(s) {
21 | return s ? `"${s}"` : '';
22 | }
23 |
24 | /**
25 | * Types of contexts we have added to map
26 | */
27 | const mappedContextTypes = {};
28 |
29 | /**
30 | * Map of numbers to names.
31 | * @type {Object}
32 | */
33 | const glEnums = {};
34 |
35 | /**
36 | * Map of names to numbers.
37 | * @type {Object}
38 | */
39 | const enumStringToValue = {};
40 |
41 | /**
42 | * Initializes this module. Safe to call more than once.
43 | * @param {!WebGLRenderingContext} ctx A WebGL context. If
44 | * you have more than one context it doesn't matter which one
45 | * you pass in, it is only used to pull out constants.
46 | */
47 | export function addEnumsForContext(ctx, type) {
48 | if (!mappedContextTypes[type]) {
49 | mappedContextTypes[type] = true;
50 | for (const propertyName in ctx) {
51 | if (typeof ctx[propertyName] === 'number') {
52 | glEnums[ctx[propertyName]] = propertyName;
53 | enumStringToValue[propertyName] = ctx[propertyName];
54 | }
55 | }
56 | }
57 | }
58 |
59 | export function enumArrayToString(gl, enums) {
60 | const enumStrings = [];
61 | if (enums.length) {
62 | for (let i = 0; i < enums.length; ++i) {
63 | enums.push(glEnumToString(enums[i])); // eslint-disable-line
64 | }
65 | return '[' + enumStrings.join(', ') + ']';
66 | }
67 | return enumStrings.toString();
68 | }
69 |
70 | export function makeBitFieldToStringFunc(enums) {
71 | return function(gl, value) {
72 | let orResult = 0;
73 | const orEnums = [];
74 | for (let i = 0; i < enums.length; ++i) {
75 | const enumValue = enumStringToValue[enums[i]];
76 | if ((value & enumValue) !== 0) {
77 | orResult |= enumValue;
78 | orEnums.push(glEnumToString(enumValue)); // eslint-disable-line
79 | }
80 | }
81 | if (orResult === value) {
82 | return orEnums.join(' | ');
83 | } else {
84 | return glEnumToString(value); // eslint-disable-line
85 | }
86 | };
87 | }
88 |
89 | /** @type Map> */
90 | const enumToStringsMap = new Map();
91 | export function addEnumsFromAPI(api) {
92 | for (const key in api) {
93 | const value = api[key];
94 | if (typeof value === 'number') {
95 | if (!enumToStringsMap.has(value)) {
96 | enumToStringsMap.set(value, new Set());
97 | }
98 | enumToStringsMap.get(value).add(key);
99 | }
100 | }
101 | }
102 |
103 | /**
104 | * Gets an string version of an WebGL enum.
105 | *
106 | * Example:
107 | * var str = WebGLDebugUtil.glEnumToString(ctx.getError());
108 | *
109 | * @param {number} value Value to return an enum for
110 | * @return {string} The string version of the enum.
111 | */
112 | export function glEnumToString(value) {
113 | const matches = enumToStringsMap.get(value);
114 | return matches
115 | ? [...matches.keys()].map(v => `${v}`).join(' | ')
116 | : `/*UNKNOWN WebGL ENUM*/ ${typeof value === 'number' ? `0x${value.toString(16)}` : value}`;
117 | }
118 |
119 | // ---------------------------------
120 | const FLOAT = 0x1406;
121 | const FLOAT_VEC2 = 0x8B50;
122 | const FLOAT_VEC3 = 0x8B51;
123 | const FLOAT_VEC4 = 0x8B52;
124 | const INT = 0x1404;
125 | const INT_VEC2 = 0x8B53;
126 | const INT_VEC3 = 0x8B54;
127 | const INT_VEC4 = 0x8B55;
128 | const BOOL = 0x8B56;
129 | const BOOL_VEC2 = 0x8B57;
130 | const BOOL_VEC3 = 0x8B58;
131 | const BOOL_VEC4 = 0x8B59;
132 | const FLOAT_MAT2 = 0x8B5A;
133 | const FLOAT_MAT3 = 0x8B5B;
134 | const FLOAT_MAT4 = 0x8B5C;
135 | const SAMPLER_2D = 0x8B5E;
136 | const SAMPLER_CUBE = 0x8B60;
137 | const SAMPLER_3D = 0x8B5F;
138 | const SAMPLER_2D_SHADOW = 0x8B62;
139 | const FLOAT_MAT2x3 = 0x8B65;
140 | const FLOAT_MAT2x4 = 0x8B66;
141 | const FLOAT_MAT3x2 = 0x8B67;
142 | const FLOAT_MAT3x4 = 0x8B68;
143 | const FLOAT_MAT4x2 = 0x8B69;
144 | const FLOAT_MAT4x3 = 0x8B6A;
145 | const SAMPLER_2D_ARRAY = 0x8DC1;
146 | const SAMPLER_2D_ARRAY_SHADOW = 0x8DC4;
147 | const SAMPLER_CUBE_SHADOW = 0x8DC5;
148 | const UNSIGNED_INT = 0x1405;
149 | const UNSIGNED_INT_VEC2 = 0x8DC6;
150 | const UNSIGNED_INT_VEC3 = 0x8DC7;
151 | const UNSIGNED_INT_VEC4 = 0x8DC8;
152 | const INT_SAMPLER_2D = 0x8DCA;
153 | const INT_SAMPLER_3D = 0x8DCB;
154 | const INT_SAMPLER_CUBE = 0x8DCC;
155 | const INT_SAMPLER_2D_ARRAY = 0x8DCF;
156 | const UNSIGNED_INT_SAMPLER_2D = 0x8DD2;
157 | const UNSIGNED_INT_SAMPLER_3D = 0x8DD3;
158 | const UNSIGNED_INT_SAMPLER_CUBE = 0x8DD4;
159 | const UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8DD7;
160 |
161 | const uniformTypeMap = new Map([
162 | [FLOAT, { size: 1, name: 'float', }],
163 | [FLOAT_VEC2, { size: 2, name: 'vec2', }],
164 | [FLOAT_VEC3, { size: 3, name: 'vec3', }],
165 | [FLOAT_VEC4, { size: 4, name: 'vec4', }],
166 | [INT, { size: 1, name: 'int', }],
167 | [INT_VEC2, { size: 2, name: 'ivec2', }],
168 | [INT_VEC3, { size: 3, name: 'ivec3', }],
169 | [INT_VEC4, { size: 4, name: 'ivec4', }],
170 | [UNSIGNED_INT, { size: 1, name: 'uint', }],
171 | [UNSIGNED_INT_VEC2, { size: 2, name: 'uvec2', }],
172 | [UNSIGNED_INT_VEC3, { size: 3, name: 'uvec3', }],
173 | [UNSIGNED_INT_VEC4, { size: 4, name: 'uvec4', }],
174 | [BOOL, { size: 1, name: 'bool', }],
175 | [BOOL_VEC2, { size: 2, name: 'bvec2', }],
176 | [BOOL_VEC3, { size: 3, name: 'bvec3', }],
177 | [BOOL_VEC4, { size: 4, name: 'bvec4', }],
178 | [FLOAT_MAT2, { size: 4, name: 'mat2', }],
179 | [FLOAT_MAT3, { size: 9, name: 'mat3', }],
180 | [FLOAT_MAT4, { size: 16, name: 'mat4', }],
181 | [FLOAT_MAT2x3, { size: 6, name: 'mat2x3', }],
182 | [FLOAT_MAT2x4, { size: 8, name: 'mat2x4', }],
183 | [FLOAT_MAT3x2, { size: 6, name: 'mat3x2', }],
184 | [FLOAT_MAT3x4, { size: 12, name: 'mat3x4', }],
185 | [FLOAT_MAT4x2, { size: 8, name: 'mat4x2', }],
186 | [FLOAT_MAT4x3, { size: 12, name: 'mat4x3', }],
187 | [SAMPLER_2D, { size: 1, name: 'sampler2D', }],
188 | [SAMPLER_CUBE, { size: 1, name: 'samplerCube', }],
189 | [SAMPLER_3D, { size: 1, name: 'sampler3D', }],
190 | [SAMPLER_2D_SHADOW, { size: 1, name: 'sampler2DShadow', }],
191 | [SAMPLER_2D_ARRAY, { size: 1, name: 'sampler2DArray', }],
192 | [SAMPLER_2D_ARRAY_SHADOW, { size: 1, name: 'sampler2DArrayShadow', }],
193 | [SAMPLER_CUBE_SHADOW, { size: 1, name: 'samplerCubeShadow', }],
194 | [INT_SAMPLER_2D, { size: 1, name: 'isampler2D', }],
195 | [INT_SAMPLER_3D, { size: 1, name: 'isampler3D', }],
196 | [INT_SAMPLER_CUBE, { size: 1, name: 'isamplerCube', }],
197 | [INT_SAMPLER_2D_ARRAY, { size: 1, name: 'isampler2DArray', }],
198 | [UNSIGNED_INT_SAMPLER_2D, { size: 1, name: 'usampler2D', }],
199 | [UNSIGNED_INT_SAMPLER_3D, { size: 1, name: 'usampler3D', }],
200 | [UNSIGNED_INT_SAMPLER_CUBE, { size: 1, name: 'usamplerCube', }],
201 | [UNSIGNED_INT_SAMPLER_2D_ARRAY, { size: 1, name: 'usampler2DArray', }],
202 | ]);
203 |
204 | export function getUniformTypeInfo(type) {
205 | return uniformTypeMap.get(type);
206 | }
207 |
208 | // ---------------------------------
209 |
210 |
211 | const TEXTURE_BINDING_2D = 0x8069;
212 | const TEXTURE_BINDING_CUBE_MAP = 0x8514;
213 | const TEXTURE_BINDING_3D = 0x806A;
214 | const TEXTURE_BINDING_2D_ARRAY = 0x8C1D;
215 |
216 |
217 | const ARRAY_BUFFER = 0x8892;
218 | const ELEMENT_ARRAY_BUFFER = 0x8893;
219 | const ARRAY_BUFFER_BINDING = 0x8894;
220 | const ELEMENT_ARRAY_BUFFER_BINDING = 0x8895;
221 | const TEXTURE_2D = 0x0DE1;
222 | const TEXTURE_3D = 0x806F;
223 | const TEXTURE_2D_ARRAY = 0x8C1A;
224 | const TEXTURE_CUBE_MAP = 0x8513;
225 | const FRAMEBUFFER = 0x8D40;
226 | const RENDERBUFFER = 0x8D41;
227 | const FRAMEBUFFER_BINDING = 0x8CA6;
228 | const RENDERBUFFER_BINDING = 0x8CA7;
229 | const TRANSFORM_FEEDBACK_BUFFER = 0x8C8E;
230 | const TRANSFORM_FEEDBACK_BUFFER_BINDING = 0x8C8F;
231 | const DRAW_FRAMEBUFFER = 0x8CA9;
232 | const READ_FRAMEBUFFER = 0x8CA8;
233 | const READ_FRAMEBUFFER_BINDING = 0x8CAA;
234 | const UNIFORM_BUFFER = 0x8A11;
235 | const UNIFORM_BUFFER_BINDING = 0x8A28;
236 | const TRANSFORM_FEEDBACK = 0x8E22;
237 | const TRANSFORM_FEEDBACK_BINDING = 0x8E25;
238 |
239 | const bindPointMap = new Map([
240 | [ARRAY_BUFFER, ARRAY_BUFFER_BINDING],
241 | [ELEMENT_ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER_BINDING],
242 | [TEXTURE_2D, TEXTURE_BINDING_2D],
243 | [TEXTURE_CUBE_MAP, TEXTURE_BINDING_CUBE_MAP],
244 | [TEXTURE_3D, TEXTURE_BINDING_3D],
245 | [TEXTURE_2D_ARRAY, TEXTURE_BINDING_2D_ARRAY],
246 | [RENDERBUFFER, RENDERBUFFER_BINDING],
247 | [FRAMEBUFFER, FRAMEBUFFER_BINDING],
248 | [DRAW_FRAMEBUFFER, FRAMEBUFFER_BINDING],
249 | [READ_FRAMEBUFFER, READ_FRAMEBUFFER_BINDING],
250 | [UNIFORM_BUFFER, UNIFORM_BUFFER_BINDING],
251 | [TRANSFORM_FEEDBACK_BUFFER, TRANSFORM_FEEDBACK_BUFFER_BINDING],
252 | [TRANSFORM_FEEDBACK, TRANSFORM_FEEDBACK_BINDING],
253 | ]);
254 |
255 | export function getBindingQueryEnumForBindPoint(bindPoint) {
256 | return bindPointMap.get(bindPoint);
257 | }
258 |
259 | const BYTE = 0x1400;
260 | const SHORT = 0x1402;
261 | const UNSIGNED_BYTE = 0x1401;
262 | const UNSIGNED_SHORT = 0x1403;
263 |
264 | const glTypeToSizeMap = new Map([
265 | [BOOL , 1],
266 | [BYTE , 1],
267 | [UNSIGNED_BYTE , 1],
268 | [SHORT , 2],
269 | [UNSIGNED_SHORT , 2],
270 | [INT , 4],
271 | [UNSIGNED_INT , 4],
272 | [FLOAT , 4],
273 | ]);
274 |
275 | export function getBytesPerValueForGLType(type) {
276 | return glTypeToSizeMap.get(type) || 0;
277 | }
278 |
279 | const glTypeToTypedArrayMap = new Map([
280 | [UNSIGNED_BYTE, Uint8Array],
281 | [UNSIGNED_SHORT, Uint16Array],
282 | [UNSIGNED_INT, Uint32Array],
283 | ]);
284 |
285 | export function glTypeToTypedArray(type) {
286 | return glTypeToTypedArrayMap.get(type);
287 | }
288 |
289 | const drawFuncsToArgs = {
290 | drawArrays(primType, startOffset, vertCount) {
291 | return {startOffset, vertCount, instances: 1};
292 | },
293 | drawElements(primType, vertCount, indexType, startOffset) {
294 | return {startOffset, vertCount, instances: 1, indexType};
295 | },
296 | drawArraysInstanced(primType, startOffset, vertCount, instances) {
297 | return {startOffset, vertCount, instances};
298 | },
299 | drawElementsInstanced(primType, vertCount, indexType, startOffset, instances) {
300 | return {startOffset, vertCount, instances, indexType};
301 | },
302 | drawArraysInstancedANGLE(primType, startOffset, vertCount, instances) {
303 | return {startOffset, vertCount, instances};
304 | },
305 | drawElementsInstancedANGLE(primType, vertCount, indexType, startOffset, instances) {
306 | return {startOffset, vertCount, instances, indexType};
307 | },
308 | drawRangeElements(primType, start, end, vertCount, indexType, startOffset) {
309 | return {startOffset, vertCount, instances: 1, indexType};
310 | },
311 | };
312 |
313 | export function getDrawFunctionArgs(funcName, args) {
314 | return drawFuncsToArgs[funcName](...args);
315 | }
316 |
317 | export function isDrawFunction(funcName) {
318 | return !!drawFuncsToArgs[funcName];
319 | }
320 |
321 | const attrTypeMap = new Map([
322 | [FLOAT, { size: 4, }],
323 | [FLOAT_VEC2, { size: 8, }],
324 | [FLOAT_VEC3, { size: 12, }],
325 | [FLOAT_VEC4, { size: 16, }],
326 | [INT, { size: 4, }],
327 | [INT_VEC2, { size: 8, }],
328 | [INT_VEC3, { size: 12, }],
329 | [INT_VEC4, { size: 16, }],
330 | [UNSIGNED_INT, { size: 4, }],
331 | [UNSIGNED_INT_VEC2, { size: 8, }],
332 | [UNSIGNED_INT_VEC3, { size: 12, }],
333 | [UNSIGNED_INT_VEC4, { size: 16, }],
334 | [BOOL, { size: 4, }],
335 | [BOOL_VEC2, { size: 8, }],
336 | [BOOL_VEC3, { size: 12, }],
337 | [BOOL_VEC4, { size: 16, }],
338 | [FLOAT_MAT2, { size: 4, count: 2, }],
339 | [FLOAT_MAT3, { size: 9, count: 3, }],
340 | [FLOAT_MAT4, { size: 16, count: 4, }],
341 | ]);
342 |
343 | export function getAttributeTypeInfo(type) {
344 | return attrTypeMap.get(type);
345 | }
346 |
347 | export const createWeakRef = obj => obj ? new WeakRef(obj) : null;
348 | export const isObjectRefEqual = (ref, obj) => {
349 | const refed = ref?.deref();
350 | // check they both reference something or both don't reference something.
351 | if (!!refed !== !!obj) {
352 | return false;
353 | }
354 | return refed ? refed === obj : true;
355 | };
356 |
357 | export function getWithDefault(v, defaultValue) {
358 | return v === undefined ? defaultValue : v;
359 | }
--------------------------------------------------------------------------------
/src/vertex-array-manager.js:
--------------------------------------------------------------------------------
1 | import {
2 | createWeakRef,
3 | isObjectRefEqual,
4 | } from './utils.js';
5 |
6 | class VertexArray {
7 | constructor(numAttribs) {
8 | this.elementAttribArray = null;
9 | this.attribs = [];
10 | for (let i = 0; i < numAttribs; ++i) {
11 | this.attribs.push({
12 | size: 0,
13 | type: 0,
14 | normalize: false,
15 | stride: 0,
16 | offset: 0,
17 | divisor: 0,
18 | buffer: null,
19 | iPointer: false,
20 | });
21 | }
22 | }
23 | setElementArrayBuffer(redundantStateSetting, buffer) {
24 | if (isObjectRefEqual(this.elementAttribArray, buffer)) {
25 | ++redundantStateSetting.bindBuffer;
26 | } else {
27 | this.elementAttribArray = createWeakRef(buffer);
28 | }
29 | }
30 | setAttrib(redundantStateSetting, iPointer, index, size, type, normalize, stride, offset, buffer) {
31 | const attrib = this.attribs[index];
32 | if (isObjectRefEqual(attrib.buffer, buffer) &&
33 | attrib.iPointer === iPointer &&
34 | attrib.size === size &&
35 | attrib.type === type &&
36 | attrib.normalize === normalize &&
37 | attrib.stride === stride &&
38 | attrib.offset === offset) {
39 | ++redundantStateSetting.vertexAttribPointer;
40 | } else {
41 | Object.assign(attrib, {
42 | iPointer, size, type, normalize, stride, offset, buffer: createWeakRef(buffer),
43 | });
44 | }
45 | }
46 | }
47 |
48 | export class VertexArrayManager {
49 | constructor(gl, redundantStateSetting) {
50 | this.gl = gl;
51 | this.redundantStateSetting = redundantStateSetting;
52 | this.numAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
53 | this.vertexArrays = new WeakMap();
54 | this.defaultVertexArray = new VertexArray(this.numAttribs);
55 | this.currentVertexArray = this.defaultVertexArray;
56 |
57 | const createVertexArray = (ctx, funcName, args, obj) => {
58 | this.vertexArrays.set(obj, new VertexArray(this.numAttribs));
59 | };
60 |
61 | const deleteVertexArray = (ctx, funcName, args) => {
62 | const [obj] = args;
63 | const vertexArray = this.vertexArrays.get(obj);
64 | if (this.currentVertexArray === vertexArray) {
65 | this.currentVertexArray = this.defaultVertexArray;
66 | }
67 | this.vertexArrays.delete(obj);
68 | };
69 |
70 | const bindVertexArray = (ctx, funcName, args) => {
71 | let [vertexArray] = args;
72 | vertexArray = vertexArray ? this.vertexArrays.get(vertexArray) : this.defaultVertexArray;
73 | if (vertexArray === this.currentVertexArray) {
74 | ++this.redundantStateSetting.bindVertexArray;
75 | } else {
76 | this.currentVertexArray = vertexArray;
77 | }
78 | };
79 |
80 | this.postChecks = {
81 | createVertexArray,
82 | createVertexArrayOES: createVertexArray,
83 | bindVertexArray,
84 | bindVertexArrayOES: bindVertexArray,
85 | bindBuffer: (ctx, funcName, args) => {
86 | const [target, buffer] = args;
87 | if (target === gl.ELEMENT_ARRAY_BUFFER) {
88 | this.currentVertexArray.setElementArrayBuffer(this.redundantStateSetting, buffer);
89 | }
90 | },
91 | deleteVertexArray,
92 | deleteVertexArrayOES: deleteVertexArray,
93 | vertexAttribPointer: (ctx, funcName, args) => {
94 | const [index, size, type, normalize, stride, offset] = args;
95 | const gl = this.gl;
96 | this.currentVertexArray.setAttrib(this.redundantStateSetting, false, index, size, type, normalize, stride, offset, gl.getParameter(gl.ARRAY_BUFFER_BINDING));
97 | },
98 | vertexAttribIPointer: (ctx, funcName, args) => {
99 | const [index, size, type, stride, offset] = args;
100 | const gl = this.gl;
101 | this.currentVertexArray.setAttrib(this.redundantStateSetting, true, index, size, type, false, stride, offset, gl.getParameter(gl.ARRAY_BUFFER_BINDING));
102 | },
103 | };
104 | }
105 | }
--------------------------------------------------------------------------------
/src/webgl-lint.js:
--------------------------------------------------------------------------------
1 | import './wrap.js';
2 |
--------------------------------------------------------------------------------
/src/wrap.js:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2019 Gregg Tavares
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | this software and associated documentation files (the "Software"), to deal in
8 | the Software without restriction, including without limitation the rights to
9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10 | the Software, and to permit persons to whom the Software is furnished to do so,
11 | subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | */
23 |
24 | import {augmentAPI} from './augment-api.js';
25 |
26 | /* global console */
27 | /* global document */
28 | /* global HTMLCanvasElement */
29 | /* global OffscreenCanvas */
30 |
31 | console.log('webgl-lint running');
32 |
33 | function wrapGetContext(Ctor) {
34 | const oldFn = Ctor.prototype.getContext;
35 | Ctor.prototype.getContext = function(type, ...args) {
36 | const ctx = oldFn.call(this, type, ...args);
37 | // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
38 | // but that might fail if wrapped by debugging extension
39 | if (ctx && ctx.bindTexture) {
40 | const config = {
41 | maxDrawCalls: 1000,
42 | throwOnError: true,
43 | failBadShadersAndPrograms: true,
44 | failUnsetUniforms: true,
45 | failUnsetSamplerUniforms: false,
46 | failZeroMatrixUniforms: true,
47 | failUnrenderableTextures: true,
48 | failUndefinedUniforms: false,
49 | warnUndefinedUniforms: true,
50 | makeDefaultTags: true,
51 | ignoreUniforms: [],
52 | };
53 | augmentAPI(ctx, type, config);
54 | const ext = ctx.getExtension('GMAN_debug_helper');
55 | document.querySelectorAll('[data-gman-debug-helper]').forEach(elem => {
56 | const str = elem.dataset.gmanDebugHelper;
57 | let config;
58 | try {
59 | config = JSON.parse(str);
60 | } catch (e) {
61 | e.message += `\n${str}\nfailed to parse data-gman-debug-helper as JSON in: ${elem.outerHTML}`;
62 | throw e;
63 | }
64 | if (config) {
65 | ext.setConfiguration(config);
66 | }
67 | });
68 | }
69 | return ctx;
70 | };
71 | }
72 |
73 | if (typeof HTMLCanvasElement !== 'undefined') {
74 | wrapGetContext(HTMLCanvasElement);
75 | }
76 | if (typeof OffscreenCanvas !== 'undefined') {
77 | wrapGetContext(OffscreenCanvas);
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/test/assert.js:
--------------------------------------------------------------------------------
1 | /* global console */
2 |
3 | export const config = {};
4 |
5 | export function setConfig(options) {
6 | Object.assign(config, options);
7 | }
8 |
9 | function formatMsg(msg) {
10 | return `${msg}${msg ? ': ' : ''}`;
11 | }
12 |
13 | export function assertTruthy(actual, msg = '') {
14 | if (!config.noLint && !actual) {
15 | throw new Error(`${formatMsg(msg)}expected: truthy, actual: ${actual}`);
16 | }
17 | }
18 |
19 | export function assertFalsy(actual, msg = '') {
20 | if (!config.noLint && actual) {
21 | throw new Error(`${formatMsg(msg)}expected: falsy, actual: ${actual}`);
22 | }
23 | }
24 |
25 | export function assertStringMatchesRegEx(actual, regex, msg = '') {
26 | if (!config.noLint && !regex.test(actual)) {
27 | throw new Error(`${formatMsg(msg)}expected: ${regex}, actual: ${actual}`);
28 | }
29 | }
30 |
31 | export function assertEqual(actual, expected, msg = '') {
32 | if (!config.noLint && actual !== expected) {
33 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`);
34 | }
35 | }
36 |
37 | export function assertNotEqual(actual, expected, msg = '') {
38 | if (!config.noLint && actual === expected) {
39 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`);
40 | }
41 | }
42 |
43 | export function assertThrowsWith(func, expectations, msg = '') {
44 | let error = '';
45 | if (config.throwOnError === false) {
46 | const origFn = console.error;
47 | const errors = [];
48 | console.error = function(...args) {
49 | errors.push(args.join(' '));
50 | };
51 | func();
52 | console.error = origFn;
53 | if (errors.length) {
54 | error = errors.join('\n');
55 | console.error(error);
56 | }
57 | } else {
58 | try {
59 | func();
60 | } catch (e) {
61 | console.error(e); // eslint-disable-line
62 | error = e;
63 | }
64 |
65 | }
66 |
67 | if (config.noLint) {
68 | return;
69 | }
70 |
71 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg);
72 | }
73 |
74 | export function assertDoesNotThrow(func, msg = '') {
75 | let error = '';
76 | if (config.throwOnError === false) {
77 | const origFn = console.error;
78 | const errors = [];
79 | console.error = function(...args) {
80 | errors.push(args.join(' '));
81 | };
82 | func();
83 | console.error = origFn;
84 | if (errors.length) {
85 | error = errors.join('\n');
86 | console.error(error);
87 | }
88 | } else {
89 | try {
90 | func();
91 | } catch (e) {
92 | console.error(e); // eslint-disable-line
93 | error = e;
94 | }
95 |
96 | }
97 |
98 | if (config.noLint) {
99 | return;
100 | }
101 |
102 | assertEqual(error.toString(), '', msg);
103 | }
104 |
105 | // check if it throws it throws with x
106 | export function assertIfThrowsItThrowsWith(func, expectations, msg = '') {
107 | let error = '';
108 | let threw = false;
109 | if (config.throwOnError === false) {
110 | const origFn = console.error;
111 | const errors = [];
112 | console.error = function(...args) {
113 | errors.push(args.join(' '));
114 | };
115 | func();
116 | console.error = origFn;
117 | if (errors.length) {
118 | error = errors.join('\n');
119 | console.error(error);
120 | }
121 | } else {
122 | try {
123 | func();
124 | } catch (e) {
125 | console.error(e); // eslint-disable-line
126 | error = e;
127 | threw = true;
128 | }
129 |
130 | }
131 |
132 | if (config.noLint) {
133 | return;
134 | }
135 |
136 | if (!threw) {
137 | return;
138 | }
139 |
140 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg);
141 | }
142 |
143 | function assertStringMatchesREs(actual, expectations, msg) {
144 | for (const expectation of expectations) {
145 | if (expectation instanceof RegExp) {
146 | if (!expectation.test(actual)) {
147 | throw new Error(`${formatMsg(msg)}expected: ${expectation}, actual: ${actual}`);
148 | }
149 | }
150 | }
151 |
152 | }
153 | export function assertWarnsWith(func, expectations, msg = '') {
154 | const warnings = [];
155 | const origWarnFn = console.warn;
156 | console.warn = function(...args) {
157 | origWarnFn.call(this, ...args);
158 | warnings.push(args.join(' '));
159 | };
160 |
161 | let error;
162 | try {
163 | func();
164 | } catch (e) {
165 | error = e;
166 | }
167 |
168 | console.warn = origWarnFn;
169 |
170 | if (error) {
171 | throw error;
172 | }
173 |
174 | if (config.noLint) {
175 | return;
176 | }
177 |
178 | assertStringMatchesREs(warnings.join(' '), expectations, msg);
179 | }
180 |
181 | export default {
182 | false: assertFalsy,
183 | equal: assertEqual,
184 | matchesRegEx: assertStringMatchesRegEx,
185 | notEqual: assertNotEqual,
186 | throwsWith: assertThrowsWith,
187 | true: assertTruthy,
188 | };
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WebGL Lint Tests
6 |
7 |
8 |
13 |
14 |
15 |
16 |
** SEE JAVASCRIPT CONSOLE!! ** compare the error messages to this