├── LICENSE ├── README.md ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Unlicense 2 | This is free and unencumbered software released into the public domain. 3 | 4 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 5 | 6 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For more information, please refer to https://unlicense.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL Spy 2 | 3 | A small utility to monitor WebGL2 drawcalls and other performance-related methods. 4 | Like a pocket-sized SpectorJS for quick peek at your pipeline. 5 | 6 | ## Usage 7 | `npm i webgl-spy` 8 | 9 | ```js 10 | import WebglSpy from 'webgl-spy' 11 | 12 | const spy = new WebglSpy(context) 13 | // three.js: new WebglSpy(renderer.getContext()) 14 | // ogl: new WebglSpy(renderer.gl) 15 | // babylon: new WebglSpy(engine._gl) 16 | 17 | function render() { 18 | requestAnimationFrame(update) 19 | 20 | spy.startCapture() 21 | renderer.render(scene, camera) 22 | const calls = spy.endCapture() 23 | } 24 | ``` 25 | 26 | `calls` is an array of `{ action, program?, type }` listing at minima the draw method name and the shader name, if available. 27 | Other info can be 28 | - clear bits (depth, stencil, color) 29 | - draw indices count / vertices count 30 | - framebufferIndice (attributed by this lib, for clarity) 31 | ```js 32 | {action: 'bindFramebuffer ID - 0', type: 'bind'} 33 | {action: 'clear: DEPTH, STENCIL, COLOR', type: 'clear'} 34 | {action: 'drawElements: TRIANGLES, 60192 indices', program: 'TreeMaterial', type: 'draw'} 35 | {action: 'bindFramebuffer', type: 'bind'} 36 | {action: 'drawArrays: TRIANGLES, 0 indices, 3 vertices', program: 'Unnamed shader', type: 'draw'} 37 | {action: 'clear: DEPTH', type: 'clear'} 38 | ``` 39 | 40 | ### API 41 | #### new WebglSpy(context) 42 | Creates a new instance to monitor the given WebGL2RenderingContext. 43 | 44 | #### startCapture() 45 | Call before any drawing operation (setRenderTarget, clear, etc). 46 | 47 | #### endCapture() 48 | Call after everything has been rendered. Returns an array of calls. Can be filtered by `type` (draw, clear, bind). 49 | 50 | #### destroy() 51 | Restore the context by removing spies. Probably not needed. 52 | 53 | ## To do 54 | This is a simple proof of concept and might not get developed further. 55 | If it happens, some ideas: 56 | - show framebuffer read/write 57 | - add state calls (i.e. enable/disable scissor, stencil, cull_face, depth_test, viewport, clearColor, blitFrameBuffer...) 58 | - add/show number of texture bounds? 59 | - Typescript for encapsulation & types 60 | - Unit tests 61 | 62 | ## Credits 63 | [BabylonJS' great SpectorJS extension](https://github.com/BabylonJS/Spector.js) for a lot of the ideas, constants & spy pattern implementation. 64 | 65 | [threejs](https://github.com/mrdoob/three.js) for the draw methods (WebGLIndexedBufferRenderer.js and WebGLBufferRenderer.js) 66 | 67 | [spite](https://gist.github.com/spite/7ae92212b4f28076ba29) for getting everyone to implement the `#define SHADER_NAME` hint 68 | 69 | [Active Theory](https://activetheory.net/)'s Hydra for the console.log idea 70 | 71 | ## Unlicense 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const SPY_OG_PREFIX = 'WEBGL_SPY_' 2 | 3 | const TYPES = { 4 | DRAW: 'draw', 5 | CLEAR: 'clear', 6 | BIND: 'bind', 7 | } 8 | 9 | // Taken from 10 | // https://github.com/mrdoob/three.js WebGLIndexedBufferRenderer.js 11 | // and WebGLBufferRenderer.js 12 | const SPY_METHODS = [ 13 | 'clear', 14 | 'bindFramebuffer', 15 | 'drawElements', 16 | 'drawElementsInstanced', 17 | 'multiDrawElementsWEBGL', 18 | 'multiDrawElementsInstancedWEBGL', 19 | 'drawArrays', 20 | 'drawArraysInstanced', 21 | 'multiDrawArraysWEBGL', 22 | 'multiDrawArraysInstancedWEBGL', 23 | ] 24 | 25 | const NO_PROGRAM = [ 26 | 'clear', 27 | 'bindFramebuffer', 28 | ] 29 | 30 | const DEPTH_BUFFER_BIT = 256 31 | const STENCIL_BUFFER_BIT = 1024 32 | const COLOR_BUFFER_BIT = 16384 33 | const DRAW_MODES = ['POINTS', 'LINES', 'LINE_LOOP', 'LINE_STRIP', 'TRIANGLES', 'TRIANGLE_FAN'] 34 | const CURRENT_PROGRAM = 35725 35 | 36 | let bufferId = 0 37 | const bufferMap = new WeakMap() 38 | 39 | export default class WebglSpy { 40 | constructor(gl) { 41 | this.gl = gl 42 | this.calls = [] 43 | this.hasSpied = false 44 | } 45 | 46 | spyContext(methods) { 47 | const gl = this.gl 48 | 49 | const spyCalls = (gl, methodName, res, args) => { 50 | const details = getDetails(gl, methodName, args) 51 | this.calls.push(details) 52 | } 53 | const spyBuffer = (gl, methodName, buffer) => { 54 | bufferMap.set(buffer, bufferId++) 55 | } 56 | 57 | methods.forEach(methodName => spyMethod(gl, methodName, spyCalls)) 58 | spyMethod(gl, 'createFramebuffer', spyBuffer) 59 | } 60 | 61 | unspyContext() { 62 | const gl = this.gl 63 | Object.getOwnPropertyNames(gl).forEach(propName => { 64 | if (propName.indexOf(SPY_OG_PREFIX) === 0) { 65 | const methodName = propName.replace(SPY_OG_PREFIX, '') 66 | 67 | // Cleanup all property functions 68 | // will default back to prototype methods 69 | gl[methodName] = null 70 | delete gl[methodName] 71 | gl[propName] = null 72 | delete gl[propName] 73 | } 74 | }) 75 | } 76 | 77 | startCapture(reset = true) { 78 | if (!this.hasSpied) { 79 | this.spyContext(SPY_METHODS) 80 | this.hasSpied = true 81 | } 82 | if (reset) this.calls.length = 0 83 | } 84 | 85 | endCapture() { 86 | let result = this.calls.slice() 87 | this.calls.length = 0 88 | return result 89 | } 90 | 91 | destroy() { 92 | this.unspyContext(this.gl) 93 | this.gl = null 94 | } 95 | } 96 | 97 | function getDetails(gl, methodName, args) { 98 | // Get drawcall details 99 | let action = methodName 100 | const type = getType(methodName) 101 | 102 | if (methodName.toLowerCase().indexOf('draw') > -1) { 103 | const mode = DRAW_MODES.filter(mode => args[0] === gl[mode]) 104 | action = `${action}: ${mode}, ${args[1]} indices` 105 | 106 | if (methodName.toLowerCase().indexOf('array') > -1) { 107 | const count = args[2] 108 | action = `${action}, ${count} vertices` 109 | 110 | } 111 | } 112 | 113 | if (methodName === 'bindFramebuffer') { 114 | if (args[1]) { 115 | const buffer = args[1] 116 | const id = bufferMap.get(buffer) 117 | action = `${action} ID - ${id}` 118 | } 119 | } 120 | 121 | // Clear bits details 122 | if (methodName === 'clear') { 123 | const depth = DEPTH_BUFFER_BIT & args[0] 124 | const stencil = STENCIL_BUFFER_BIT & args[0] 125 | const color = COLOR_BUFFER_BIT & args[0] 126 | 127 | const bits = ['DEPTH', 'STENCIL', 'COLOR'].filter((value, i) => [depth, stencil, color][i]) 128 | action = `${action}: ${bits.join(', ')}` 129 | } 130 | 131 | // Get program name 132 | let programName 133 | if (NO_PROGRAM.indexOf(methodName) < 0) { 134 | const program = gl.getParameter(CURRENT_PROGRAM) 135 | if (program) { 136 | const shaders = gl.getAttachedShaders(program) 137 | const source = gl.getShaderSource(shaders[0]) 138 | const name = readNameFromShaderSource(source) 139 | programName = name || 'Unnamed shader' 140 | } 141 | } 142 | 143 | return programName ? { action, program: programName, type } : { action, type } 144 | } 145 | 146 | function getType(methodName) { 147 | const method = methodName.toLowerCase() 148 | if (method.indexOf('draw') > -1) return TYPES.DRAW 149 | if (method.indexOf('bind') > -1) return TYPES.BIND 150 | if (methodName === 'clear') return TYPES.CLEAR 151 | } 152 | 153 | function spyMethod(gl, methodName, spy) { 154 | const ogMethod = gl[methodName] 155 | gl[SPY_OG_PREFIX + methodName] = ogMethod 156 | gl[methodName] = (...args) => { 157 | const res = ogMethod.apply(gl, args) 158 | spy(gl, methodName, res, args) 159 | return res 160 | } 161 | } 162 | 163 | // Taken from 164 | // https://github.com/BabylonJS/Spector.js/blob/5f652baa091b96e79f8d7d98bc6cc1d4e404ad00/src/backend/utils/readProgramHelper.ts#L63C5-L101C2 165 | // could replace with https://github.com/glslify/glsl-shader-name ? 166 | function readNameFromShaderSource(source) { 167 | try { 168 | let name = '' 169 | let match 170 | 171 | const shaderNameRegex = /#define[\s]+SHADER_NAME[\s]+([\S]+)(\n|$)/gi 172 | match = shaderNameRegex.exec(source) 173 | if (match !== null) { 174 | if (match.index === shaderNameRegex.lastIndex) { 175 | shaderNameRegex.lastIndex++ 176 | } 177 | name = match[1] 178 | } 179 | 180 | if (name === '') { 181 | // #define SHADER_NAME_B64 44K344Kn44O844OA44O8 182 | // #define SHADER_NAME_B64 8J+YjvCfmIE= 183 | const shaderName64Regex = /#define[\s]+SHADER_NAME_B64[\s]+([\S]+)(\n|$)/gi 184 | match = shaderName64Regex.exec(source) 185 | if (match !== null) { 186 | if (match.index === shaderName64Regex.lastIndex) { 187 | shaderName64Regex.lastIndex++ 188 | } 189 | 190 | name = match[1] 191 | } 192 | 193 | if (name) { 194 | name = decodeURIComponent(atob(name)) 195 | } 196 | } 197 | 198 | return name 199 | } 200 | catch (e) { 201 | return null 202 | } 203 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-spy", 3 | "version": "0.0.3", 4 | "description": "A tool capturing the webgl instructions to monitor the number of drawcalls & other performance-related commands.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ayamflow/webgl-spy.git" 9 | }, 10 | "keywords": [ 11 | "webgl", 12 | "performance", 13 | "monitor", 14 | "draw", 15 | "drawcall" 16 | ], 17 | "author": "Florian Morel", 18 | "license": "Unlicense", 19 | "bugs": { 20 | "url": "https://github.com/ayamflow/webgl-spy/issues" 21 | }, 22 | "homepage": "https://github.com/ayamflow/webgl-spy#readme" 23 | } 24 | --------------------------------------------------------------------------------