├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── build-and-publish.yml │ └── test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── _config.yml ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── augment-api.js ├── texture-utils.js ├── utils.js ├── webgl-memory.js └── wrap.js ├── test ├── assert.js ├── index.html ├── index.js ├── js │ └── twgl-full.module.js ├── mocha-support.js ├── mocha.css ├── mocha.js ├── puppeteer.js ├── tests │ ├── buffer-tests.js │ ├── contextloss-tests.js │ ├── info-tests.js │ ├── program-tests.js │ ├── renderbuffer-tests.js │ ├── sampler-tests.js │ ├── shader-tests.js │ ├── stack-tests.js │ ├── sync-tests.js │ ├── test-utils.js │ ├── texture-tests.js │ ├── transformfeedback-tests.js │ └── vertexarray-tests.js └── webgl.js ├── webgl-memory.js └── webgl-memory.png /.eslintignore: -------------------------------------------------------------------------------- 1 | test/mocha.js 2 | test/js/twgl-full.module.js 3 | webgl-memory.js 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | }, 7 | "globals": { 8 | "chroma": true, 9 | "importScripts": true, 10 | "SharedArrayBuffer": true, 11 | "twgl": true, 12 | }, 13 | "parserOptions": { 14 | "sourceType": "module", 15 | "ecmaVersion": 2020, 16 | }, 17 | "plugins": [ 18 | "eslint-plugin-html", 19 | "eslint-plugin-optional-comma-spacing", 20 | "eslint-plugin-one-variable-per-var", 21 | "eslint-plugin-require-trailing-comma", 22 | ], 23 | "extends": "eslint:recommended", 24 | "rules": { 25 | "no-alert": 2, 26 | "no-array-constructor": 2, 27 | "no-caller": 2, 28 | "no-catch-shadow": 2, 29 | "no-const-assign": 2, 30 | "no-eval": 2, 31 | "no-extend-native": 2, 32 | "no-extra-bind": 2, 33 | "no-implied-eval": 2, 34 | "no-iterator": 2, 35 | "no-label-var": 2, 36 | "no-labels": 2, 37 | "no-lone-blocks": 2, 38 | "no-loop-func": 2, 39 | "no-multi-str": 2, 40 | "no-native-reassign": 2, 41 | "no-new": 2, 42 | "no-new-func": 2, 43 | "no-new-object": 2, 44 | "no-new-wrappers": 2, 45 | "no-octal-escape": 2, 46 | "no-process-exit": 2, 47 | "no-proto": 2, 48 | "no-return-assign": 2, 49 | "no-script-url": 2, 50 | "no-sequences": 2, 51 | "no-shadow-restricted-names": 2, 52 | "no-spaced-func": 2, 53 | "no-trailing-spaces": 2, 54 | "no-undef-init": 2, 55 | "no-underscore-dangle": 2, 56 | "no-unused-expressions": 2, 57 | "no-use-before-define": 0, 58 | "no-with": 2, 59 | "consistent-return": 2, 60 | "curly": [2, "all"], 61 | "no-extra-parens": [2, "functions"], 62 | "eqeqeq": 2, 63 | "new-cap": 2, 64 | "new-parens": 2, 65 | "semi-spacing": [2, {"before": false, "after": true}], 66 | "space-infix-ops": 2, 67 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 68 | "strict": [2, "function"], 69 | "yoda": [2, "never"], 70 | "prefer-const": 2, 71 | "no-var": 2, 72 | 73 | "brace-style": [2, "1tbs", { "allowSingleLine": false }], 74 | "camelcase": [0], 75 | "comma-spacing": 0, 76 | "comma-dangle": 0, 77 | "comma-style": [2, "last"], 78 | "dot-notation": 0, 79 | "eol-last": [0], 80 | "global-strict": [0], 81 | "key-spacing": [0], 82 | "no-comma-dangle": [0], 83 | "no-irregular-whitespace": 2, 84 | "no-multi-spaces": [0], 85 | "no-obj-calls": 2, 86 | "no-shadow": [0], 87 | "no-undef": 2, 88 | "no-unreachable": 2, 89 | "one-variable-per-var/one-variable-per-var": [2], 90 | "optional-comma-spacing/optional-comma-spacing": [2, {"after": true}], 91 | "quotes": [0, "single"], 92 | "require-trailing-comma/require-trailing-comma": [2], 93 | "semi": [2, "always"], 94 | "space-before-function-paren": [2, "never"], 95 | "keyword-spacing": [1, {"before": true, "after": true, "overrides": {}} ], 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🍔🍟🥤 11 | uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Use Node.js 😂 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 18 19 | 20 | - name: Test 🧪 21 | run: | 22 | npm ci 23 | npm run check-ci 24 | 25 | - name: Publish to NPM 26 | uses: JS-DevTools/npm-publish@v3 27 | with: 28 | token: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🍔🍟🥤 12 | uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | 21 | - name: Test 🧪 22 | run: | 23 | npm ci 24 | npm run check-ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL-Memory 2 | 3 | 4 | 5 | This is a WebGL-Memory tracker. You add the script to your page 6 | before you initialize WebGL and then for a given context 7 | you can ask how much WebGL memory you're using. 8 | 9 | Note: This is only a guess as various GPUs have different 10 | internal requirements. For example a GPU might require that 11 | RGB be expanded internally to RGBA. Similarly a GPU might 12 | have alignment requirements. Still, this is likely to give 13 | a reasonable approximation. 14 | 15 | ## Usage 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | or 22 | 23 | ```js 24 | import 'https://greggman.github.io/webgl-memory/webgl-memory.js'; 25 | ``` 26 | 27 | Then in your code 28 | 29 | ```js 30 | const ext = gl.getExtension('GMAN_webgl_memory'); 31 | ... 32 | if (ext) { 33 | // memory info 34 | const info = ext.getMemoryInfo(); 35 | // every texture, it's size, a stack of where it was created and a stack of where it was last updated. 36 | const textures = ext.getResourcesInfo(WebGLTexture); 37 | // every buffer, it's size, a stack of where it was created and a stack of where it was last updated. 38 | const buffers = ext.getResourcesInfo(WebGLBuffer); 39 | } 40 | ``` 41 | 42 | The info returned is 43 | 44 | ```js 45 | { 46 | memory: { 47 | buffer: 48 | texture: 49 | renderbuffer: 50 | drawingbuffer: 51 | total: 52 | }, 53 | resources: { 54 | buffer: , 55 | renderbuffer: 56 | program: 57 | query: 58 | sampler: 59 | shader: 60 | sync: 61 | texture: 62 | transformFeedback: 63 | vertexArray: 64 | } 65 | } 66 | ``` 67 | 68 | The data for textures and buffers 69 | 70 | ```js 71 | const ext = gl.getExtension('GMAN_webgl_memory'); 72 | ... 73 | if (ext) { 74 | const tex = gl.createTexture(); // 1 75 | gl.bindTexture(gl.TEXTURE_2D, tex); 76 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 4, 1); // 2 77 | 78 | const buf = gl.createBuffer(); // 3 79 | gl.bindBuffer(gl.ARRAY_BUFFER); 80 | gl.bufferData(gl.ARRAY_BUFFER, 32, gl.STATIC_DRAW); // 4 81 | 82 | 83 | const textures = ext.getResourcesInfo(WebGLTexture); 84 | const buffers = ext.getResourcesInfo(WebGLBuffer); 85 | ``` 86 | 87 | ```js 88 | textures = [ 89 | { size: 16, stackCreated: '...1...', stackUpdated: '...2...' } 90 | ] 91 | 92 | buffers = [ 93 | { size: 32, stackCreated: '...3'''., stackUpdated: '...4...' } 94 | ] 95 | ``` 96 | 97 | ## Caveats 98 | 99 | 1. You must have WebGL error free code. 100 | 101 | If your code is generating WebGL errors you must fix those first 102 | before using this library. Consider using [webgl-lint](https://greggman.github.io/webgl-lint) to help find your errors. 103 | 104 | 2. Resource reference counting is not supported. 105 | 106 | In WebGL if you delete a WebGLObject (a buffer, a texture, etc..), 107 | then, if that object is still attached to something else (a buffer 108 | attached to a vertex array, a texture attached to a framebuffer, 109 | a shader attached to a program), the object is not actually deleted 110 | until it's detached or the thing it's attached to is itself deleted 111 | unless the thing it's attached to is currently bound. It's complicated 😭 112 | 113 | Tracking all of that in JavaScript is more work than I was willing 114 | to put in ATM. My belief is that the stuff that is still attached 115 | is usually not a problem because either (a) you'll delete the objects 116 | that are holding the attachments (b) you'll detach the attachments 117 | by binding new ones (c) you have a leak where you're creating more and 118 | more of these objects that hold attachments in which case you can find 119 | the issue by watching your resources counts climb. 120 | 121 | Given that it seemed okay to skip this for now. 122 | 123 | 3. Deletion by Garbage Collection (GC) is not supported 124 | 125 | In JavaScript and WebGL, it's possible to let things get auto deleted by GC. 126 | 127 | ```js 128 | { 129 | const buf = gl.createBuffer(); 130 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 131 | gl.bufferData(gl.ARRAY_BUFFER, 1024 * 1024 * 256, gl.STATIC_DRAW); 132 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 133 | } 134 | ``` 135 | 136 | Given the code above, buffer will, at some point in the future, get automatically 137 | deleted. The problem is you have no idea when. JavaScript does know now the size of 138 | VRAM nor does it have any concept of the size of the WebGL buffer (256meg in this case). 139 | All JavaScript has is a tiny object that holds an ID for the actual OpenGL buffer 140 | and maybe a little metadata. 141 | 142 | That means there's absolutely no pressure to delete the buffer above in a timely 143 | manner nor either is there any way for JavaScript to know that releasing that 144 | object would free up VRAM. 145 | 146 | In other words. Let's say you had a total of 384meg of ram. You'd expect this to 147 | work. 148 | 149 | ```js 150 | { 151 | const a = new Uint32Array(256 * 1024 * 1024) 152 | } 153 | { 154 | const b = new Uint32Array(256 * 1024 * 1024) 155 | } 156 | ``` 157 | 158 | The code above allocates 512meg. Given we were pretending the system only has 384meg, 159 | JavaScript will likely free `a` to make room for `b` 160 | 161 | Now, Let's do the WebGL case and assume 384meg of VRAM 162 | 163 | ```js 164 | { 165 | const a = gl.createBuffer(); 166 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 167 | gl.bufferData(gl.ARRAY_BUFFER, 1024 * 1024 * 256, gl.STATIC_DRAW); 168 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 169 | } 170 | { 171 | const b = gl.createBuffer(); 172 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 173 | gl.bufferData(gl.ARRAY_BUFFER, 1024 * 1024 * 256, gl.STATIC_DRAW); 174 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 175 | } 176 | ``` 177 | 178 | In this case, JavaScript only sees `a` as taking a few bytes (the object that tracks 179 | the OpenGL resource) so it has no idea that it needs to free `a` to make room for `b`. 180 | This could would fail, ideally with `gl.OUT_OF_MEMORY`. 181 | 182 | That was the long way of saying, you should never count on GC for WebGL! 183 | Free your resources explicitly! 184 | 185 | That's also part of the reason why we don't support this case because 186 | counting on GC is not a useful solution. 187 | 188 | 4. `texImage2D/3D` vs `texStorage2D/3D` 189 | 190 | Be aware that `texImage2D/3D` *may* require double the memory of 191 | `texStorage2D/3D`. 192 | 193 | Based on the design of `texImage2D/3D`, every mip level can have a 194 | different size/format and so until it's time to draw, there is 195 | no way to know if those levels will be updated by the app to be 196 | matching. Further, in WebGL2, there's no way to know before draw time 197 | if the app will set `TEXTURE_BASE_LEVEL` and `TEXTURE_MAX_LEVEL` to 198 | be a *texture complete* subset of mip levels. 199 | 200 | WebGL-memory does not report this difference 201 | because it's up to the implementation what really happens behind the scenes. 202 | In general though, `texStorage2D/3D` has a much higher probability 203 | of using less memory overall. 204 | 205 | The tradeoff for using `texStorage` is that the texture's size is immutable. 206 | So, for example, if you wanted to wrap a user's image to a cube, and then 207 | change that image when the user selects a different sized image, with `texImage` 208 | you can just upload the new image to the existing texture. With `texStorage` 209 | you'd be required to create a new texture. 210 | 211 | 5. `ELEMENT_ARRAY_BUFFER` 212 | 213 | Buffers used with `ELEMENT_ARRAY_BUFFER` may need a second copy in ram. 214 | This is because WebGL requires no out of bounds memory access (eg, 215 | you have a buffer with 10 vertices but you have an index greater >= 10). 216 | This can be handled in 2 ways (1) if the driver advertizes "robustness" 217 | then rely on the driver (2) keep a copy of that data in ram and check 218 | before draw time that no indices are out of range. 219 | 220 | WebGL-memory does not report this difference because it's up to the 221 | implementation. Further, unlike the texture issue above there is 222 | nothing an app can do. Fortunately such buffers are usually 223 | a small percent of the data used by most WebGL apps. 224 | 225 | ## Example: 226 | 227 | [Click here for an Example](https://jsgist.org/?src=57dafa41cb1d2d5bc1520832db49f946) 228 | [Unity example here](https://greggman.github.io/webgl-memory-unity-example/) 229 | 230 | ## Development 231 | 232 | ```bash 233 | git clone https://github.com/greggman/webgl-memory.git 234 | cd webgl-memory 235 | npm install 236 | ``` 237 | 238 | now serve the folder 239 | 240 | ``` 241 | npx servez 242 | ``` 243 | 244 | and go to [`http://localhost:8080/test?src=true`](http://localhost:8080/test?src=true) 245 | 246 | `src=true` tells the test harness to use the unrolled source from the `src` folder 247 | where as without it uses `webgl-memory.js` in the root folder which is built using 248 | `npm run build`. 249 | 250 | `grep=` will limit the tests as in `...?src=true&grep=renderbuffer` only 251 | runs the tests with `renderbuffer` in their description. 252 | 253 | ## Live Tests 254 | 255 | [built version](https://greggman.github.io/webgl-memory/test/) 256 | [source version](https://greggman.github.io/webgl-memory/test/?src=true) 257 | 258 | ## Thoughts 259 | 260 | I'm not totally convinced this is the right way to do this. If I was making a 261 | webgl app and I wanted to know this stuff I think I'd track it myself by wrapping 262 | my own creation functions. 263 | 264 | In other words, lets say I wanted to know how many times I call 265 | `fetch`. 266 | 267 | ```js 268 | const req = await fetch(url); 269 | const text = await req.text(); 270 | ``` 271 | 272 | I'd just refactor that 273 | 274 | ```js 275 | let fetchCount = 0; 276 | function doFetch(url) { 277 | fetchCount++; 278 | return fetch(url); 279 | } 280 | 281 | ... 282 | const req = await doFetch(url); 283 | const text = await req.text(); 284 | ``` 285 | 286 | No need for some fancy library. Simple. 287 | 288 | I could do similar things for WebGL functions. 289 | 290 | ```js 291 | let textureCount = 0; 292 | function makeTexture(gl) { 293 | textureCount++; 294 | return gl.createTexture(gl); 295 | } 296 | function freeTexture(gl, tex) { 297 | --textureCount; 298 | gl.deleteTexture(tex); 299 | } 300 | 301 | const tex = makeTexture(gl); 302 | ... 303 | freeTexture(gl, tex); 304 | ``` 305 | 306 | Also, even if webgl-memory is an okay way to do it I'm not sure making it an extension was the best way 307 | vs just some library you call like `webglMemoryTracker.init(someWebGLRenderingContext)`. 308 | I structured it this way just because I used [webgl-lint](https://greggman.github.io/webgl-lint) as 309 | the basis to get this working. 310 | 311 | ## License 312 | 313 | [MIT](https://github.com/greggman/webgl-memory/blob/main/LICENCE.md) 314 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-memory", 3 | "version": "1.1.2", 4 | "description": "A WebGL memory tracker", 5 | "main": "webgl-memory.js", 6 | "module": "webgl-memory.js", 7 | "type": "module", 8 | "scripts": { 9 | "build": "rollup -c", 10 | "check-ci": "npm run eslint && npm run build && npm run test", 11 | "eslint": "eslint \"**/*.js\"", 12 | "pre-push": "npm run eslint && npm run test", 13 | "test": "npm run test-browser", 14 | "test-browser": "node test/puppeteer.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/greggman/webgl-memory.git" 19 | }, 20 | "keywords": [ 21 | "webgl" 22 | ], 23 | "author": "Gregg Tavares", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/greggman/webgl-memory/issues" 27 | }, 28 | "files": [ 29 | "webgl-memory.js" 30 | ], 31 | "homepage": "https://github.com/greggman/webgl-memory#readme", 32 | "devDependencies": { 33 | "@rollup/plugin-node-resolve": "^15.2.3", 34 | "eslint": "^8.57.0", 35 | "eslint-plugin-html": "^8.1.1", 36 | "eslint-plugin-one-variable-per-var": "^0.0.3", 37 | "eslint-plugin-optional-comma-spacing": "^0.0.4", 38 | "eslint-plugin-require-trailing-comma": "^0.0.1", 39 | "express": "^4.21.2", 40 | "puppeteer": "^23.11.1", 41 | "rollup": "^4.29.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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-memory@${pkg.version}, license MIT */`; 6 | 7 | export default [ 8 | { 9 | input: 'src/webgl-memory.js', 10 | plugins: [ 11 | resolve({ 12 | modulesOnly: true, 13 | }), 14 | ], 15 | output: [ 16 | { 17 | format: 'umd', 18 | file: 'webgl-memory.js', 19 | indent: ' ', 20 | banner, 21 | }, 22 | ], 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/augment-api.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 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 { 25 | getBytesForMip, 26 | } from './texture-utils.js'; 27 | import { 28 | computeDrawingbufferSize, 29 | getDrawingbufferInfo, 30 | isBufferSource, 31 | isNumber, 32 | getStackTrace, 33 | collectObjects, 34 | } from './utils.js'; 35 | 36 | //------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ] 37 | 38 | /* 39 | ** Copyright (c) 2012 The Khronos Group Inc. 40 | ** 41 | ** Permission is hereby granted, free of charge, to any person obtaining a 42 | ** copy of this software and/or associated documentation files (the 43 | ** "Materials"), to deal in the Materials without restriction, including 44 | ** without limitation the rights to use, copy, modify, merge, publish, 45 | ** distribute, sublicense, and/or sell copies of the Materials, and to 46 | ** permit persons to whom the Materials are furnished to do so, subject to 47 | ** the following conditions: 48 | ** 49 | ** The above copyright notice and this permission notice shall be included 50 | ** in all copies or substantial portions of the Materials. 51 | ** 52 | ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 53 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 54 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 55 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 56 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 57 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 58 | ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. 59 | */ 60 | 61 | 62 | const augmentedSet = new Set(); 63 | 64 | /** 65 | * Given a WebGL context replaces all the functions with wrapped functions 66 | * that call gl.getError after every command 67 | * 68 | * @param {WebGLRenderingContext|Extension} ctx The webgl context to wrap. 69 | * @param {string} nameOfClass (eg, webgl, webgl2, OES_texture_float) 70 | */ 71 | // eslint-disable-next-line consistent-return 72 | export function augmentAPI(ctx, nameOfClass, options = {}) { 73 | 74 | if (augmentedSet.has(ctx)) { 75 | return ctx; 76 | } 77 | augmentedSet.add(ctx); 78 | 79 | const origGLErrorFn = options.origGLErrorFn || ctx.getError; 80 | 81 | function createSharedState(ctx) { 82 | const drawingBufferInfo = getDrawingbufferInfo(ctx); 83 | const sharedState = { 84 | baseContext: ctx, 85 | config: options, 86 | apis: { 87 | // custom extension 88 | gman_webgl_memory: { 89 | ctx: { 90 | getMemoryInfo() { 91 | const drawingbuffer = computeDrawingbufferSize(ctx, drawingBufferInfo); 92 | return { 93 | memory: { 94 | ...memory, 95 | drawingbuffer, 96 | total: drawingbuffer + memory.buffer + memory.texture + memory.renderbuffer, 97 | }, 98 | resources: { 99 | ...resources, 100 | }, 101 | }; 102 | }, 103 | getResourcesInfo(type) { 104 | return collectObjects(sharedState, type); 105 | }, 106 | }, 107 | }, 108 | }, 109 | resources: {}, 110 | memory: { 111 | texture: 0, 112 | buffer: 0, 113 | renderbuffer: 0, 114 | }, 115 | bindings: new Map(), 116 | defaultVertexArray: {}, 117 | webglObjectToMemory: new Map(), 118 | }; 119 | 120 | const unRestorableAPIs = new Set([ 121 | 'webgl', 122 | 'webgl2', 123 | 'webgl_lose_context', 124 | ]); 125 | 126 | function resetSharedState() { 127 | sharedState.bindings.clear(); 128 | sharedState.webglObjectToMemory.clear(); 129 | sharedState.webglObjectToMemory.set(sharedState.defaultVertexArray, {}); 130 | sharedState.currentVertexArray = sharedState.defaultVertexArray; 131 | [sharedState.resources, sharedState.memory].forEach(function(obj) { 132 | for (const prop in obj) { 133 | obj[prop] = 0; 134 | } 135 | }); 136 | } 137 | 138 | function handleContextLost() { 139 | // Issues: 140 | // * all resources are lost. 141 | // Solution: handled by resetSharedState 142 | // * all functions are no-op 143 | // Solutions: 144 | // * swap all functions for noop 145 | // (not so easy because some functions return values) 146 | // * wrap all functions is a isContextLost check forwarder 147 | // (slow? and same as above) 148 | // * have each function manually check for context lost 149 | // (simple but repetitive) 150 | // * all extensions are lost 151 | // Solution: For these we go through and restore all the functions 152 | // on each extension 153 | resetSharedState(); 154 | sharedState.isContextLost = true; 155 | 156 | // restore all original functions for extensions since 157 | // user will have to get new extensions. 158 | for (const [name, {ctx, origFuncs}] of [...Object.entries(sharedState.apis)]) { 159 | if (!unRestorableAPIs.has(name) && origFuncs) { 160 | augmentedSet.delete(ctx); 161 | for (const [funcName, origFn] of Object.entries(origFuncs)) { 162 | ctx[funcName] = origFn; 163 | } 164 | delete apis[name]; 165 | } 166 | } 167 | } 168 | 169 | function handleContextRestored() { 170 | sharedState.isContextLost = false; 171 | } 172 | 173 | if (ctx.canvas) { 174 | ctx.canvas.addEventListener('webglcontextlost', handleContextLost); 175 | ctx.canvas.addEventListener('webglcontextrestored', handleContextRestored); 176 | } 177 | 178 | resetSharedState(); 179 | return sharedState; 180 | } 181 | 182 | const sharedState = options.sharedState || createSharedState(ctx); 183 | options.sharedState = sharedState; 184 | 185 | const { 186 | apis, 187 | bindings, 188 | memory, 189 | resources, 190 | webglObjectToMemory, 191 | } = sharedState; 192 | 193 | const origFuncs = {}; 194 | 195 | function noop() { 196 | } 197 | 198 | function makeCreateWrapper(ctx, typeName, _funcName) { 199 | const funcName = _funcName || `create${typeName[0].toUpperCase()}${typeName.substr(1)}`; 200 | if (!ctx[funcName]) { 201 | return null; 202 | } 203 | resources[typeName] = 0; 204 | return function(ctx, funcName, args, webglObj) { 205 | if (sharedState.isContextLost) { 206 | return; 207 | } 208 | ++resources[typeName]; 209 | webglObjectToMemory.set(webglObj, { 210 | size: 0, 211 | stackCreated: getStackTrace(), 212 | }); 213 | }; 214 | } 215 | 216 | function makeDeleteWrapper(typeName, fn = noop, _funcName) { 217 | const funcName = _funcName || `delete${typeName[0].toUpperCase()}${typeName.substr(1)}`; 218 | if (!ctx[funcName]) { 219 | return null; 220 | } 221 | return function(ctx, funcName, args) { 222 | if (sharedState.isContextLost) { 223 | return; 224 | } 225 | const [obj] = args; 226 | const info = webglObjectToMemory.get(obj); 227 | if (info) { 228 | --resources[typeName]; 229 | fn(obj, info); 230 | // TODO: handle resource counts 231 | webglObjectToMemory.delete(obj); 232 | } 233 | }; 234 | } 235 | 236 | function updateRenderbuffer(target, samples, internalFormat, width, height) { 237 | if (sharedState.isContextLost) { 238 | return; 239 | } 240 | const obj = bindings.get(target); 241 | if (!obj) { 242 | throw new Error(`no renderbuffer bound to ${target}`); 243 | } 244 | const info = webglObjectToMemory.get(obj); 245 | if (!info) { 246 | throw new Error(`unknown renderbuffer ${obj}`); 247 | } 248 | 249 | const bytesForMip = getBytesForMip(internalFormat, width, height, 1); 250 | const newSize = bytesForMip * samples; 251 | 252 | memory.renderbuffer -= info.size; 253 | info.size = newSize; 254 | info.stackUpdated = getStackTrace(); 255 | memory.renderbuffer += newSize; 256 | } 257 | 258 | const ELEMENT_ARRAY_BUFFER = 0x8893; 259 | 260 | const UNSIGNED_BYTE = 0x1401; 261 | const TEXTURE_CUBE_MAP = 0x8513; 262 | const TEXTURE_2D_ARRAY = 0x8C1A; 263 | const TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; 264 | const TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; 265 | const TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; 266 | const TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; 267 | const TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; 268 | const TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A; 269 | 270 | const TEXTURE_BASE_LEVEL = 0x813C; 271 | const TEXTURE_MAX_LEVEL = 0x813D; 272 | 273 | const cubemapTargets = new Set([ 274 | TEXTURE_CUBE_MAP_POSITIVE_X, 275 | TEXTURE_CUBE_MAP_NEGATIVE_X, 276 | TEXTURE_CUBE_MAP_POSITIVE_Y, 277 | TEXTURE_CUBE_MAP_NEGATIVE_Y, 278 | TEXTURE_CUBE_MAP_POSITIVE_Z, 279 | TEXTURE_CUBE_MAP_NEGATIVE_Z, 280 | ]); 281 | 282 | function isCubemapFace(target) { 283 | return cubemapTargets.has(target); 284 | } 285 | 286 | function getTextureInfo(target) { 287 | target = isCubemapFace(target) ? TEXTURE_CUBE_MAP : target; 288 | const obj = bindings.get(target); 289 | if (!obj) { 290 | throw new Error(`no texture bound to ${target}`); 291 | } 292 | const info = webglObjectToMemory.get(obj); 293 | if (!info) { 294 | throw new Error(`unknown texture ${obj}`); 295 | } 296 | return info; 297 | } 298 | 299 | function updateMipLevel(info, target, level, internalFormat, width, height, depth, type) { 300 | const oldSize = info.size; 301 | const newMipSize = getBytesForMip(internalFormat, width, height, depth, type); 302 | 303 | const faceNdx = isCubemapFace(target) 304 | ? target - TEXTURE_CUBE_MAP_POSITIVE_X 305 | : 0; 306 | 307 | info.mips = info.mips || []; 308 | info.mips[level] = info.mips[level] || []; 309 | const mipFaceInfo = info.mips[level][faceNdx] || {}; 310 | info.size -= mipFaceInfo.size || 0; 311 | 312 | mipFaceInfo.size = newMipSize; 313 | mipFaceInfo.internalFormat = internalFormat; 314 | mipFaceInfo.type = type; 315 | mipFaceInfo.width = width; 316 | mipFaceInfo.height = height; 317 | mipFaceInfo.depth = depth; 318 | 319 | info.mips[level][faceNdx] = mipFaceInfo; 320 | info.size += newMipSize; 321 | 322 | memory.texture -= oldSize; 323 | memory.texture += info.size; 324 | 325 | info.stackUpdated = getStackTrace(); 326 | } 327 | 328 | function updateTexStorage(target, levels, internalFormat, width, height, depth) { 329 | const info = getTextureInfo(target); 330 | const numFaces = target === TEXTURE_CUBE_MAP ? 6 : 1; 331 | const baseFaceTarget = target === TEXTURE_CUBE_MAP ? TEXTURE_CUBE_MAP_POSITIVE_X : target; 332 | for (let level = 0; level < levels; ++level) { 333 | for (let face = 0; face < numFaces; ++face) { 334 | updateMipLevel(info, baseFaceTarget + face, level, internalFormat, width, height, depth); 335 | } 336 | width = Math.ceil(Math.max(width / 2, 1)); 337 | height = Math.ceil(Math.max(height / 2, 1)); 338 | depth = target === TEXTURE_2D_ARRAY ? depth : Math.ceil(Math.max(depth / 2, 1)); 339 | } 340 | } 341 | 342 | function handleBindVertexArray(gl, funcName, args) { 343 | if (sharedState.isContextLost) { 344 | return; 345 | } 346 | const [va] = args; 347 | sharedState.currentVertexArray = va ? va : sharedState.defaultVertexArray; 348 | } 349 | 350 | function handleBufferBinding(target, obj) { 351 | if (sharedState.isContextLost) { 352 | return; 353 | } 354 | switch (target) { 355 | case ELEMENT_ARRAY_BUFFER: { 356 | const info = webglObjectToMemory.get(sharedState.currentVertexArray); 357 | info.elementArrayBuffer = obj; 358 | break; 359 | } 360 | default: 361 | bindings.set(target, obj); 362 | break; 363 | } 364 | } 365 | 366 | const preChecks = {}; 367 | const postChecks = { 368 | // WebGL1 369 | // void bufferData(GLenum target, GLsizeiptr size, GLenum usage); 370 | // void bufferData(GLenum target, [AllowShared] BufferSource? srcData, GLenum usage); 371 | // WebGL2: 372 | // void bufferData(GLenum target, [AllowShared] ArrayBufferView srcData, GLenum usage, GLuint srcOffset, 373 | // optional GLuint length = 0); 374 | bufferData(gl, funcName, args) { 375 | if (sharedState.isContextLost) { 376 | return; 377 | } 378 | const [target, src, /* usage */, /*srcOffset = 0*/, length = undefined] = args; 379 | let obj; 380 | switch (target) { 381 | case ELEMENT_ARRAY_BUFFER: 382 | { 383 | const info = webglObjectToMemory.get(sharedState.currentVertexArray); 384 | obj = info.elementArrayBuffer; 385 | } 386 | break; 387 | default: 388 | obj = bindings.get(target); 389 | break; 390 | } 391 | if (!obj) { 392 | throw new Error(`no buffer bound to ${target}`); 393 | } 394 | let newSize = 0; 395 | if (length !== undefined) { 396 | newSize = length * src.BYTES_PER_ELEMENT; 397 | } else if (isBufferSource(src)) { 398 | newSize = src.byteLength; 399 | } else if (isNumber(src)) { 400 | newSize = src; 401 | } else { 402 | throw new Error(`unsupported bufferData src type ${src}`); 403 | } 404 | 405 | const info = webglObjectToMemory.get(obj); 406 | if (!info) { 407 | throw new Error(`unknown buffer ${obj}`); 408 | } 409 | 410 | memory.buffer -= info.size; 411 | info.size = newSize; 412 | info.stackUpdated = getStackTrace(); 413 | memory.buffer += newSize; 414 | }, 415 | 416 | bindVertexArray: handleBindVertexArray, 417 | bindVertexArrayOES: handleBindVertexArray, 418 | 419 | bindBuffer(gl, funcName, args) { 420 | const [target, obj] = args; 421 | handleBufferBinding(target, obj); 422 | }, 423 | 424 | bindBufferBase(gl, funcName, args) { 425 | const [target, /*ndx*/, obj] = args; 426 | handleBufferBinding(target, obj); 427 | }, 428 | 429 | bindBufferRange(gl, funcName, args) { 430 | const [target, /*ndx*/, obj, /*offset*/, /*size*/] = args; 431 | handleBufferBinding(target, obj); 432 | }, 433 | 434 | bindRenderbuffer(gl, funcName, args) { 435 | if (sharedState.isContextLost) { 436 | return; 437 | } 438 | const [target, obj] = args; 439 | bindings.set(target, obj); 440 | }, 441 | 442 | bindTexture(gl, funcName, args) { 443 | if (sharedState.isContextLost) { 444 | return; 445 | } 446 | const [target, obj] = args; 447 | bindings.set(target, obj); 448 | }, 449 | 450 | // void gl.copyTexImage2D(target, level, internalformat, x, y, width, height, border); 451 | copyTexImage2D(ctx, funcName, args) { 452 | if (sharedState.isContextLost) { 453 | return; 454 | } 455 | const [target, level, internalFormat, /*x*/, /*y*/, width, height, /*border*/] = args; 456 | const info = getTextureInfo(target); 457 | updateMipLevel(info, target, level, internalFormat, width, height, 1, UNSIGNED_BYTE); 458 | }, 459 | 460 | createBuffer: makeCreateWrapper(ctx, 'buffer'), 461 | createFramebuffer: makeCreateWrapper(ctx, 'framebuffer'), 462 | createRenderbuffer: makeCreateWrapper(ctx, 'renderbuffer'), 463 | createProgram: makeCreateWrapper(ctx, 'program'), 464 | createQuery: makeCreateWrapper(ctx, 'query'), 465 | createShader: makeCreateWrapper(ctx, 'shader'), 466 | createSampler: makeCreateWrapper(ctx, 'sampler'), 467 | createTexture: makeCreateWrapper(ctx, 'texture'), 468 | createTransformFeedback: makeCreateWrapper(ctx, 'transformFeedback'), 469 | createVertexArray: makeCreateWrapper(ctx, 'vertexArray'), 470 | createVertexArrayOES: makeCreateWrapper(ctx, 'vertexArray', 'createVertexArrayOES'), 471 | 472 | // WebGL 1: 473 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, ArrayBufferView? pixels); 474 | // 475 | // Additionally available in WebGL 2: 476 | // read from buffer bound to gl.PIXEL_UNPACK_BUFFER 477 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, GLsizei imageSize, GLintptr offset); 478 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, 479 | // ArrayBufferView srcData, optional srcOffset, optional srcLengthOverride); 480 | compressedTexImage2D(ctx, funcName, args) { 481 | if (sharedState.isContextLost) { 482 | return; 483 | } 484 | const [target, level, internalFormat, width, height] = args; 485 | const info = getTextureInfo(target); 486 | updateMipLevel(info, target, level, internalFormat, width, height, 1, UNSIGNED_BYTE); 487 | }, 488 | 489 | // read from buffer bound to gl.PIXEL_UNPACK_BUFFER 490 | // void gl.compressedTexImage3D(target, level, internalformat, width, height, depth, border, GLsizei imageSize, GLintptr offset); 491 | // void gl.compressedTexImage3D(target, level, internalformat, width, height, depth, border, 492 | // ArrayBufferView srcData, optional srcOffset, optional srcLengthOverride); 493 | compressedTexImage3D(ctx, funcName, args) { 494 | if (sharedState.isContextLost) { 495 | return; 496 | } 497 | const [target, level, internalFormat, width, height, depth] = args; 498 | const info = getTextureInfo(target); 499 | updateMipLevel(info, target, level, internalFormat, width, height, depth, UNSIGNED_BYTE); 500 | }, 501 | 502 | deleteBuffer: makeDeleteWrapper('buffer', function(obj, info) { 503 | memory.buffer -= info.size; 504 | }), 505 | deleteFramebuffer: makeDeleteWrapper('framebuffer'), 506 | deleteProgram: makeDeleteWrapper('program'), 507 | deleteQuery: makeDeleteWrapper('query'), 508 | deleteRenderbuffer: makeDeleteWrapper('renderbuffer', function(obj, info) { 509 | memory.renderbuffer -= info.size; 510 | }), 511 | deleteSampler: makeDeleteWrapper('sampler'), 512 | deleteShader: makeDeleteWrapper('shader'), 513 | deleteSync: makeDeleteWrapper('sync'), 514 | deleteTexture: makeDeleteWrapper('texture', function(obj, info) { 515 | memory.texture -= info.size; 516 | }), 517 | deleteTransformFeedback: makeDeleteWrapper('transformFeedback'), 518 | deleteVertexArray: makeDeleteWrapper('vertexArray'), 519 | deleteVertexArrayOES: makeDeleteWrapper('vertexArray', noop, 'deleteVertexArrayOES'), 520 | 521 | fenceSync: function(ctx) { 522 | if (sharedState.isContextLost) { 523 | return undefined; 524 | } 525 | if (!ctx.fenceSync) { 526 | return undefined; 527 | } 528 | resources.sync = 0; 529 | return function(ctx, funcName, args, webglObj) { 530 | ++resources.sync; 531 | 532 | webglObjectToMemory.set(webglObj, { 533 | size: 0, 534 | }); 535 | }; 536 | }(ctx), 537 | 538 | generateMipmap(ctx, funcName, args) { 539 | if (sharedState.isContextLost) { 540 | return; 541 | } 542 | const [target] = args; 543 | const info = getTextureInfo(target); 544 | const baseMipNdx = info.parameters ? info.parameters.get(TEXTURE_BASE_LEVEL) || 0 : 0; 545 | const maxMipNdx = info.parameters ? info.parameters.get(TEXTURE_MAX_LEVEL) || 1024 : 1024; 546 | const mipInfo = info.mips[baseMipNdx][0]; 547 | let {width, height, depth} = mipInfo; 548 | const {internalFormat, type} = mipInfo; 549 | let level = baseMipNdx + 1; 550 | 551 | const numFaces = target === TEXTURE_CUBE_MAP ? 6 : 1; 552 | const baseFaceTarget = target === TEXTURE_CUBE_MAP ? TEXTURE_CUBE_MAP_POSITIVE_X : target; 553 | while (level <= maxMipNdx && !(width === 1 && height === 1 && (depth === 1 || target === TEXTURE_2D_ARRAY))) { 554 | width = Math.ceil(Math.max(width / 2, 1)); 555 | height = Math.ceil(Math.max(height / 2, 1)); 556 | depth = target === TEXTURE_2D_ARRAY ? depth : Math.ceil(Math.max(depth / 2, 1)); 557 | for (let face = 0; face < numFaces; ++face) { 558 | updateMipLevel(info, baseFaceTarget + face, level, internalFormat, width, height, depth, type); 559 | } 560 | ++level; 561 | } 562 | }, 563 | 564 | getSupportedExtensions(ctx, funcName, args, result) { 565 | if (sharedState.isContextLost) { 566 | return; 567 | } 568 | result.push('GMAN_webgl_memory'); 569 | }, 570 | 571 | // void gl.renderbufferStorage(target, internalFormat, width, height); 572 | // gl.RGBA4: 4 red bits, 4 green bits, 4 blue bits 4 alpha bits. 573 | // gl.RGB565: 5 red bits, 6 green bits, 5 blue bits. 574 | // gl.RGB5_A1: 5 red bits, 5 green bits, 5 blue bits, 1 alpha bit. 575 | // gl.DEPTH_COMPONENT16: 16 depth bits. 576 | // gl.STENCIL_INDEX8: 8 stencil bits. 577 | // gl.DEPTH_STENCIL 578 | renderbufferStorage(ctx, funcName, args) { 579 | const [target, internalFormat, width, height] = args; 580 | updateRenderbuffer(target, 1, internalFormat, width, height); 581 | }, 582 | 583 | // void gl.renderbufferStorageMultisample(target, samples, internalFormat, width, height); 584 | renderbufferStorageMultisample(ctx, funcName, args) { 585 | const [target, samples, internalFormat, width, height] = args; 586 | updateRenderbuffer(target, samples, internalFormat, width, height); 587 | }, 588 | 589 | texImage2D(ctx, funcName, args) { 590 | if (sharedState.isContextLost) { 591 | return; 592 | } 593 | // WebGL1: 594 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView? pixels); 595 | // void gl.texImage2D(target, level, internalformat, format, type, ImageData? pixels); 596 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels); 597 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels); 598 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLVideoElement? pixels); 599 | // void gl.texImage2D(target, level, internalformat, format, type, ImageBitmap? pixels// ); 600 | 601 | // WebGL2: 602 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, GLintptr offset); 603 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLCanvasElement source); 604 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLImageElement source); 605 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLVideoElement source); 606 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageBitmap source); 607 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageData source); 608 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView srcData, srcOffset); 609 | const [target, level, internalFormat] = args; 610 | let width; 611 | let height; 612 | let type; 613 | if (args.length === 6) { 614 | const src = args[5]; 615 | width = src.width; 616 | height = src.height; 617 | type = args[4]; 618 | } else { 619 | width = args[3]; 620 | height = args[4]; 621 | type = args[7]; 622 | } 623 | 624 | const info = getTextureInfo(target); 625 | updateMipLevel(info, target, level, internalFormat, width, height, 1, type); 626 | }, 627 | 628 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, GLintptr offset); 629 | // 630 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLCanvasElement source); 631 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLImageElement source); 632 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLVideoElement source); 633 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ImageBitmap source); 634 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ImageData source); 635 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ArrayBufferView? srcData); 636 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ArrayBufferView srcData, srcOffset); 637 | 638 | texImage3D(ctx, funcName, args) { 639 | if (sharedState.isContextLost) { 640 | return; 641 | } 642 | const [target, level, internalFormat, width, height, depth, /*border*/, /*format*/, type] = args; 643 | const info = getTextureInfo(target); 644 | updateMipLevel(info, target, level, internalFormat, width, height, depth, type); 645 | }, 646 | 647 | texParameteri(ctx, funcName, args) { 648 | if (sharedState.isContextLost) { 649 | return; 650 | } 651 | const [target, pname, value] = args; 652 | const info = getTextureInfo(target); 653 | info.parameters = info.parameters || new Map(); 654 | info.parameters.set(pname, value); 655 | }, 656 | 657 | // void gl.texStorage2D(target, levels, internalformat, width, height); 658 | texStorage2D(ctx, funcName, args) { 659 | const [target, levels, internalFormat, width, height] = args; 660 | updateTexStorage(target, levels, internalFormat, width, height, 1); 661 | }, 662 | 663 | // void gl.texStorage3D(target, levels, internalformat, width, height, depth); 664 | texStorage3D(ctx, funcName, args) { 665 | const [target, levels, internalFormat, width, height, depth] = args; 666 | updateTexStorage(target, levels, internalFormat, width, height, depth); 667 | }, 668 | }; 669 | 670 | const extraWrappers = { 671 | getExtension(ctx, propertyName) { 672 | if (sharedState.isContextLost) { 673 | return; 674 | } 675 | const origFn = ctx[propertyName]; 676 | ctx[propertyName] = function(...args) { 677 | const extensionName = args[0].toLowerCase(); 678 | const api = apis[extensionName]; 679 | if (api) { 680 | return api.ctx; 681 | } 682 | const ext = origFn.call(ctx, ...args); 683 | if (ext) { 684 | augmentAPI(ext, extensionName, {...options, origGLErrorFn}); 685 | } 686 | return ext; 687 | }; 688 | }, 689 | }; 690 | 691 | // Makes a function that calls a WebGL function and then calls getError. 692 | function makeErrorWrapper(ctx, funcName) { 693 | const origFn = ctx[funcName]; 694 | const preCheck = preChecks[funcName] || noop; 695 | const postCheck = postChecks[funcName] || noop; 696 | if (preCheck === noop && postChecks === noop) { 697 | return; 698 | } 699 | ctx[funcName] = function(...args) { 700 | preCheck(ctx, funcName, args); 701 | const result = origFn.call(ctx, ...args); 702 | postCheck(ctx, funcName, args, result); 703 | return result; 704 | }; 705 | const extraWrapperFn = extraWrappers[funcName]; 706 | if (extraWrapperFn) { 707 | extraWrapperFn(ctx, funcName, origGLErrorFn); 708 | } 709 | } 710 | 711 | // Wrap each function 712 | for (const propertyName in ctx) { 713 | if (typeof ctx[propertyName] === 'function') { 714 | origFuncs[propertyName] = ctx[propertyName]; 715 | makeErrorWrapper(ctx, propertyName); 716 | } 717 | } 718 | 719 | apis[nameOfClass.toLowerCase()] = { ctx, origFuncs }; 720 | } 721 | 722 | -------------------------------------------------------------------------------- /src/texture-utils.js: -------------------------------------------------------------------------------- 1 | 2 | /* PixelFormat */ 3 | const ALPHA = 0x1906; 4 | const RGB = 0x1907; 5 | const RGBA = 0x1908; 6 | const LUMINANCE = 0x1909; 7 | const LUMINANCE_ALPHA = 0x190A; 8 | const DEPTH_COMPONENT = 0x1902; 9 | const DEPTH_STENCIL = 0x84F9; 10 | 11 | const R8 = 0x8229; 12 | const R8_SNORM = 0x8F94; 13 | const R16F = 0x822D; 14 | const R32F = 0x822E; 15 | const R8UI = 0x8232; 16 | const R8I = 0x8231; 17 | const RG16UI = 0x823A; 18 | const RG16I = 0x8239; 19 | const RG32UI = 0x823C; 20 | const RG32I = 0x823B; 21 | const RG8 = 0x822B; 22 | const RG8_SNORM = 0x8F95; 23 | const RG16F = 0x822F; 24 | const RG32F = 0x8230; 25 | const RG8UI = 0x8238; 26 | const RG8I = 0x8237; 27 | const R16UI = 0x8234; 28 | const R16I = 0x8233; 29 | const R32UI = 0x8236; 30 | const R32I = 0x8235; 31 | const RGB8 = 0x8051; 32 | const SRGB8 = 0x8C41; 33 | const RGB565 = 0x8D62; 34 | const RGB8_SNORM = 0x8F96; 35 | const R11F_G11F_B10F = 0x8C3A; 36 | const RGB9_E5 = 0x8C3D; 37 | const RGB16F = 0x881B; 38 | const RGB32F = 0x8815; 39 | const RGB8UI = 0x8D7D; 40 | const RGB8I = 0x8D8F; 41 | const RGB16UI = 0x8D77; 42 | const RGB16I = 0x8D89; 43 | const RGB32UI = 0x8D71; 44 | const RGB32I = 0x8D83; 45 | const RGBA8 = 0x8058; 46 | const SRGB8_ALPHA8 = 0x8C43; 47 | const RGBA8_SNORM = 0x8F97; 48 | const RGB5_A1 = 0x8057; 49 | const RGBA4 = 0x8056; 50 | const RGB10_A2 = 0x8059; 51 | const RGBA16F = 0x881A; 52 | const RGBA32F = 0x8814; 53 | const RGBA8UI = 0x8D7C; 54 | const RGBA8I = 0x8D8E; 55 | const RGB10_A2UI = 0x906F; 56 | const RGBA16UI = 0x8D76; 57 | const RGBA16I = 0x8D88; 58 | const RGBA32I = 0x8D82; 59 | const RGBA32UI = 0x8D70; 60 | 61 | const DEPTH_COMPONENT16 = 0x81A5; 62 | const DEPTH_COMPONENT24 = 0x81A6; 63 | const DEPTH_COMPONENT32F = 0x8CAC; 64 | const DEPTH32F_STENCIL8 = 0x8CAD; 65 | const DEPTH24_STENCIL8 = 0x88F0; 66 | const STENCIL_INDEX8 = 0x8d48; 67 | 68 | /* DataType */ 69 | // const BYTE = 0x1400; 70 | const UNSIGNED_BYTE = 0x1401; 71 | // const SHORT = 0x1402; 72 | const UNSIGNED_SHORT = 0x1403; 73 | // const INT = 0x1404; 74 | const UNSIGNED_INT = 0x1405; 75 | const FLOAT = 0x1406; 76 | const UNSIGNED_SHORT_4_4_4_4 = 0x8033; 77 | const UNSIGNED_SHORT_5_5_5_1 = 0x8034; 78 | const UNSIGNED_SHORT_5_6_5 = 0x8363; 79 | const HALF_FLOAT = 0x140B; 80 | const HALF_FLOAT_OES = 0x8D61; // Thanks Khronos for making this different >:( 81 | // const UNSIGNED_INT_2_10_10_10_REV = 0x8368; 82 | // const UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B; 83 | // const UNSIGNED_INT_5_9_9_9_REV = 0x8C3E; 84 | // const FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD; 85 | // const UNSIGNED_INT_24_8 = 0x84FA; 86 | 87 | const RG = 0x8227; 88 | const RG_INTEGER = 0x8228; 89 | const RED = 0x1903; 90 | const RED_INTEGER = 0x8D94; 91 | const RGB_INTEGER = 0x8D98; 92 | const RGBA_INTEGER = 0x8D99; 93 | 94 | const SRGB_ALPHA_EXT = 0x8C42; 95 | 96 | 97 | const formatInfo = {}; 98 | { 99 | // NOTE: this is named `numColorComponents` vs `numComponents` so we can let Uglify mangle 100 | // the name. 101 | const f = formatInfo; 102 | f[ALPHA] = { numColorComponents: 1, }; 103 | f[LUMINANCE] = { numColorComponents: 1, }; 104 | f[LUMINANCE_ALPHA] = { numColorComponents: 2, }; 105 | f[RGB] = { numColorComponents: 3, }; 106 | f[RGBA] = { numColorComponents: 4, }; 107 | f[RED] = { numColorComponents: 1, }; 108 | f[RED_INTEGER] = { numColorComponents: 1, }; 109 | f[RG] = { numColorComponents: 2, }; 110 | f[RG_INTEGER] = { numColorComponents: 2, }; 111 | f[RGB] = { numColorComponents: 3, }; 112 | f[RGB_INTEGER] = { numColorComponents: 3, }; 113 | f[RGBA] = { numColorComponents: 4, }; 114 | f[RGBA_INTEGER] = { numColorComponents: 4, }; 115 | f[DEPTH_COMPONENT] = { numColorComponents: 1, }; 116 | f[DEPTH_STENCIL] = { numColorComponents: 2, }; 117 | } 118 | 119 | /** 120 | * @typedef {Object} TextureFormatDetails 121 | * @property {number} textureFormat format to pass texImage2D and similar functions. 122 | * @property {boolean} colorRenderable true if you can render to this format of texture. 123 | * @property {boolean} textureFilterable true if you can filter the texture, false if you can ony use `NEAREST`. 124 | * @property {number[]} type Array of possible types you can pass to texImage2D and similar function 125 | * @property {Object.} bytesPerElementMap A map of types to bytes per element 126 | * @private 127 | */ 128 | 129 | let s_textureInternalFormatInfo; 130 | function getTextureInternalFormatInfo(internalFormat) { 131 | if (!s_textureInternalFormatInfo) { 132 | // NOTE: these properties need unique names so we can let Uglify mangle the name. 133 | const t = {}; 134 | // unsized formats 135 | t[ALPHA] = { bytesPerElement: [1, 2, 2, 4], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 136 | t[LUMINANCE] = { bytesPerElement: [1, 2, 2, 4], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 137 | t[LUMINANCE_ALPHA] = { bytesPerElement: [2, 4, 4, 8], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 138 | t[RGB] = { bytesPerElement: [3, 6, 6, 12, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_5_6_5], }; 139 | t[RGBA] = { bytesPerElement: [4, 8, 8, 16, 2, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_4_4_4_4, UNSIGNED_SHORT_5_5_5_1], }; 140 | t[SRGB_ALPHA_EXT] = { bytesPerElement: [4, 8, 8, 16, 2, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_4_4_4_4, UNSIGNED_SHORT_5_5_5_1], }; 141 | t[DEPTH_COMPONENT] = { bytesPerElement: [2, 4], type: [UNSIGNED_INT, UNSIGNED_SHORT], }; 142 | t[DEPTH_STENCIL] = { bytesPerElement: [4], }; 143 | 144 | // sized formats 145 | t[R8] = { bytesPerElement: [1], }; 146 | t[R8_SNORM] = { bytesPerElement: [1], }; 147 | t[R16F] = { bytesPerElement: [2], }; 148 | t[R32F] = { bytesPerElement: [4], }; 149 | t[R8UI] = { bytesPerElement: [1], }; 150 | t[R8I] = { bytesPerElement: [1], }; 151 | t[R16UI] = { bytesPerElement: [2], }; 152 | t[R16I] = { bytesPerElement: [2], }; 153 | t[R32UI] = { bytesPerElement: [4], }; 154 | t[R32I] = { bytesPerElement: [4], }; 155 | t[RG8] = { bytesPerElement: [2], }; 156 | t[RG8_SNORM] = { bytesPerElement: [2], }; 157 | t[RG16F] = { bytesPerElement: [4], }; 158 | t[RG32F] = { bytesPerElement: [8], }; 159 | t[RG8UI] = { bytesPerElement: [2], }; 160 | t[RG8I] = { bytesPerElement: [2], }; 161 | t[RG16UI] = { bytesPerElement: [4], }; 162 | t[RG16I] = { bytesPerElement: [4], }; 163 | t[RG32UI] = { bytesPerElement: [8], }; 164 | t[RG32I] = { bytesPerElement: [8], }; 165 | t[RGB8] = { bytesPerElement: [3], }; 166 | t[SRGB8] = { bytesPerElement: [3], }; 167 | t[RGB565] = { bytesPerElement: [2], }; 168 | t[RGB8_SNORM] = { bytesPerElement: [3], }; 169 | t[R11F_G11F_B10F] = { bytesPerElement: [4], }; 170 | t[RGB9_E5] = { bytesPerElement: [4], }; 171 | t[RGB16F] = { bytesPerElement: [6], }; 172 | t[RGB32F] = { bytesPerElement: [12], }; 173 | t[RGB8UI] = { bytesPerElement: [3], }; 174 | t[RGB8I] = { bytesPerElement: [3], }; 175 | t[RGB16UI] = { bytesPerElement: [6], }; 176 | t[RGB16I] = { bytesPerElement: [6], }; 177 | t[RGB32UI] = { bytesPerElement: [12], }; 178 | t[RGB32I] = { bytesPerElement: [12], }; 179 | t[RGBA8] = { bytesPerElement: [4], }; 180 | t[SRGB8_ALPHA8] = { bytesPerElement: [4], }; 181 | t[RGBA8_SNORM] = { bytesPerElement: [4], }; 182 | t[RGB5_A1] = { bytesPerElement: [2], }; 183 | t[RGBA4] = { bytesPerElement: [2], }; 184 | t[RGB10_A2] = { bytesPerElement: [4], }; 185 | t[RGBA16F] = { bytesPerElement: [8], }; 186 | t[RGBA32F] = { bytesPerElement: [16], }; 187 | t[RGBA8UI] = { bytesPerElement: [4], }; 188 | t[RGBA8I] = { bytesPerElement: [4], }; 189 | t[RGB10_A2UI] = { bytesPerElement: [4], }; 190 | t[RGBA16UI] = { bytesPerElement: [8], }; 191 | t[RGBA16I] = { bytesPerElement: [8], }; 192 | t[RGBA32I] = { bytesPerElement: [16], }; 193 | t[RGBA32UI] = { bytesPerElement: [16], }; 194 | // Sized Internal 195 | t[DEPTH_COMPONENT16] = { bytesPerElement: [2], }; 196 | t[DEPTH_COMPONENT24] = { bytesPerElement: [4], }; 197 | t[DEPTH_COMPONENT32F] = { bytesPerElement: [4], }; 198 | t[DEPTH24_STENCIL8] = { bytesPerElement: [4], }; 199 | t[DEPTH32F_STENCIL8] = { bytesPerElement: [4], }; 200 | t[STENCIL_INDEX8] = { bytesPerElement: [1], }; 201 | 202 | s_textureInternalFormatInfo = t; 203 | } 204 | return s_textureInternalFormatInfo[internalFormat]; 205 | } 206 | 207 | function makeComputeBlockRectSizeFunction(blockWidth, blockHeight, bytesPerBlock) { 208 | return function(width, height, depth) { 209 | const blocksAcross = (width + blockWidth - 1) / blockWidth | 0; 210 | const blocksDown = (height + blockHeight - 1) / blockHeight | 0; 211 | return blocksAcross * blocksDown * bytesPerBlock * depth; 212 | }; 213 | } 214 | 215 | function makeComputePaddedRectSizeFunction(minWidth, minHeight, divisor) { 216 | return function(width, height, depth) { 217 | return (Math.max(width, minWidth) * Math.max(height, minHeight) / divisor | 0) * depth; 218 | }; 219 | } 220 | 221 | // WEBGL_compressed_texture_s3tc 222 | const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; 223 | const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; 224 | const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; 225 | const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; 226 | // WEBGL_compressed_texture_etc1 227 | const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64; 228 | // WEBGL_compressed_texture_pvrtc 229 | const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00; 230 | const COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01; 231 | const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02; 232 | const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03; 233 | // WEBGL_compressed_texture_etc 234 | const COMPRESSED_R11_EAC = 0x9270; 235 | const COMPRESSED_SIGNED_R11_EAC = 0x9271; 236 | const COMPRESSED_RG11_EAC = 0x9272; 237 | const COMPRESSED_SIGNED_RG11_EAC = 0x9273; 238 | const COMPRESSED_RGB8_ETC2 = 0x9274; 239 | const COMPRESSED_SRGB8_ETC2 = 0x9275; 240 | const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; 241 | const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; 242 | const COMPRESSED_RGBA8_ETC2_EAC = 0x9278; 243 | const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; 244 | // WEBGL_compressed_texture_astc 245 | const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; 246 | const COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1; 247 | const COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2; 248 | const COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3; 249 | const COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4; 250 | const COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5; 251 | const COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6; 252 | const COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7; 253 | const COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8; 254 | const COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9; 255 | const COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA; 256 | const COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB; 257 | const COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC; 258 | const COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD; 259 | const COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0; 260 | const COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1; 261 | const COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2; 262 | const COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3; 263 | const COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4; 264 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5; 265 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6; 266 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7; 267 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8; 268 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9; 269 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA; 270 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB; 271 | const COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC; 272 | const COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD; 273 | // WEBGL_compressed_texture_s3tc_srgb 274 | const COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C; 275 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D; 276 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E; 277 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F; 278 | // EXT_texture_compression_bptc 279 | const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C; 280 | const COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; 281 | const COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E; 282 | const COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F; 283 | // EXT_texture_compression_rgtc 284 | const COMPRESSED_RED_RGTC1_EXT = 0x8DBB; 285 | const COMPRESSED_SIGNED_RED_RGTC1_EXT = 0x8DBC; 286 | const COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD; 287 | const COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 0x8DBE; 288 | 289 | const compressedTextureFunctions = new Map([ 290 | [ COMPRESSED_RGB_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 291 | [ COMPRESSED_RGBA_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 292 | [ COMPRESSED_RGBA_S3TC_DXT3_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 293 | [ COMPRESSED_RGBA_S3TC_DXT5_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 294 | 295 | [ COMPRESSED_RGB_ETC1_WEBGL, makeComputeBlockRectSizeFunction(4, 4, 8) ], 296 | 297 | [ COMPRESSED_RGB_PVRTC_4BPPV1_IMG, makeComputePaddedRectSizeFunction(8, 8, 2) ], 298 | [ COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, makeComputePaddedRectSizeFunction(8, 8, 2) ], 299 | [ COMPRESSED_RGB_PVRTC_2BPPV1_IMG, makeComputePaddedRectSizeFunction(16, 8, 4) ], 300 | [ COMPRESSED_RGBA_PVRTC_2BPPV1_IMG, makeComputePaddedRectSizeFunction(16, 8, 4) ], 301 | 302 | [ COMPRESSED_R11_EAC, makeComputeBlockRectSizeFunction(4, 4, 8) ], 303 | [ COMPRESSED_SIGNED_R11_EAC, makeComputeBlockRectSizeFunction(4, 4, 8) ], 304 | [ COMPRESSED_RGB8_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 305 | [ COMPRESSED_SRGB8_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 306 | [ COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 307 | [ COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 308 | 309 | [ COMPRESSED_RG11_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 310 | [ COMPRESSED_SIGNED_RG11_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 311 | [ COMPRESSED_RGBA8_ETC2_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 312 | [ COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 313 | 314 | [ COMPRESSED_RGBA_ASTC_4x4_KHR, makeComputeBlockRectSizeFunction(4, 4, 16) ], 315 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR, makeComputeBlockRectSizeFunction(4, 4, 16) ], 316 | [ COMPRESSED_RGBA_ASTC_5x4_KHR, makeComputeBlockRectSizeFunction(5, 4, 16) ], 317 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR, makeComputeBlockRectSizeFunction(5, 4, 16) ], 318 | [ COMPRESSED_RGBA_ASTC_5x5_KHR, makeComputeBlockRectSizeFunction(5, 5, 16) ], 319 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR, makeComputeBlockRectSizeFunction(5, 5, 16) ], 320 | [ COMPRESSED_RGBA_ASTC_6x5_KHR, makeComputeBlockRectSizeFunction(6, 5, 16) ], 321 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR, makeComputeBlockRectSizeFunction(6, 5, 16) ], 322 | [ COMPRESSED_RGBA_ASTC_6x6_KHR, makeComputeBlockRectSizeFunction(6, 6, 16) ], 323 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR, makeComputeBlockRectSizeFunction(6, 6, 16) ], 324 | [ COMPRESSED_RGBA_ASTC_8x5_KHR, makeComputeBlockRectSizeFunction(8, 5, 16) ], 325 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR, makeComputeBlockRectSizeFunction(8, 5, 16) ], 326 | [ COMPRESSED_RGBA_ASTC_8x6_KHR, makeComputeBlockRectSizeFunction(8, 6, 16) ], 327 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR, makeComputeBlockRectSizeFunction(8, 6, 16) ], 328 | [ COMPRESSED_RGBA_ASTC_8x8_KHR, makeComputeBlockRectSizeFunction(8, 8, 16) ], 329 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR, makeComputeBlockRectSizeFunction(8, 8, 16) ], 330 | [ COMPRESSED_RGBA_ASTC_10x5_KHR, makeComputeBlockRectSizeFunction(10, 5, 16) ], 331 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR, makeComputeBlockRectSizeFunction(10, 5, 16) ], 332 | [ COMPRESSED_RGBA_ASTC_10x6_KHR, makeComputeBlockRectSizeFunction(10, 6, 16) ], 333 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR, makeComputeBlockRectSizeFunction(10, 6, 16) ], 334 | [ COMPRESSED_RGBA_ASTC_10x8_KHR, makeComputeBlockRectSizeFunction(10, 8, 16) ], 335 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR, makeComputeBlockRectSizeFunction(10, 8, 16) ], 336 | [ COMPRESSED_RGBA_ASTC_10x10_KHR, makeComputeBlockRectSizeFunction(10, 10, 16) ], 337 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR, makeComputeBlockRectSizeFunction(10, 10, 16) ], 338 | [ COMPRESSED_RGBA_ASTC_12x10_KHR, makeComputeBlockRectSizeFunction(12, 10, 16) ], 339 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR, makeComputeBlockRectSizeFunction(12, 10, 16) ], 340 | [ COMPRESSED_RGBA_ASTC_12x12_KHR, makeComputeBlockRectSizeFunction(12, 12, 16) ], 341 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR, makeComputeBlockRectSizeFunction(12, 12, 16) ], 342 | 343 | [ COMPRESSED_SRGB_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 344 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 345 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 346 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 347 | 348 | [ COMPRESSED_RGBA_BPTC_UNORM_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 349 | [ COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 350 | [ COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 351 | [ COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 352 | 353 | [ COMPRESSED_RED_RGTC1_EXT, makeComputeBlockRectSizeFunction( 4, 4, 8 ) ], 354 | [ COMPRESSED_SIGNED_RED_RGTC1_EXT, makeComputeBlockRectSizeFunction( 4, 4, 8 ) ], 355 | [ COMPRESSED_RED_GREEN_RGTC2_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 356 | [ COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 357 | ]); 358 | 359 | /** 360 | * Gets the number of bytes per element for a given internalFormat / type 361 | * @param {number} internalFormat The internalFormat parameter from texImage2D etc.. 362 | * @param {number} type The type parameter for texImage2D etc.. 363 | * @return {number} the number of bytes per element for the given internalFormat, type combo 364 | * @memberOf module:twgl/textures 365 | */ 366 | export function getBytesPerElementForInternalFormat(internalFormat, type) { 367 | const info = getTextureInternalFormatInfo(internalFormat); 368 | if (!info) { 369 | throw "unknown internal format"; 370 | } 371 | if (info.type) { 372 | const ndx = info.type.indexOf(type); 373 | if (ndx < 0) { 374 | throw new Error(`unsupported type ${type} for internalformat ${internalFormat}`); 375 | } 376 | return info.bytesPerElement[ndx]; 377 | } 378 | return info.bytesPerElement[0]; 379 | } 380 | 381 | function getBytesForMipUncompressed(internalFormat, width, height, depth, type) { 382 | const bytesPerElement = getBytesPerElementForInternalFormat(internalFormat, type); 383 | return width * height * depth * bytesPerElement; 384 | } 385 | 386 | export function getBytesForMip(internalFormat, width, height, depth, type) { 387 | const fn = compressedTextureFunctions.get(internalFormat); 388 | return fn ? fn(width, height, depth) : getBytesForMipUncompressed(internalFormat, width, height, depth, type); 389 | } 390 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function isWebGL2(gl) { 2 | // a proxy for if this is webgl 3 | return !!gl.texImage3D; 4 | } 5 | 6 | export function isTypedArray(v) { 7 | return v && v.buffer && v.buffer instanceof ArrayBuffer; 8 | } 9 | 10 | export function isBufferSource(v) { 11 | return isTypedArray(v) || v instanceof ArrayBuffer; 12 | } 13 | 14 | // --------------------------------- 15 | const FLOAT = 0x1406; 16 | const INT = 0x1404; 17 | const BOOL = 0x8B56; 18 | const UNSIGNED_INT = 0x1405; 19 | 20 | const TEXTURE_BINDING_2D = 0x8069; 21 | const TEXTURE_BINDING_CUBE_MAP = 0x8514; 22 | const TEXTURE_BINDING_3D = 0x806A; 23 | const TEXTURE_BINDING_2D_ARRAY = 0x8C1D; 24 | 25 | 26 | const ARRAY_BUFFER = 0x8892; 27 | const ELEMENT_ARRAY_BUFFER = 0x8893; 28 | const ARRAY_BUFFER_BINDING = 0x8894; 29 | const ELEMENT_ARRAY_BUFFER_BINDING = 0x8895; 30 | const TEXTURE_2D = 0x0DE1; 31 | const TEXTURE_3D = 0x806F; 32 | const TEXTURE_2D_ARRAY = 0x8C1A; 33 | const TEXTURE_CUBE_MAP = 0x8513; 34 | const FRAMEBUFFER = 0x8D40; 35 | const RENDERBUFFER = 0x8D41; 36 | const FRAMEBUFFER_BINDING = 0x8CA6; 37 | const RENDERBUFFER_BINDING = 0x8CA7; 38 | const TRANSFORM_FEEDBACK_BUFFER = 0x8C8E; 39 | const TRANSFORM_FEEDBACK_BUFFER_BINDING = 0x8C8F; 40 | const DRAW_FRAMEBUFFER = 0x8CA9; 41 | const READ_FRAMEBUFFER = 0x8CA8; 42 | const READ_FRAMEBUFFER_BINDING = 0x8CAA; 43 | const UNIFORM_BUFFER = 0x8A11; 44 | const UNIFORM_BUFFER_BINDING = 0x8A28; 45 | const TRANSFORM_FEEDBACK = 0x8E22; 46 | const TRANSFORM_FEEDBACK_BINDING = 0x8E25; 47 | 48 | const bindPointMap = new Map([ 49 | [ARRAY_BUFFER, ARRAY_BUFFER_BINDING], 50 | [ELEMENT_ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER_BINDING], 51 | [TEXTURE_2D, TEXTURE_BINDING_2D], 52 | [TEXTURE_CUBE_MAP, TEXTURE_BINDING_CUBE_MAP], 53 | [TEXTURE_3D, TEXTURE_BINDING_3D], 54 | [TEXTURE_2D_ARRAY, TEXTURE_BINDING_2D_ARRAY], 55 | [RENDERBUFFER, RENDERBUFFER_BINDING], 56 | [FRAMEBUFFER, FRAMEBUFFER_BINDING], 57 | [DRAW_FRAMEBUFFER, FRAMEBUFFER_BINDING], 58 | [READ_FRAMEBUFFER, READ_FRAMEBUFFER_BINDING], 59 | [UNIFORM_BUFFER, UNIFORM_BUFFER_BINDING], 60 | [TRANSFORM_FEEDBACK_BUFFER, TRANSFORM_FEEDBACK_BUFFER_BINDING], 61 | [TRANSFORM_FEEDBACK, TRANSFORM_FEEDBACK_BINDING], 62 | ]); 63 | 64 | export function getBindingQueryEnumForBindPoint(bindPoint) { 65 | return bindPointMap.get(bindPoint); 66 | } 67 | 68 | const BYTE = 0x1400; 69 | const SHORT = 0x1402; 70 | const UNSIGNED_BYTE = 0x1401; 71 | const UNSIGNED_SHORT = 0x1403; 72 | 73 | const glTypeToSizeMap = new Map([ 74 | [BOOL , 1], 75 | [BYTE , 1], 76 | [UNSIGNED_BYTE , 1], 77 | [SHORT , 2], 78 | [UNSIGNED_SHORT , 2], 79 | [INT , 4], 80 | [UNSIGNED_INT , 4], 81 | [FLOAT , 4], 82 | ]); 83 | 84 | export function getBytesPerValueForGLType(type) { 85 | return glTypeToSizeMap.get(type) || 0; 86 | } 87 | 88 | const glTypeToTypedArrayMap = new Map([ 89 | [UNSIGNED_BYTE, Uint8Array], 90 | [UNSIGNED_SHORT, Uint16Array], 91 | [UNSIGNED_INT, Uint32Array], 92 | ]); 93 | 94 | export function glTypeToTypedArray(type) { 95 | return glTypeToTypedArrayMap.get(type); 96 | } 97 | 98 | export function getDrawingbufferInfo(gl) { 99 | return { 100 | samples: gl.getParameter(gl.SAMPLES) || 1, 101 | depthBits: gl.getParameter(gl.DEPTH_BITS), 102 | stencilBits: gl.getParameter(gl.STENCIL_BITS), 103 | contextAttributes: gl.getContextAttributes(), 104 | }; 105 | } 106 | 107 | function computeDepthStencilSize(drawingBufferInfo) { 108 | const {depthBits, stencilBits} = drawingBufferInfo; 109 | const depthSize = (depthBits + stencilBits + 7) / 8 | 0; 110 | return depthSize === 3 ? 4 : depthSize; 111 | } 112 | 113 | export function computeDrawingbufferSize(gl, drawingBufferInfo) { 114 | if (gl.isContextLost()) { 115 | return 0; 116 | } 117 | const {samples} = drawingBufferInfo; 118 | // this will need to change for hi-color support 119 | const colorSize = 4; 120 | const size = gl.drawingBufferWidth * gl.drawingBufferHeight; 121 | const depthStencilSize = computeDepthStencilSize(drawingBufferInfo); 122 | return size * colorSize + size * samples * colorSize + size * depthStencilSize; 123 | } 124 | 125 | // I know this is not a full check 126 | export function isNumber(v) { 127 | return typeof v === 'number'; 128 | } 129 | 130 | export function collectObjects(state, type) { 131 | const list = [...state.webglObjectToMemory.keys()] 132 | .filter(obj => obj instanceof type) 133 | .map((obj) => state.webglObjectToMemory.get(obj)); 134 | 135 | return list; 136 | } 137 | 138 | export function getStackTrace() { 139 | const stack = (new Error()).stack; 140 | const lines = stack.split('\n'); 141 | // Remove the first two entries, the error message and this function itself, or the webgl-memory itself. 142 | const userLines = lines.slice(2).filter((l) => !l.includes('webgl-memory.js')); 143 | return userLines.join('\n'); 144 | } 145 | -------------------------------------------------------------------------------- /src/webgl-memory.js: -------------------------------------------------------------------------------- 1 | import './wrap.js'; 2 | -------------------------------------------------------------------------------- /src/wrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 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 | function wrapGetContext(Ctor) { 27 | const oldFn = Ctor.prototype.getContext; 28 | Ctor.prototype.getContext = function(type, ...args) { 29 | const ctx = oldFn.call(this, type, ...args); 30 | // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext 31 | // but that might fail if wrapped by debugging extension 32 | if (ctx && ctx.bindTexture) { 33 | const config = {}; 34 | augmentAPI(ctx, type, config); 35 | ctx.getExtension('GMAN_webgl_memory'); 36 | } 37 | return ctx; 38 | }; 39 | } 40 | 41 | if (typeof HTMLCanvasElement !== 'undefined') { 42 | wrapGetContext(HTMLCanvasElement); 43 | } 44 | if (typeof OffscreenCanvas !== 'undefined') { 45 | wrapGetContext(OffscreenCanvas); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | export const config = {}; 2 | 3 | export function setConfig(options) { 4 | Object.assign(config, options); 5 | } 6 | 7 | function formatMsg(msg) { 8 | return `${msg}${msg ? ': ' : ''}`; 9 | } 10 | 11 | export function assertTruthy(actual, msg = '') { 12 | if (!config.noLint && !actual) { 13 | throw new Error(`${formatMsg(msg)}expected: truthy, actual: ${actual}`); 14 | } 15 | } 16 | 17 | export function assertFalsy(actual, msg = '') { 18 | if (!config.noLint && actual) { 19 | throw new Error(`${formatMsg(msg)}expected: falsy, actual: ${actual}`); 20 | } 21 | } 22 | 23 | export function assertStringMatchesRegEx(actual, regex, msg = '') { 24 | if (!config.noLint && !regex.test(actual)) { 25 | throw new Error(`${formatMsg(msg)}expected: ${regex}, actual: ${actual}`); 26 | } 27 | } 28 | 29 | export function assertEqual(actual, expected, msg = '') { 30 | if (!config.noLint && actual !== expected) { 31 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`); 32 | } 33 | } 34 | 35 | export function assertNotEqual(actual, expected, msg = '') { 36 | if (!config.noLint && actual === expected) { 37 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`); 38 | } 39 | } 40 | 41 | export function assertThrowsWith(func, expectations, msg = '') { 42 | let error = ''; 43 | if (config.throwOnError === false) { 44 | const origFn = console.error; 45 | const errors = []; 46 | console.error = function(...args) { 47 | errors.push(args.join(' ')); 48 | }; 49 | func(); 50 | console.error = origFn; 51 | if (errors.length) { 52 | error = errors.join('\n'); 53 | console.error(error); 54 | } 55 | } else { 56 | try { 57 | func(); 58 | } catch (e) { 59 | console.error(e); // eslint-disable-line 60 | error = e; 61 | } 62 | 63 | } 64 | 65 | if (config.noLint) { 66 | return; 67 | } 68 | 69 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 70 | } 71 | 72 | // check if it throws it throws with x 73 | export function assertIfThrowsItThrowsWith(func, expectations, msg = '') { 74 | let error = ''; 75 | let threw = false; 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 | threw = true; 95 | } 96 | 97 | } 98 | 99 | if (config.noLint) { 100 | return; 101 | } 102 | 103 | if (!threw) { 104 | return; 105 | } 106 | 107 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 108 | } 109 | 110 | function assertStringMatchesREs(actual, expectations, msg) { 111 | for (const expectation of expectations) { 112 | if (expectation instanceof RegExp) { 113 | if (!expectation.test(actual)) { 114 | throw new Error(`${formatMsg(msg)}expected: ${expectation}, actual: ${actual}`); 115 | } 116 | } 117 | } 118 | 119 | } 120 | export function assertWarnsWith(func, expectations, msg = '') { 121 | const warnings = []; 122 | const origWarnFn = console.warn; 123 | console.warn = function(...args) { 124 | origWarnFn.call(this, ...args); 125 | warnings.push(args.join(' ')); 126 | }; 127 | 128 | let error; 129 | try { 130 | func(); 131 | } catch (e) { 132 | error = e; 133 | } 134 | 135 | console.warn = origWarnFn; 136 | 137 | if (error) { 138 | throw error; 139 | } 140 | 141 | if (config.noLint) { 142 | return; 143 | } 144 | 145 | assertStringMatchesREs(warnings.join(' '), expectations, msg); 146 | } 147 | 148 | export default { 149 | false: assertFalsy, 150 | equal: assertEqual, 151 | matchesRegEx: assertStringMatchesRegEx, 152 | notEqual: assertNotEqual, 153 | throwsWith: assertThrowsWith, 154 | true: assertTruthy, 155 | }; -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebGL Memory Tests 6 | 7 | 8 | 13 | 14 | 15 |
16 |
17 | 33 | 34 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | 3 | import './tests/buffer-tests.js'; 4 | import './tests/contextloss-tests.js'; 5 | import './tests/info-tests.js'; 6 | import './tests/program-tests.js'; 7 | import './tests/renderbuffer-tests.js'; 8 | import './tests/sampler-tests.js'; 9 | import './tests/shader-tests.js'; 10 | import './tests/sync-tests.js'; 11 | import './tests/texture-tests.js'; 12 | import './tests/transformfeedback-tests.js'; 13 | import './tests/vertexarray-tests.js'; 14 | import './tests/stack-tests.js'; 15 | 16 | const settings = Object.fromEntries(new URLSearchParams(window.location.search).entries()); 17 | if (settings.reporter) { 18 | mocha.reporter(settings.reporter); 19 | } 20 | mocha.run((failures) => { 21 | window.testsPromiseInfo.resolve(failures); 22 | }); 23 | -------------------------------------------------------------------------------- /test/mocha-support.js: -------------------------------------------------------------------------------- 1 | export const describe = window.describe; 2 | export const it = window.it; 3 | export const before = window.before; 4 | export const after = window.after; 5 | export const beforeEach = window.beforeEach; 6 | export const afterEach = window.afterEach; 7 | 8 | -------------------------------------------------------------------------------- /test/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | :root { 4 | --mocha-color: #000; 5 | --mocha-bg-color: #fff; 6 | --mocha-pass-icon-color: #00d6b2; 7 | --mocha-pass-color: #fff; 8 | --mocha-pass-shadow-color: rgba(0,0,0,.2); 9 | --mocha-pass-mediump-color: #c09853; 10 | --mocha-pass-slow-color: #b94a48; 11 | --mocha-test-pending-color: #0b97c4; 12 | --mocha-test-pending-icon-color: #0b97c4; 13 | --mocha-test-fail-color: #c00; 14 | --mocha-test-fail-icon-color: #c00; 15 | --mocha-test-fail-pre-color: #000; 16 | --mocha-test-fail-pre-error-color: #c00; 17 | --mocha-test-html-error-color: #000; 18 | --mocha-box-shadow-color: #eee; 19 | --mocha-box-bottom-color: #ddd; 20 | --mocha-test-replay-color: #888; 21 | --mocha-test-replay-bg-color: #eee; 22 | --mocha-stats-color: #888; 23 | --mocha-stats-em-color: #000; 24 | --mocha-stats-hover-color: #eee; 25 | --mocha-error-color: #c00; 26 | 27 | --mocha-code-comment: #ddd; 28 | --mocha-code-init: #2f6fad; 29 | --mocha-code-string: #5890ad; 30 | --mocha-code-keyword: #8a6343; 31 | --mocha-code-number: #2f6fad; 32 | } 33 | 34 | @media (prefers-color-scheme: dark) { 35 | :root { 36 | --mocha-color: #fff; 37 | --mocha-bg-color: #222; 38 | --mocha-pass-icon-color: #00d6b2; 39 | --mocha-pass-color: #222; 40 | --mocha-pass-shadow-color: rgba(255,255,255,.2); 41 | --mocha-pass-mediump-color: #f1be67; 42 | --mocha-pass-slow-color: #f49896; 43 | --mocha-test-pending-color: #0b97c4; 44 | --mocha-test-pending-icon-color: #0b97c4; 45 | --mocha-test-fail-color: #f44; 46 | --mocha-test-fail-icon-color: #f44; 47 | --mocha-test-fail-pre-color: #fff; 48 | --mocha-test-fail-pre-error-color: #f44; 49 | --mocha-test-html-error-color: #fff; 50 | --mocha-box-shadow-color: #444; 51 | --mocha-box-bottom-color: #555; 52 | --mocha-test-replay-color: #888; 53 | --mocha-test-replay-bg-color: #444; 54 | --mocha-stats-color: #aaa; 55 | --mocha-stats-em-color: #fff; 56 | --mocha-stats-hover-color: #444; 57 | --mocha-error-color: #f44; 58 | 59 | --mocha-code-comment: #ddd; 60 | --mocha-code-init: #9cc7f1; 61 | --mocha-code-string: #80d4ff; 62 | --mocha-code-keyword: #e3a470; 63 | --mocha-code-number: #4ca7ff; 64 | } 65 | } 66 | 67 | body { 68 | margin:0; 69 | background-color: var(--mocha-bg-color); 70 | color: var(--mocha-color); 71 | } 72 | 73 | #mocha { 74 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 75 | margin: 60px 50px; 76 | } 77 | 78 | #mocha ul, 79 | #mocha li { 80 | margin: 0; 81 | padding: 0; 82 | } 83 | 84 | #mocha ul { 85 | list-style: none; 86 | } 87 | 88 | #mocha h1, 89 | #mocha h2 { 90 | margin: 0; 91 | } 92 | 93 | #mocha h1 { 94 | margin-top: 15px; 95 | font-size: 1em; 96 | font-weight: 200; 97 | } 98 | 99 | #mocha h1 a { 100 | text-decoration: none; 101 | color: inherit; 102 | } 103 | 104 | #mocha h1 a:hover { 105 | text-decoration: underline; 106 | } 107 | 108 | #mocha .suite .suite h1 { 109 | margin-top: 0; 110 | font-size: .8em; 111 | } 112 | 113 | #mocha .hidden { 114 | display: none; 115 | } 116 | 117 | #mocha h2 { 118 | font-size: 12px; 119 | font-weight: normal; 120 | cursor: pointer; 121 | } 122 | 123 | #mocha .suite { 124 | margin-left: 15px; 125 | } 126 | 127 | #mocha .test { 128 | margin-left: 15px; 129 | overflow: hidden; 130 | } 131 | 132 | #mocha .test.pending:hover h2::after { 133 | content: '(pending)'; 134 | font-family: arial, sans-serif; 135 | } 136 | 137 | #mocha .test.pass.medium .duration { 138 | background: var(--mocha-pass-mediump-color); 139 | } 140 | 141 | #mocha .test.pass.slow .duration { 142 | background: var(--mocha-pass-slow-color); 143 | } 144 | 145 | #mocha .test.pass::before { 146 | content: '✓'; 147 | font-size: 12px; 148 | display: block; 149 | float: left; 150 | margin-right: 5px; 151 | color: var(--mocha-pass-icon-color); 152 | } 153 | 154 | #mocha .test.pass .duration { 155 | font-size: 9px; 156 | margin-left: 5px; 157 | padding: 2px 5px; 158 | color: var(--mocha-pass-color); 159 | -webkit-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color); 160 | -moz-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color); 161 | box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color); 162 | -webkit-border-radius: 5px; 163 | -moz-border-radius: 5px; 164 | -ms-border-radius: 5px; 165 | -o-border-radius: 5px; 166 | border-radius: 5px; 167 | } 168 | 169 | #mocha .test.pass.fast .duration { 170 | display: none; 171 | } 172 | 173 | #mocha .test.pending { 174 | color: var(--mocha-test-pending-color); 175 | } 176 | 177 | #mocha .test.pending::before { 178 | content: '◦'; 179 | color: var(--mocha-test-pending-icon-color); 180 | } 181 | 182 | #mocha .test.fail { 183 | color: var(--mocha-test-fail-color); 184 | } 185 | 186 | #mocha .test.fail pre { 187 | color: var(--mocha-test-fail-pre-color); 188 | } 189 | 190 | #mocha .test.fail::before { 191 | content: '✖'; 192 | font-size: 12px; 193 | display: block; 194 | float: left; 195 | margin-right: 5px; 196 | color: var(--mocha-pass-icon-color); 197 | } 198 | 199 | #mocha .test pre.error { 200 | color: var(--mocha-test-fail-pre-error-color); 201 | max-height: 300px; 202 | overflow: auto; 203 | } 204 | 205 | #mocha .test .html-error { 206 | overflow: auto; 207 | color: var(--mocha-test-html-error-color); 208 | display: block; 209 | float: left; 210 | clear: left; 211 | font: 12px/1.5 monaco, monospace; 212 | margin: 5px; 213 | padding: 15px; 214 | border: 1px solid var(--mocha-box-shadow-color); 215 | max-width: 85%; /*(1)*/ 216 | max-width: -webkit-calc(100% - 42px); 217 | max-width: -moz-calc(100% - 42px); 218 | max-width: calc(100% - 42px); /*(2)*/ 219 | max-height: 300px; 220 | word-wrap: break-word; 221 | border-bottom-color: var(--mocha-box-bottom-color); 222 | -webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 223 | -moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 224 | box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 225 | -webkit-border-radius: 3px; 226 | -moz-border-radius: 3px; 227 | border-radius: 3px; 228 | } 229 | 230 | #mocha .test .html-error pre.error { 231 | border: none; 232 | -webkit-border-radius: 0; 233 | -moz-border-radius: 0; 234 | border-radius: 0; 235 | -webkit-box-shadow: 0; 236 | -moz-box-shadow: 0; 237 | box-shadow: 0; 238 | padding: 0; 239 | margin: 0; 240 | margin-top: 18px; 241 | max-height: none; 242 | } 243 | 244 | /** 245 | * (1): approximate for browsers not supporting calc 246 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 247 | * ^^ seriously 248 | */ 249 | #mocha .test pre { 250 | display: block; 251 | float: left; 252 | clear: left; 253 | font: 12px/1.5 monaco, monospace; 254 | margin: 5px; 255 | padding: 15px; 256 | border: 1px solid var(--mocha-box-shadow-color); 257 | max-width: 85%; /*(1)*/ 258 | max-width: -webkit-calc(100% - 42px); 259 | max-width: -moz-calc(100% - 42px); 260 | max-width: calc(100% - 42px); /*(2)*/ 261 | word-wrap: break-word; 262 | border-bottom-color: var(--mocha-box-bottom-color); 263 | -webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 264 | -moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 265 | box-shadow: 0 1px 3px var(--mocha-box-shadow-color); 266 | -webkit-border-radius: 3px; 267 | -moz-border-radius: 3px; 268 | border-radius: 3px; 269 | } 270 | 271 | #mocha .test h2 { 272 | position: relative; 273 | } 274 | 275 | #mocha .test a.replay { 276 | position: absolute; 277 | top: 3px; 278 | right: 0; 279 | text-decoration: none; 280 | vertical-align: middle; 281 | display: block; 282 | width: 15px; 283 | height: 15px; 284 | line-height: 15px; 285 | text-align: center; 286 | background: var(--mocha-test-replay-bg-color); 287 | font-size: 15px; 288 | -webkit-border-radius: 15px; 289 | -moz-border-radius: 15px; 290 | border-radius: 15px; 291 | -webkit-transition:opacity 200ms; 292 | -moz-transition:opacity 200ms; 293 | -o-transition:opacity 200ms; 294 | transition: opacity 200ms; 295 | opacity: 0.3; 296 | color: var(--mocha-test-replay-color); 297 | } 298 | 299 | #mocha .test:hover a.replay { 300 | opacity: 1; 301 | } 302 | 303 | #mocha-report.pass .test.fail { 304 | display: none; 305 | } 306 | 307 | #mocha-report.fail .test.pass { 308 | display: none; 309 | } 310 | 311 | #mocha-report.pending .test.pass, 312 | #mocha-report.pending .test.fail { 313 | display: none; 314 | } 315 | #mocha-report.pending .test.pass.pending { 316 | display: block; 317 | } 318 | 319 | #mocha-error { 320 | color: var(--mocha-error-color); 321 | font-size: 1.5em; 322 | font-weight: 100; 323 | letter-spacing: 1px; 324 | } 325 | 326 | #mocha-stats { 327 | position: fixed; 328 | top: 15px; 329 | right: 10px; 330 | font-size: 12px; 331 | margin: 0; 332 | color: var(--mocha-stats-color); 333 | z-index: 1; 334 | } 335 | 336 | #mocha-stats .progress { 337 | float: right; 338 | padding-top: 0; 339 | 340 | /** 341 | * Set safe initial values, so mochas .progress does not inherit these 342 | * properties from Bootstrap .progress (which causes .progress height to 343 | * equal line height set in Bootstrap). 344 | */ 345 | height: auto; 346 | -webkit-box-shadow: none; 347 | -moz-box-shadow: none; 348 | box-shadow: none; 349 | background-color: initial; 350 | } 351 | 352 | #mocha-stats em { 353 | color: var(--mocha-stats-em-color); 354 | } 355 | 356 | #mocha-stats a { 357 | text-decoration: none; 358 | color: inherit; 359 | } 360 | 361 | #mocha-stats a:hover { 362 | border-bottom: 1px solid var(--mocha-stats-hover-color); 363 | } 364 | 365 | #mocha-stats li { 366 | display: inline-block; 367 | margin: 0 5px; 368 | list-style: none; 369 | padding-top: 11px; 370 | } 371 | 372 | #mocha-stats canvas { 373 | width: 40px; 374 | height: 40px; 375 | } 376 | 377 | #mocha code .comment { color: var(--mocha-code-comment); } 378 | #mocha code .init { color: var(--mocha-code-init); } 379 | #mocha code .string { color: var(--mocha-code-string); } 380 | #mocha code .keyword { color: var(--mocha-code-keyword); } 381 | #mocha code .number { color: var(--mocha-code-number); } 382 | 383 | @media screen and (max-device-width: 480px) { 384 | #mocha { 385 | margin: 60px 0px; 386 | } 387 | 388 | #mocha #stats { 389 | position: absolute; 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /test/puppeteer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import puppeteer from 'puppeteer'; 4 | import path from 'path'; 5 | import url from 'url'; 6 | import express from 'express'; 7 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); // eslint-disable-line 8 | const app = express(); 9 | const port = 3000; 10 | 11 | app.use(express.static(path.dirname(__dirname))); 12 | const server = app.listen(port, () => { 13 | console.log(`Example app listening on port ${port}!`); 14 | test(port); 15 | }); 16 | 17 | function makePromiseInfo() { 18 | const info = {}; 19 | const promise = new Promise((resolve, reject) => { 20 | Object.assign(info, {resolve, reject}); 21 | }); 22 | info.promise = promise; 23 | return info; 24 | } 25 | 26 | async function test(port) { 27 | const browser = await puppeteer.launch({ 28 | args: [ 29 | '--no-sandbox', 30 | '--disable-setuid-sandbox', 31 | ], 32 | }); 33 | const page = await browser.newPage(); 34 | 35 | page.on('console', async e => { 36 | const args = await Promise.all(e.args().map(a => a.jsonValue())); 37 | console.log(...args); 38 | }); 39 | 40 | let totalFailures = 0; 41 | let waitingPromiseInfo; 42 | 43 | page.on('domcontentloaded', async() => { 44 | const failures = await page.evaluate(() => { 45 | return window.testsPromiseInfo.promise; 46 | }); 47 | 48 | totalFailures += failures; 49 | if (failures) { 50 | console.error('FAILED'); 51 | } 52 | 53 | waitingPromiseInfo.resolve(); 54 | }); 55 | 56 | const testPages = [ 57 | {url: `http://localhost:${port}/test/index.html?reporter=spec` }, 58 | ]; 59 | 60 | for (const {url, js} of testPages) { 61 | waitingPromiseInfo = makePromiseInfo(); 62 | console.log(`===== [ ${url} ] =====`); 63 | if (js) { 64 | await page.evaluateOnNewDocument(js); 65 | } 66 | await page.goto(url); 67 | await page.waitForNetworkIdle(); 68 | if (js) { 69 | await page.evaluate(() => { 70 | setTimeout(() => { 71 | window.testsPromiseInfo.resolve(0); 72 | }, 10); 73 | }); 74 | } 75 | await waitingPromiseInfo.promise; 76 | } 77 | 78 | await browser.close(); 79 | server.close(); 80 | 81 | process.exit(totalFailures ? 1 : 0); // eslint-disable-line 82 | } 83 | -------------------------------------------------------------------------------- /test/tests/buffer-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext, createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('buffer tests', () => { 6 | it('test bufferData with typedarray', () => { 7 | const {gl} = createContext(); 8 | const tracker = new MemInfoTracker(gl, 'buffer'); 9 | 10 | const buf1 = gl.createBuffer(); 11 | tracker.addObjects(1); 12 | 13 | gl.bindBuffer(gl.ARRAY_BUFFER, buf1); 14 | const data1 = new Float32Array(25); 15 | gl.bufferData(gl.ARRAY_BUFFER, data1, gl.STATIC_DRAW); 16 | tracker.addMemory(data1.byteLength); 17 | 18 | const data1a = new Uint16Array(37); 19 | gl.bufferData(gl.ARRAY_BUFFER, data1a, gl.STATIC_DRAW); 20 | tracker.addMemory(data1a.byteLength - data1.byteLength); 21 | 22 | const buf2 = gl.createBuffer(); 23 | tracker.addObjects(1); 24 | 25 | gl.bindBuffer(gl.ARRAY_BUFFER, buf2); 26 | const data2 = new Float32Array(55); 27 | gl.bufferData(gl.ARRAY_BUFFER, data2, gl.STATIC_DRAW); 28 | tracker.addMemory(data2.byteLength); 29 | 30 | gl.deleteBuffer(buf1); 31 | tracker.deleteObjectAndMemory(data1a.byteLength); 32 | 33 | gl.deleteBuffer(buf2); 34 | tracker.deleteObjectAndMemory(data2.byteLength); 35 | }); 36 | 37 | it('test bufferData with ArrayBufferView', () => { 38 | const {gl} = createContext2(); 39 | if (!gl) { 40 | return; 41 | } 42 | const tracker = new MemInfoTracker(gl, 'buffer'); 43 | 44 | const buf1 = gl.createBuffer(); 45 | tracker.addObjects(1); 46 | 47 | gl.bindBuffer(gl.ARRAY_BUFFER, buf1); 48 | const data1 = new Float32Array(25); 49 | const length1 = 15; 50 | const size1 = length1 * data1.BYTES_PER_ELEMENT; 51 | gl.bufferData(gl.ARRAY_BUFFER, data1, gl.STATIC_DRAW, 0, length1); 52 | tracker.addMemory(size1); 53 | 54 | const data1a = new Uint16Array(37); 55 | const length1a = 30; 56 | const size1a = length1a * data1a.BYTES_PER_ELEMENT; 57 | gl.bufferData(gl.ARRAY_BUFFER, data1a, gl.STATIC_DRAW, 0, length1a); 58 | tracker.addMemory(size1a - size1); 59 | 60 | const buf2 = gl.createBuffer(); 61 | tracker.addObjects(1); 62 | 63 | gl.bindBuffer(gl.ARRAY_BUFFER, buf2); 64 | const data2 = new Float32Array(55); 65 | const length2 = 41; 66 | const size2 = length2 * data2.BYTES_PER_ELEMENT; 67 | gl.bufferData(gl.ARRAY_BUFFER, data2, gl.STATIC_DRAW, 0, length2); 68 | tracker.addMemory(size2); 69 | 70 | gl.deleteBuffer(buf1); 71 | tracker.deleteObjectAndMemory(size1a); 72 | 73 | gl.deleteBuffer(buf2); 74 | tracker.deleteObjectAndMemory(size2); 75 | }); 76 | 77 | it('test bufferData with ArrayBuffer', () => { 78 | const {gl} = createContext2(); 79 | if (!gl) { 80 | return; 81 | } 82 | const tracker = new MemInfoTracker(gl, 'buffer'); 83 | 84 | const buf1 = gl.createBuffer(); 85 | tracker.addObjects(1); 86 | 87 | gl.bindBuffer(gl.ARRAY_BUFFER, buf1); 88 | const data1 = new Float32Array(25); 89 | gl.bufferData(gl.ARRAY_BUFFER, data1.buffer, gl.STATIC_DRAW); 90 | tracker.addMemory(data1.byteLength); 91 | 92 | const data1a = new Uint16Array(37); 93 | gl.bufferData(gl.ARRAY_BUFFER, data1a.buffer, gl.STATIC_DRAW); 94 | tracker.addMemory(data1a.byteLength - data1.byteLength); 95 | 96 | const buf2 = gl.createBuffer(); 97 | tracker.addObjects(1); 98 | 99 | gl.bindBuffer(gl.ARRAY_BUFFER, buf2); 100 | const data2 = new Float32Array(55); 101 | gl.bufferData(gl.ARRAY_BUFFER, data2.buffer, gl.STATIC_DRAW); 102 | tracker.addMemory(data2.byteLength); 103 | 104 | gl.deleteBuffer(buf1); 105 | tracker.deleteObjectAndMemory(data1a.byteLength); 106 | 107 | gl.deleteBuffer(buf2); 108 | tracker.deleteObjectAndMemory(data2.byteLength); 109 | }); 110 | 111 | it('test bufferData with size', () => { 112 | const {gl} = createContext(); 113 | const tracker = new MemInfoTracker(gl, 'buffer'); 114 | 115 | const buf1 = gl.createBuffer(); 116 | tracker.addObjects(1); 117 | 118 | gl.bindBuffer(gl.ARRAY_BUFFER, buf1); 119 | const size1 = 26; 120 | gl.bufferData(gl.ARRAY_BUFFER, size1, gl.STATIC_DRAW); 121 | tracker.addMemory(size1); 122 | 123 | const size1a = 38; 124 | gl.bufferData(gl.ARRAY_BUFFER, size1a, gl.STATIC_DRAW); 125 | tracker.addMemory(size1a - size1); 126 | 127 | const buf2 = gl.createBuffer(); 128 | tracker.addObjects(1); 129 | 130 | gl.bindBuffer(gl.ARRAY_BUFFER, buf2); 131 | const size2 = 55; 132 | gl.bufferData(gl.ARRAY_BUFFER, size2, gl.STATIC_DRAW); 133 | tracker.addMemory(size2); 134 | 135 | gl.deleteBuffer(buf1); 136 | tracker.deleteObjectAndMemory(size1a); 137 | 138 | gl.deleteBuffer(buf2); 139 | tracker.deleteObjectAndMemory(size2); 140 | }); 141 | 142 | it('test ELEMENT_ARRAY_BUFFER', () => { 143 | const {gl} = createContext2(); 144 | if (!gl) { 145 | return; 146 | } 147 | const tracker = new MemInfoTracker(gl, 'buffer'); 148 | 149 | const va1 = gl.createVertexArray(); 150 | gl.bindVertexArray(va1); 151 | 152 | const buf1 = gl.createBuffer(); 153 | tracker.addObjects(1); 154 | 155 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf1); 156 | const size1 = 26; 157 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, size1, gl.STATIC_DRAW); 158 | tracker.addMemory(size1); 159 | 160 | const va2 = gl.createVertexArray(); 161 | gl.bindVertexArray(va2); 162 | 163 | const buf2 = gl.createBuffer(); 164 | tracker.addObjects(1); 165 | 166 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf2); 167 | const size2 = 55; 168 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, size2, gl.STATIC_DRAW); 169 | tracker.addMemory(size2); 170 | 171 | gl.bindVertexArray(va1); 172 | const size1a = 5; 173 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, size1a, gl.STATIC_DRAW); 174 | tracker.addMemory(size1a - size1); 175 | 176 | gl.deleteBuffer(buf1); 177 | tracker.deleteObjectAndMemory(size1a); 178 | 179 | gl.deleteBuffer(buf2); 180 | tracker.deleteObjectAndMemory(size2); 181 | }); 182 | 183 | it('test bindBufferBase/bindBufferRange', () => { 184 | const {gl} = createContext2(); 185 | if (!gl) { 186 | return; 187 | } 188 | const tracker = new MemInfoTracker(gl, 'buffer'); 189 | 190 | const buf1 = gl.createBuffer(); 191 | tracker.addObjects(1); 192 | 193 | gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buf1); 194 | const data1 = new Float32Array(25); 195 | gl.bufferData(gl.UNIFORM_BUFFER, data1, gl.STATIC_DRAW); 196 | tracker.addMemory(data1.byteLength); 197 | 198 | const data1a = new Uint16Array(37); 199 | gl.bufferData(gl.UNIFORM_BUFFER, data1a, gl.STATIC_DRAW); 200 | tracker.addMemory(data1a.byteLength - data1.byteLength); 201 | 202 | const buf2 = gl.createBuffer(); 203 | tracker.addObjects(1); 204 | 205 | const data2 = new Float32Array(55); 206 | gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf2, 0, data2.byteLength); 207 | gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, data2, gl.STATIC_DRAW); 208 | tracker.addMemory(data2.byteLength); 209 | 210 | gl.deleteBuffer(buf1); 211 | tracker.deleteObjectAndMemory(data1a.byteLength); 212 | 213 | gl.deleteBuffer(buf2); 214 | tracker.deleteObjectAndMemory(data2.byteLength); 215 | 216 | }); 217 | 218 | }); -------------------------------------------------------------------------------- /test/tests/contextloss-tests.js: -------------------------------------------------------------------------------- 1 | import { assertEqual, assertFalsy } from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {createContext} from '../webgl.js'; 4 | import {MemInfoTracker} from './test-utils.js'; 5 | 6 | function createExposedPromise() { 7 | const p = {}; 8 | p.promise = new Promise((resolve, reject) => { 9 | p.resolve = resolve; 10 | p.reject = reject; 11 | }); 12 | return p; 13 | } 14 | 15 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 16 | 17 | describe('webgl context lost tests', () => { 18 | 19 | async function testContextLost(gl) { 20 | const contextLostExposedPromise = createExposedPromise(); 21 | const contextRestoredExposedPromise = createExposedPromise(); 22 | 23 | const handleContextLost = e => { 24 | // To enable context restoration we must preventDefault on the context loss event: 25 | e.preventDefault(); 26 | contextLostExposedPromise.resolve(); 27 | }; 28 | const handleContextRestored = () => { 29 | contextRestoredExposedPromise.resolve(); 30 | }; 31 | 32 | gl.canvas.addEventListener('webglcontextlost', handleContextLost); 33 | gl.canvas.addEventListener('contextlost', handleContextLost); 34 | 35 | gl.canvas.addEventListener('webglcontextrestored', handleContextRestored); 36 | gl.canvas.addEventListener('contextrestored', handleContextRestored); 37 | 38 | const loseContextExt = gl.getExtension('WEBGL_lose_context'); 39 | const oesVertexArrayExt = gl.getExtension('OES_vertex_array_object'); 40 | if (!loseContextExt && !oesVertexArrayExt) { 41 | return; 42 | } 43 | const memExt = gl.getExtension('GMAN_webgl_memory'); 44 | 45 | const tracker = new MemInfoTracker(gl, 'texture'); 46 | 47 | // Add a texture 48 | const tex1 = gl.createTexture(); 49 | tracker.addObjects(1); 50 | gl.bindTexture(gl.TEXTURE_2D, tex1); 51 | const texSize = 32 * 16 * 4; 52 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 32, 16, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 53 | tracker.addMemory(texSize); 54 | 55 | const va = oesVertexArrayExt.createVertexArrayOES(); 56 | oesVertexArrayExt.bindVertexArrayOES(va); 57 | assertEqual(memExt.getMemoryInfo().resources.vertexArray, 1); 58 | 59 | // Force context loss and wait for event loop to complete 60 | loseContextExt.loseContext(); 61 | await contextLostExposedPromise.promise; 62 | await wait(); 63 | 64 | // Verify memory tracking was zeroed out 65 | tracker.deleteObjectAndMemory(texSize, 1); 66 | assertEqual(memExt.getMemoryInfo().resources.vertexArray, 0); 67 | 68 | // Check that calling functions while context is lost does not add memory 69 | gl.bindTexture(gl.TEXTURE_2D, tex1); 70 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 32, 16, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 71 | tracker.addMemory(0); 72 | 73 | oesVertexArrayExt.createVertexArrayOES(); 74 | assertEqual(memExt.getMemoryInfo().resources.vertexArray, 0); 75 | 76 | // Check that getting an extension while context lost returns null? 77 | // Actually not entirely sure what's supposed to happen here. 78 | const ext = gl.getExtension('OES_vertex_array_object'); 79 | assertFalsy(ext); 80 | 81 | // Force context restoration and wait for event loop to complete 82 | loseContextExt.restoreContext(); 83 | 84 | await contextRestoredExposedPromise.promise; 85 | await wait(); 86 | 87 | // Verify you can still add new things 88 | const tex2 = gl.createTexture(); 89 | tracker.addObjects(1); 90 | gl.bindTexture(gl.TEXTURE_2D, tex2); 91 | const texSize2 = 64 * 24 * 4; 92 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 64, 24, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 93 | tracker.addMemory(texSize2); 94 | 95 | // Check that old extensions do not add new info 96 | oesVertexArrayExt.createVertexArrayOES(); 97 | assertEqual(memExt.getMemoryInfo().resources.vertexArray, 0); 98 | 99 | // Check that new extensions do add new info 100 | const newOESVertexArrayExt = gl.getExtension('OES_vertex_array_object'); 101 | if (!newOESVertexArrayExt) { 102 | return; 103 | } 104 | 105 | newOESVertexArrayExt.createVertexArrayOES(); 106 | assertEqual(memExt.getMemoryInfo().resources.vertexArray, 1); 107 | } 108 | 109 | it('test context loss', async() => { 110 | const {gl} = createContext(); 111 | await testContextLost(gl); 112 | }); 113 | 114 | it('test context loss OffscreenCanvas', async() => { 115 | if (typeof OffscreenCanvas === 'undefined') { 116 | return; 117 | } 118 | 119 | const gl = new OffscreenCanvas(300, 150).getContext('webgl'); 120 | await testContextLost(gl); 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /test/tests/info-tests.js: -------------------------------------------------------------------------------- 1 | import { computeDrawingbufferSize, getDrawingbufferInfo } from '../../src/utils.js'; 2 | import {assertEqual, assertNotEqual, assertTruthy} from '../assert.js'; 3 | import {describe, it} from '../mocha-support.js'; 4 | import {createContext, createContext2} from '../webgl.js'; 5 | 6 | describe('info tests', () => { 7 | it('test base state webgl1', () => { 8 | const {gl, ext, drawingbufferSize} = createContext(); 9 | assertTruthy(ext, 'got extension'); 10 | 11 | // compute this size ourselves so we can compare 12 | const size = gl.drawingBufferWidth * gl.drawingBufferHeight; 13 | const colorSize = 4; 14 | const samples = gl.getParameter(gl.SAMPLES) || 1; 15 | const depthStencilBytes = (gl.getParameter(gl.DEPTH_BITS) + gl.getParameter(gl.STENCIL_BITS) + 7) / 8 | 0; 16 | const depthStencilSize = depthStencilBytes === 3 ? 4 : depthStencilBytes; 17 | const canvasSize = 18 | size * colorSize + // display buffer 19 | size * colorSize * samples + // drawing buffer 20 | size * depthStencilSize; // depth + stencil buffer 21 | 22 | assertEqual(drawingbufferSize, canvasSize); 23 | 24 | const info = ext.getMemoryInfo(); 25 | const {memory, resources} = info; 26 | 27 | assertEqual(memory.buffer, 0); 28 | assertEqual(memory.texture, 0); 29 | assertEqual(memory.renderbuffer, 0); 30 | assertEqual(memory.drawingbuffer, drawingbufferSize); 31 | assertEqual(memory.total, drawingbufferSize); 32 | assertEqual(resources.buffer, 0); 33 | assertEqual(resources.framebuffer, 0); 34 | assertEqual(resources.renderbuffer, 0); 35 | assertEqual(resources.program, 0); 36 | assertEqual(resources.query, undefined); 37 | assertEqual(resources.sampler, undefined); 38 | assertEqual(resources.shader, 0); 39 | assertEqual(resources.sync, undefined); 40 | assertEqual(resources.texture, 0); 41 | assertEqual(resources.transformFeedback, undefined); 42 | assertEqual(resources.vertexArray, undefined); 43 | 44 | const textures = ext.getResourcesInfo(WebGLTexture); 45 | assertEqual(textures.length, 0); 46 | }); 47 | 48 | it('test base state webgl2', () => { 49 | const {ext, drawingbufferSize} = createContext2(); 50 | assertTruthy(ext, 'got extension'); 51 | 52 | const info = ext.getMemoryInfo(); 53 | const {memory, resources} = info; 54 | 55 | assertEqual(memory.buffer, 0); 56 | assertEqual(memory.texture, 0); 57 | assertEqual(memory.renderbuffer, 0); 58 | assertEqual(memory.drawingbuffer, drawingbufferSize); 59 | assertEqual(memory.total, drawingbufferSize); 60 | assertEqual(resources.buffer, 0); 61 | assertEqual(resources.framebuffer, 0); 62 | assertEqual(resources.renderbuffer, 0); 63 | assertEqual(resources.program, 0); 64 | assertEqual(resources.query, 0); 65 | assertEqual(resources.sampler, 0); 66 | assertEqual(resources.shader, 0); 67 | assertEqual(resources.sync, 0); 68 | assertEqual(resources.texture, 0); 69 | assertEqual(resources.transformFeedback, 0); 70 | assertEqual(resources.vertexArray, 0); 71 | 72 | const textures = ext.getResourcesInfo(WebGLTexture); 73 | assertEqual(textures.length, 0); 74 | }); 75 | 76 | it('test canvas resize', () => { 77 | const {gl, ext, drawingbufferSize} = createContext(); 78 | assertTruthy(ext, 'got extension'); 79 | 80 | { 81 | const info = ext.getMemoryInfo(); 82 | const {memory} = info; 83 | assertEqual(memory.drawingbuffer, drawingbufferSize); 84 | assertEqual(memory.total, drawingbufferSize); 85 | } 86 | 87 | gl.canvas.width = 150; 88 | gl.canvas.height = 75; 89 | 90 | { 91 | const newDrawingbufferSize = computeDrawingbufferSize(gl, getDrawingbufferInfo(gl)); 92 | const info = ext.getMemoryInfo(); 93 | const {memory} = info; 94 | assertEqual(memory.drawingbuffer, newDrawingbufferSize); 95 | assertEqual(memory.total, newDrawingbufferSize); 96 | assertNotEqual(drawingbufferSize, newDrawingbufferSize); 97 | } 98 | 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /test/tests/program-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('program tests', () => { 6 | 7 | it('test program', () => { 8 | const {gl} = createContext(); 9 | if (!gl) { 10 | return; 11 | } 12 | const tracker = new MemInfoTracker(gl, 'program'); 13 | const prg = gl.createProgram(); 14 | tracker.addObjects(1); 15 | gl.deleteProgram(prg); 16 | tracker.deleteObjectAndMemory(0); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /test/tests/renderbuffer-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext, createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('renderbuffer tests', () => { 6 | 7 | it('test renderbufferStorage', () => { 8 | const {gl} = createContext(); 9 | const tracker = new MemInfoTracker(gl, 'renderbuffer'); 10 | 11 | const rb1 = gl.createRenderbuffer(); 12 | tracker.addObjects(1); 13 | 14 | gl.bindRenderbuffer(gl.RENDERBUFFER, rb1); 15 | let size1a; 16 | { 17 | const width = 17; 18 | const height = 49; 19 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, width, height); 20 | size1a = width * height * 2; 21 | tracker.addMemory(size1a); 22 | } 23 | 24 | let size1b; 25 | { 26 | const width = 37; 27 | const height = 9; 28 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); 29 | size1b = width * height * 2; 30 | tracker.addMemory(size1b - size1a); 31 | } 32 | 33 | const rb2 = gl.createRenderbuffer(); 34 | tracker.addObjects(1); 35 | 36 | gl.bindRenderbuffer(gl.RENDERBUFFER, rb2); 37 | let size2; 38 | { 39 | const width = 71; 40 | const height = 94; 41 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, width, height); 42 | size2 = width * height * 2; 43 | tracker.addMemory(size2); 44 | } 45 | 46 | gl.deleteRenderbuffer(rb1); 47 | tracker.deleteObjectAndMemory(size1b); 48 | gl.deleteRenderbuffer(rb2); 49 | tracker.deleteObjectAndMemory(size2); 50 | }); 51 | 52 | it('test renderbufferStorage STENCIL_INDEX8', () => { 53 | const {gl} = createContext(); 54 | const tracker = new MemInfoTracker(gl, 'renderbuffer'); 55 | 56 | const rb1 = gl.createRenderbuffer(); 57 | tracker.addObjects(1); 58 | 59 | gl.bindRenderbuffer(gl.RENDERBUFFER, rb1); 60 | let size1a; 61 | { 62 | const width = 17; 63 | const height = 49; 64 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, width, height); 65 | size1a = width * height; 66 | tracker.addMemory(size1a); 67 | } 68 | 69 | gl.deleteRenderbuffer(rb1); 70 | tracker.deleteObjectAndMemory(size1a); 71 | }); 72 | 73 | 74 | it('test renderbufferStorageMultisample', () => { 75 | const {gl} = createContext2(); 76 | if (!gl) { 77 | return; 78 | } 79 | const tracker = new MemInfoTracker(gl, 'renderbuffer'); 80 | 81 | const rb1 = gl.createRenderbuffer(); 82 | tracker.addObjects(1); 83 | 84 | gl.bindRenderbuffer(gl.RENDERBUFFER, rb1); 85 | let size1a; 86 | { 87 | const width = 17; 88 | const height = 49; 89 | gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 2, gl.RGB565, width, height); 90 | size1a = width * height * 2 * 2; 91 | tracker.addMemory(size1a); 92 | } 93 | 94 | let size1b; 95 | { 96 | const width = 37; 97 | const height = 9; 98 | gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.DEPTH_COMPONENT16, width, height); 99 | size1b = width * height * 2 * 4; 100 | tracker.addMemory(size1b - size1a); 101 | } 102 | 103 | const rb2 = gl.createRenderbuffer(); 104 | tracker.addObjects(1); 105 | 106 | gl.bindRenderbuffer(gl.RENDERBUFFER, rb2); 107 | let size2; 108 | { 109 | const width = 71; 110 | const height = 94; 111 | gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 2, gl.RGBA4, width, height); 112 | size2 = width * height * 2 * 2; 113 | tracker.addMemory(size2); 114 | } 115 | 116 | gl.deleteRenderbuffer(rb1); 117 | tracker.deleteObjectAndMemory(size1b); 118 | gl.deleteRenderbuffer(rb2); 119 | tracker.deleteObjectAndMemory(size2); 120 | }); 121 | }); -------------------------------------------------------------------------------- /test/tests/sampler-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('sampler tests', () => { 6 | 7 | it('test sampler', () => { 8 | const {gl} = createContext2(); 9 | if (!gl) { 10 | return; 11 | } 12 | const tracker = new MemInfoTracker(gl, 'sampler'); 13 | const s = gl.createSampler(); 14 | tracker.addObjects(1); 15 | gl.deleteSampler(s); 16 | tracker.deleteObjectAndMemory(0); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /test/tests/shader-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('shader tests', () => { 6 | 7 | it('test shader', () => { 8 | const {gl} = createContext(); 9 | if (!gl) { 10 | return; 11 | } 12 | const tracker = new MemInfoTracker(gl, 'shader'); 13 | const sh = gl.createShader(gl.VERTEX_SHADER); 14 | tracker.addObjects(1); 15 | gl.deleteShader(sh); 16 | tracker.deleteObjectAndMemory(0); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /test/tests/stack-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {assertEqual, assertFalsy, assertTruthy} from '../assert.js'; 3 | import {createContext} from '../webgl.js'; 4 | 5 | describe('stack tests', () => { 6 | 7 | it('test texture stack capture', () => { 8 | const {gl, ext} = createContext(); 9 | 10 | const tex1 = gl.createTexture(); 11 | 12 | gl.bindTexture(gl.TEXTURE_2D, tex1); 13 | gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA, 16, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 14 | 15 | { 16 | const textures = ext.getResourcesInfo(WebGLTexture); 17 | assertEqual(textures.length, 1); 18 | assertTruthy(textures[0].stackCreated); 19 | assertTruthy(textures[0].stackUpdated); 20 | } 21 | 22 | gl.deleteTexture(tex1); 23 | 24 | { 25 | const textures = ext.getResourcesInfo(WebGLTexture); 26 | assertEqual(textures.length, 0); 27 | } 28 | }); 29 | 30 | it('test buffers stack capture', () => { 31 | const {gl, ext} = createContext(); 32 | 33 | const buf = gl.createBuffer(); 34 | 35 | { 36 | const buffers = ext.getResourcesInfo(WebGLBuffer); 37 | assertEqual(buffers.length, 1); 38 | assertTruthy(buffers[0].stackCreated); 39 | assertFalsy(buffers[0].stackUpdated); 40 | } 41 | 42 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 43 | gl.bufferData(gl.ARRAY_BUFFER, 16, gl.STATIC_DRAW); 44 | 45 | { 46 | const buffers = ext.getResourcesInfo(WebGLBuffer); 47 | assertEqual(buffers.length, 1); 48 | assertTruthy(buffers[0].stackCreated); 49 | assertTruthy(buffers[0].stackUpdated); 50 | } 51 | 52 | gl.deleteBuffer(buf); 53 | 54 | { 55 | const buffers = ext.getResourcesInfo(WebGLBuffer); 56 | assertEqual(buffers.length, 0); 57 | } 58 | }); 59 | 60 | it('test program stack capture', () => { 61 | const {gl, ext} = createContext(); 62 | 63 | const program = gl.createProgram(); 64 | 65 | { 66 | const programs = ext.getResourcesInfo(WebGLProgram); 67 | assertEqual(programs.length, 1); 68 | assertTruthy(programs[0].stackCreated); 69 | } 70 | 71 | gl.deleteProgram(program); 72 | 73 | { 74 | const programs = ext.getResourcesInfo(WebGLProgram); 75 | assertEqual(programs.length, 0); 76 | } 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /test/tests/sync-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('sync tests', () => { 6 | 7 | it('test sync', () => { 8 | const {gl} = createContext2(); 9 | if (!gl) { 10 | return; 11 | } 12 | const tracker = new MemInfoTracker(gl, 'sync'); 13 | const s = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); 14 | tracker.addObjects(1); 15 | gl.deleteSync(s); 16 | tracker.deleteObjectAndMemory(0); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /test/tests/test-utils.js: -------------------------------------------------------------------------------- 1 | import {assertEqual, assertTruthy} from '../assert.js'; 2 | import {computeDrawingbufferSize, getDrawingbufferInfo} from '../../src/utils.js'; 3 | 4 | export class MemInfoTracker { 5 | constructor(gl, type) { 6 | assertTruthy(gl); 7 | this.ext = gl.getExtension('GMAN_webgl_memory'); 8 | assertTruthy(this.ext); 9 | this.gl = gl; 10 | this.numObjects = 0; 11 | this.memSize = 0; 12 | this.type = type; 13 | } 14 | check() { 15 | const {gl} = this; 16 | const drawingbufferSize = computeDrawingbufferSize(gl, getDrawingbufferInfo(gl)); 17 | const {ext, type, memSize, numObjects} = this; 18 | const {memory, resources} = ext.getMemoryInfo(); 19 | if (memory[type] !== undefined) { 20 | assertEqual(memory[type], memSize, `memory.${type}`); 21 | } 22 | assertEqual(resources[type], numObjects, `resources.${type}`); 23 | assertEqual(memory.total, drawingbufferSize + memSize, `total`); 24 | } 25 | addObjects(deltaObjects = 1) { 26 | this.numObjects += deltaObjects; 27 | this.check(); 28 | } 29 | addMemory(deltaMemory = 0) { 30 | this.memSize += deltaMemory; 31 | this.check(); 32 | } 33 | deleteObjectAndMemory(deltaMemory, deltaObjects = 1) { 34 | this.memSize -= deltaMemory; 35 | this.numObjects -= deltaObjects; 36 | this.check(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/tests/texture-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext, createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('tex-image tests', () => { 6 | 7 | it('test texImage2D', () => { 8 | const {gl} = createContext(); 9 | const tracker = new MemInfoTracker(gl, 'texture'); 10 | 11 | const tex1 = gl.createTexture(); 12 | tracker.addObjects(1); 13 | 14 | gl.bindTexture(gl.TEXTURE_2D, tex1); 15 | gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA, 16, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 16 | const mip1Size = 16 * 8 * 4; 17 | tracker.addMemory(mip1Size); 18 | 19 | const mip0Size = 32 * 16 * 4; 20 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 32, 16, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 21 | tracker.addMemory(mip0Size); 22 | 23 | gl.generateMipmap(gl.TEXTURE_2D); 24 | const texSize = 4 * ( 25 | 32 * 16 + // level0 26 | 16 * 8 + // level1 27 | 8 * 4 + // level2 28 | 4 * 2 + // level3 29 | 2 * 1 + // level4 30 | 1 * 1); // level5 31 | tracker.addMemory(texSize - mip0Size - mip1Size); 32 | 33 | gl.deleteTexture(tex1); 34 | tracker.deleteObjectAndMemory(texSize); 35 | }); 36 | 37 | it('test texImage2D cube map', () => { 38 | const {gl} = createContext(); 39 | const tracker = new MemInfoTracker(gl, 'texture'); 40 | 41 | const tex1 = gl.createTexture(); 42 | tracker.addObjects(1); 43 | 44 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex1); 45 | 46 | const faces = [ 47 | gl.TEXTURE_CUBE_MAP_POSITIVE_X, 48 | gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 49 | gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 50 | gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 51 | gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 52 | gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 53 | ]; 54 | 55 | const mip0Size = 32 * 32 * 4; 56 | for (const face of faces) { 57 | gl.texImage2D(face, 0, gl.RGBA, 32, 32, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 58 | tracker.addMemory(mip0Size); 59 | } 60 | const level0Size = mip0Size * 6; 61 | 62 | gl.generateMipmap(gl.TEXTURE_CUBE_MAP); 63 | const faceSize = 4 * ( 64 | 32 * 32 + // level0 65 | 16 * 16 + // level1 66 | 8 * 8 + // level2 67 | 4 * 4 + // level3 68 | 2 * 2 + // level4 69 | 1 * 1); // level5 70 | tracker.addMemory(faceSize * 6 - level0Size); 71 | 72 | gl.deleteTexture(tex1); 73 | tracker.deleteObjectAndMemory(faceSize * 6); 74 | }); 75 | 76 | it('test compressedTexImage2D', () => { 77 | const {gl} = createContext(); 78 | const ext = gl.getExtension('WEBGL_compressed_texture_s3tc'); 79 | if (!ext) { 80 | return; 81 | } 82 | const tracker = new MemInfoTracker(gl, 'texture'); 83 | 84 | const tex1 = gl.createTexture(); 85 | tracker.addObjects(1); 86 | 87 | gl.bindTexture(gl.TEXTURE_2D, tex1); 88 | const mip0Size = (12 / 4) * (12 / 4) * 8; 89 | gl.compressedTexImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RGB_S3TC_DXT1_EXT, 12, 12, 0, new Uint8Array(mip0Size)); 90 | tracker.addMemory(mip0Size); 91 | 92 | gl.deleteTexture(tex1); 93 | tracker.deleteObjectAndMemory(mip0Size); 94 | }); 95 | 96 | /* 97 | it('test compressedTexImage3D', () => { 98 | const {gl} = createContext2(); 99 | const ext = gl.getExtension('WEBGL_compressed_texture_s3tc'); 100 | if (!ext) { 101 | return; 102 | } 103 | const tracker = new MemInfoTracker(gl, 'texture'); 104 | 105 | const tex1 = gl.createTexture(); 106 | tracker.addObjects(1); 107 | 108 | gl.bindTexture(gl.TEXTURE_3D, tex1); 109 | const mip0Size = (12 / 4) * (12 / 4) * 8 * 6; 110 | gl.compressedTexImage3D(gl.TEXTURE_3D, 0, ext.COMPRESSED_RGB_S3TC_DXT1_EXT, 12, 12, 6, 0, new Uint8Array(mip0Size)); 111 | tracker.addMemory(mip0Size); 112 | 113 | gl.deleteTexture(tex1); 114 | tracker.deleteObjectAndMemory(mip0Size); 115 | }); 116 | */ 117 | 118 | it('test OES_texture_float', () => { 119 | const {gl} = createContext(); 120 | const ext = gl.getExtension('OES_texture_float'); 121 | if (!ext) { 122 | return; 123 | } 124 | const tracker = new MemInfoTracker(gl, 'texture'); 125 | 126 | const tex1 = gl.createTexture(); 127 | tracker.addObjects(1); 128 | 129 | gl.bindTexture(gl.TEXTURE_2D, tex1); 130 | gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA, 16, 8, 0, gl.RGBA, gl.FLOAT, null); 131 | const mip1Size = 16 * 8 * 16; 132 | tracker.addMemory(mip1Size); 133 | 134 | const mip0Size = 32 * 16 * 16; 135 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 32, 16, 0, gl.RGBA, gl.FLOAT, null); 136 | tracker.addMemory(mip0Size); 137 | 138 | gl.deleteTexture(tex1); 139 | tracker.deleteObjectAndMemory(mip1Size + mip0Size); 140 | }); 141 | 142 | it('test texImage3D', () => { 143 | const {gl} = createContext2(); 144 | if (!gl) { 145 | return; 146 | } 147 | const tracker = new MemInfoTracker(gl, 'texture'); 148 | 149 | const tex1 = gl.createTexture(); 150 | tracker.addObjects(1); 151 | 152 | gl.bindTexture(gl.TEXTURE_3D, tex1); 153 | gl.texImage3D(gl.TEXTURE_3D, 1, gl.RGBA, 10, 20, 5, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 154 | const mip1Size = 10 * 20 * 5 * 4; 155 | tracker.addMemory(mip1Size); 156 | 157 | const mip0Size = 30 * 15 * 6 * 4; 158 | gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA, 30, 15, 6, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 159 | tracker.addMemory(mip0Size); 160 | 161 | gl.generateMipmap(gl.TEXTURE_3D); 162 | const texSize = 4 * ( 163 | 30 * 15 * 6 + // level0 164 | 15 * 8 * 3 + // level1 165 | 8 * 4 * 2 + // level2 166 | 4 * 2 * 1 + // level3 167 | 2 * 1 * 1 + // level4 168 | 1 * 1 * 1); // level5 169 | tracker.addMemory(texSize - mip0Size - mip1Size); 170 | 171 | gl.deleteTexture(tex1); 172 | tracker.deleteObjectAndMemory(texSize); 173 | }); 174 | 175 | it('test texStorage2D', () => { 176 | const {gl} = createContext2(); 177 | if (!gl) { 178 | return; 179 | } 180 | const tracker = new MemInfoTracker(gl, 'texture'); 181 | 182 | const tex1 = gl.createTexture(); 183 | tracker.addObjects(1); 184 | 185 | gl.bindTexture(gl.TEXTURE_2D, tex1); 186 | gl.texStorage2D(gl.TEXTURE_2D, 4, gl.RGB16I, 10, 20); 187 | const size = 6 * ( 188 | 10 * 20 + // level0 189 | 5 * 10 + // level1 190 | 3 * 5 + // level2 191 | 2 * 3 // level3 192 | ); 193 | tracker.addMemory(size); 194 | 195 | gl.deleteTexture(tex1); 196 | tracker.deleteObjectAndMemory(size); 197 | }); 198 | 199 | it('test texStorage3D', () => { 200 | const {gl} = createContext2(); 201 | if (!gl) { 202 | return; 203 | } 204 | const tracker = new MemInfoTracker(gl, 'texture'); 205 | 206 | const tex1 = gl.createTexture(); 207 | tracker.addObjects(1); 208 | 209 | gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex1); 210 | gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 4, gl.RGB16I, 10, 20, 6); 211 | const size = 6 * ( 212 | 10 * 20 * 6 + // level0 213 | 5 * 10 * 6 + // level1 214 | 3 * 5 * 6 + // level2 215 | 2 * 3 * 6 // level3 216 | ); 217 | tracker.addMemory(size); 218 | 219 | gl.deleteTexture(tex1); 220 | tracker.deleteObjectAndMemory(size); 221 | }); 222 | 223 | it('test copyTexImage2D', () => { 224 | const {gl} = createContext(); 225 | const tracker = new MemInfoTracker(gl, 'texture'); 226 | 227 | const tex1 = gl.createTexture(); 228 | tracker.addObjects(1); 229 | 230 | gl.bindTexture(gl.TEXTURE_2D, tex1); 231 | gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, 15, 25, 0); 232 | const mip0Size = 15 * 25 * 4; 233 | tracker.addMemory(mip0Size); 234 | 235 | gl.deleteTexture(tex1); 236 | tracker.deleteObjectAndMemory(mip0Size); 237 | }); 238 | 239 | it('test TEXTURE_BASE_LEVEL, TEXTURE_MAX_LEVEL', () => { 240 | const {gl} = createContext2(); 241 | if (!gl) { 242 | return; 243 | } 244 | const tracker = new MemInfoTracker(gl, 'texture'); 245 | 246 | const tex1 = gl.createTexture(); 247 | tracker.addObjects(1); 248 | 249 | gl.bindTexture(gl.TEXTURE_2D, tex1); 250 | gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA, 16, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 251 | const mip1Size = 16 * 8 * 4; 252 | tracker.addMemory(mip1Size); 253 | 254 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 1); 255 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 3); 256 | gl.generateMipmap(gl.TEXTURE_2D); 257 | 258 | const texSize = 4 * ( 259 | 16 * 8 + // level1 260 | 8 * 4 + // level2 261 | 4 * 2); // level3 262 | tracker.addMemory(texSize - mip1Size); 263 | 264 | gl.deleteTexture(tex1); 265 | tracker.deleteObjectAndMemory(texSize); 266 | }); 267 | 268 | }); -------------------------------------------------------------------------------- /test/tests/transformfeedback-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('transformfeedback tests', () => { 6 | 7 | it('test transformfeedback', () => { 8 | const {gl} = createContext2(); 9 | if (!gl) { 10 | return; 11 | } 12 | const tracker = new MemInfoTracker(gl, 'transformFeedback'); 13 | const tf = gl.createTransformFeedback(); 14 | tracker.addObjects(1); 15 | gl.deleteTransformFeedback(tf); 16 | tracker.deleteObjectAndMemory(0); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /test/tests/vertexarray-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import {createContext, createContext2} from '../webgl.js'; 3 | import {MemInfoTracker} from './test-utils.js'; 4 | 5 | describe('vertex-array tests', () => { 6 | 7 | it('test vertex-array WebGL1', () => { 8 | const {gl} = createContext(); 9 | const ext = gl.getExtension('OES_vertex_array_object'); 10 | if (!ext) { 11 | return; 12 | } 13 | const tracker = new MemInfoTracker(gl, 'vertexArray'); 14 | const va = ext.createVertexArrayOES(); 15 | tracker.addObjects(1); 16 | ext.deleteVertexArrayOES(va); 17 | tracker.deleteObjectAndMemory(0); 18 | }); 19 | 20 | it('test vertex-array WebGL2', () => { 21 | const {gl} = createContext2(); 22 | if (!gl) { 23 | return; 24 | } 25 | const tracker = new MemInfoTracker(gl, 'vertexArray'); 26 | const va = gl.createVertexArray(); 27 | tracker.addObjects(1); 28 | gl.deleteVertexArray(va); 29 | tracker.deleteObjectAndMemory(0); 30 | }); 31 | 32 | }); -------------------------------------------------------------------------------- /test/webgl.js: -------------------------------------------------------------------------------- 1 | import { 2 | computeDrawingbufferSize, 3 | getDrawingbufferInfo, 4 | } from '../src/utils.js'; 5 | 6 | export function createContext() { 7 | const gl = document.createElement('canvas').getContext('webgl'); 8 | const ext = gl.getExtension('GMAN_webgl_memory'); 9 | return { gl, ext, drawingbufferSize: computeDrawingbufferSize(gl, getDrawingbufferInfo(gl)) }; 10 | } 11 | 12 | export function createContext2() { 13 | const gl = document.createElement('canvas').getContext('webgl2'); 14 | const ext = gl ? gl.getExtension('GMAN_webgl_memory') : null; 15 | return { gl, ext, drawingbufferSize: computeDrawingbufferSize(gl, getDrawingbufferInfo(gl)) }; 16 | } 17 | 18 | function resetContext(gl) { 19 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 20 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); 21 | gl.bindRenderbuffer(gl.RENDERBUFFER, null); 22 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 23 | gl.activeTexture(gl.TEXTURE0); 24 | gl.bindTexture(gl.TEXTURE_2D, null); 25 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); 26 | gl.useProgram(null); 27 | } 28 | 29 | export function resetContexts(context) { 30 | const { gl, gl2, vaoExt } = context; 31 | if (vaoExt) { 32 | vaoExt.bindVertexArrayOES(null); 33 | } 34 | resetContext(gl); 35 | 36 | if (gl2) { 37 | gl2.bindVertexArray(null); 38 | resetContext(gl2); 39 | } 40 | } 41 | 42 | export function escapeRE(str) { 43 | return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); 44 | } 45 | 46 | export function not(str) { 47 | return new RegExp(`^((?!${escapeRE(str)}).)*$`); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /webgl-memory.js: -------------------------------------------------------------------------------- 1 | /* webgl-memory@1.1.2, license MIT */ 2 | (function (factory) { 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | factory(); 5 | })((function () { 'use strict'; 6 | 7 | /* PixelFormat */ 8 | const ALPHA = 0x1906; 9 | const RGB = 0x1907; 10 | const RGBA = 0x1908; 11 | const LUMINANCE = 0x1909; 12 | const LUMINANCE_ALPHA = 0x190A; 13 | const DEPTH_COMPONENT = 0x1902; 14 | const DEPTH_STENCIL = 0x84F9; 15 | 16 | const R8 = 0x8229; 17 | const R8_SNORM = 0x8F94; 18 | const R16F = 0x822D; 19 | const R32F = 0x822E; 20 | const R8UI = 0x8232; 21 | const R8I = 0x8231; 22 | const RG16UI = 0x823A; 23 | const RG16I = 0x8239; 24 | const RG32UI = 0x823C; 25 | const RG32I = 0x823B; 26 | const RG8 = 0x822B; 27 | const RG8_SNORM = 0x8F95; 28 | const RG16F = 0x822F; 29 | const RG32F = 0x8230; 30 | const RG8UI = 0x8238; 31 | const RG8I = 0x8237; 32 | const R16UI = 0x8234; 33 | const R16I = 0x8233; 34 | const R32UI = 0x8236; 35 | const R32I = 0x8235; 36 | const RGB8 = 0x8051; 37 | const SRGB8 = 0x8C41; 38 | const RGB565 = 0x8D62; 39 | const RGB8_SNORM = 0x8F96; 40 | const R11F_G11F_B10F = 0x8C3A; 41 | const RGB9_E5 = 0x8C3D; 42 | const RGB16F = 0x881B; 43 | const RGB32F = 0x8815; 44 | const RGB8UI = 0x8D7D; 45 | const RGB8I = 0x8D8F; 46 | const RGB16UI = 0x8D77; 47 | const RGB16I = 0x8D89; 48 | const RGB32UI = 0x8D71; 49 | const RGB32I = 0x8D83; 50 | const RGBA8 = 0x8058; 51 | const SRGB8_ALPHA8 = 0x8C43; 52 | const RGBA8_SNORM = 0x8F97; 53 | const RGB5_A1 = 0x8057; 54 | const RGBA4 = 0x8056; 55 | const RGB10_A2 = 0x8059; 56 | const RGBA16F = 0x881A; 57 | const RGBA32F = 0x8814; 58 | const RGBA8UI = 0x8D7C; 59 | const RGBA8I = 0x8D8E; 60 | const RGB10_A2UI = 0x906F; 61 | const RGBA16UI = 0x8D76; 62 | const RGBA16I = 0x8D88; 63 | const RGBA32I = 0x8D82; 64 | const RGBA32UI = 0x8D70; 65 | 66 | const DEPTH_COMPONENT16 = 0x81A5; 67 | const DEPTH_COMPONENT24 = 0x81A6; 68 | const DEPTH_COMPONENT32F = 0x8CAC; 69 | const DEPTH32F_STENCIL8 = 0x8CAD; 70 | const DEPTH24_STENCIL8 = 0x88F0; 71 | const STENCIL_INDEX8 = 0x8d48; 72 | 73 | /* DataType */ 74 | // const BYTE = 0x1400; 75 | const UNSIGNED_BYTE = 0x1401; 76 | // const SHORT = 0x1402; 77 | const UNSIGNED_SHORT = 0x1403; 78 | // const INT = 0x1404; 79 | const UNSIGNED_INT = 0x1405; 80 | const FLOAT = 0x1406; 81 | const UNSIGNED_SHORT_4_4_4_4 = 0x8033; 82 | const UNSIGNED_SHORT_5_5_5_1 = 0x8034; 83 | const UNSIGNED_SHORT_5_6_5 = 0x8363; 84 | const HALF_FLOAT = 0x140B; 85 | const HALF_FLOAT_OES = 0x8D61; // Thanks Khronos for making this different >:( 86 | 87 | const SRGB_ALPHA_EXT = 0x8C42; 88 | 89 | /** 90 | * @typedef {Object} TextureFormatDetails 91 | * @property {number} textureFormat format to pass texImage2D and similar functions. 92 | * @property {boolean} colorRenderable true if you can render to this format of texture. 93 | * @property {boolean} textureFilterable true if you can filter the texture, false if you can ony use `NEAREST`. 94 | * @property {number[]} type Array of possible types you can pass to texImage2D and similar function 95 | * @property {Object.} bytesPerElementMap A map of types to bytes per element 96 | * @private 97 | */ 98 | 99 | let s_textureInternalFormatInfo; 100 | function getTextureInternalFormatInfo(internalFormat) { 101 | if (!s_textureInternalFormatInfo) { 102 | // NOTE: these properties need unique names so we can let Uglify mangle the name. 103 | const t = {}; 104 | // unsized formats 105 | t[ALPHA] = { bytesPerElement: [1, 2, 2, 4], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 106 | t[LUMINANCE] = { bytesPerElement: [1, 2, 2, 4], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 107 | t[LUMINANCE_ALPHA] = { bytesPerElement: [2, 4, 4, 8], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT], }; 108 | t[RGB] = { bytesPerElement: [3, 6, 6, 12, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_5_6_5], }; 109 | t[RGBA] = { bytesPerElement: [4, 8, 8, 16, 2, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_4_4_4_4, UNSIGNED_SHORT_5_5_5_1], }; 110 | t[SRGB_ALPHA_EXT] = { bytesPerElement: [4, 8, 8, 16, 2, 2], type: [UNSIGNED_BYTE, HALF_FLOAT, HALF_FLOAT_OES, FLOAT, UNSIGNED_SHORT_4_4_4_4, UNSIGNED_SHORT_5_5_5_1], }; 111 | t[DEPTH_COMPONENT] = { bytesPerElement: [2, 4], type: [UNSIGNED_INT, UNSIGNED_SHORT], }; 112 | t[DEPTH_STENCIL] = { bytesPerElement: [4], }; 113 | 114 | // sized formats 115 | t[R8] = { bytesPerElement: [1], }; 116 | t[R8_SNORM] = { bytesPerElement: [1], }; 117 | t[R16F] = { bytesPerElement: [2], }; 118 | t[R32F] = { bytesPerElement: [4], }; 119 | t[R8UI] = { bytesPerElement: [1], }; 120 | t[R8I] = { bytesPerElement: [1], }; 121 | t[R16UI] = { bytesPerElement: [2], }; 122 | t[R16I] = { bytesPerElement: [2], }; 123 | t[R32UI] = { bytesPerElement: [4], }; 124 | t[R32I] = { bytesPerElement: [4], }; 125 | t[RG8] = { bytesPerElement: [2], }; 126 | t[RG8_SNORM] = { bytesPerElement: [2], }; 127 | t[RG16F] = { bytesPerElement: [4], }; 128 | t[RG32F] = { bytesPerElement: [8], }; 129 | t[RG8UI] = { bytesPerElement: [2], }; 130 | t[RG8I] = { bytesPerElement: [2], }; 131 | t[RG16UI] = { bytesPerElement: [4], }; 132 | t[RG16I] = { bytesPerElement: [4], }; 133 | t[RG32UI] = { bytesPerElement: [8], }; 134 | t[RG32I] = { bytesPerElement: [8], }; 135 | t[RGB8] = { bytesPerElement: [3], }; 136 | t[SRGB8] = { bytesPerElement: [3], }; 137 | t[RGB565] = { bytesPerElement: [2], }; 138 | t[RGB8_SNORM] = { bytesPerElement: [3], }; 139 | t[R11F_G11F_B10F] = { bytesPerElement: [4], }; 140 | t[RGB9_E5] = { bytesPerElement: [4], }; 141 | t[RGB16F] = { bytesPerElement: [6], }; 142 | t[RGB32F] = { bytesPerElement: [12], }; 143 | t[RGB8UI] = { bytesPerElement: [3], }; 144 | t[RGB8I] = { bytesPerElement: [3], }; 145 | t[RGB16UI] = { bytesPerElement: [6], }; 146 | t[RGB16I] = { bytesPerElement: [6], }; 147 | t[RGB32UI] = { bytesPerElement: [12], }; 148 | t[RGB32I] = { bytesPerElement: [12], }; 149 | t[RGBA8] = { bytesPerElement: [4], }; 150 | t[SRGB8_ALPHA8] = { bytesPerElement: [4], }; 151 | t[RGBA8_SNORM] = { bytesPerElement: [4], }; 152 | t[RGB5_A1] = { bytesPerElement: [2], }; 153 | t[RGBA4] = { bytesPerElement: [2], }; 154 | t[RGB10_A2] = { bytesPerElement: [4], }; 155 | t[RGBA16F] = { bytesPerElement: [8], }; 156 | t[RGBA32F] = { bytesPerElement: [16], }; 157 | t[RGBA8UI] = { bytesPerElement: [4], }; 158 | t[RGBA8I] = { bytesPerElement: [4], }; 159 | t[RGB10_A2UI] = { bytesPerElement: [4], }; 160 | t[RGBA16UI] = { bytesPerElement: [8], }; 161 | t[RGBA16I] = { bytesPerElement: [8], }; 162 | t[RGBA32I] = { bytesPerElement: [16], }; 163 | t[RGBA32UI] = { bytesPerElement: [16], }; 164 | // Sized Internal 165 | t[DEPTH_COMPONENT16] = { bytesPerElement: [2], }; 166 | t[DEPTH_COMPONENT24] = { bytesPerElement: [4], }; 167 | t[DEPTH_COMPONENT32F] = { bytesPerElement: [4], }; 168 | t[DEPTH24_STENCIL8] = { bytesPerElement: [4], }; 169 | t[DEPTH32F_STENCIL8] = { bytesPerElement: [4], }; 170 | t[STENCIL_INDEX8] = { bytesPerElement: [1], }; 171 | 172 | s_textureInternalFormatInfo = t; 173 | } 174 | return s_textureInternalFormatInfo[internalFormat]; 175 | } 176 | 177 | function makeComputeBlockRectSizeFunction(blockWidth, blockHeight, bytesPerBlock) { 178 | return function(width, height, depth) { 179 | const blocksAcross = (width + blockWidth - 1) / blockWidth | 0; 180 | const blocksDown = (height + blockHeight - 1) / blockHeight | 0; 181 | return blocksAcross * blocksDown * bytesPerBlock * depth; 182 | }; 183 | } 184 | 185 | function makeComputePaddedRectSizeFunction(minWidth, minHeight, divisor) { 186 | return function(width, height, depth) { 187 | return (Math.max(width, minWidth) * Math.max(height, minHeight) / divisor | 0) * depth; 188 | }; 189 | } 190 | 191 | // WEBGL_compressed_texture_s3tc 192 | const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; 193 | const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; 194 | const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; 195 | const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; 196 | // WEBGL_compressed_texture_etc1 197 | const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64; 198 | // WEBGL_compressed_texture_pvrtc 199 | const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00; 200 | const COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01; 201 | const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02; 202 | const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03; 203 | // WEBGL_compressed_texture_etc 204 | const COMPRESSED_R11_EAC = 0x9270; 205 | const COMPRESSED_SIGNED_R11_EAC = 0x9271; 206 | const COMPRESSED_RG11_EAC = 0x9272; 207 | const COMPRESSED_SIGNED_RG11_EAC = 0x9273; 208 | const COMPRESSED_RGB8_ETC2 = 0x9274; 209 | const COMPRESSED_SRGB8_ETC2 = 0x9275; 210 | const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; 211 | const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; 212 | const COMPRESSED_RGBA8_ETC2_EAC = 0x9278; 213 | const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; 214 | // WEBGL_compressed_texture_astc 215 | const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; 216 | const COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1; 217 | const COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2; 218 | const COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3; 219 | const COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4; 220 | const COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5; 221 | const COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6; 222 | const COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7; 223 | const COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8; 224 | const COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9; 225 | const COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA; 226 | const COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB; 227 | const COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC; 228 | const COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD; 229 | const COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0; 230 | const COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1; 231 | const COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2; 232 | const COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3; 233 | const COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4; 234 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5; 235 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6; 236 | const COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7; 237 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8; 238 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9; 239 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA; 240 | const COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB; 241 | const COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC; 242 | const COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD; 243 | // WEBGL_compressed_texture_s3tc_srgb 244 | const COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C; 245 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D; 246 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E; 247 | const COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F; 248 | // EXT_texture_compression_bptc 249 | const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C; 250 | const COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; 251 | const COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E; 252 | const COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F; 253 | // EXT_texture_compression_rgtc 254 | const COMPRESSED_RED_RGTC1_EXT = 0x8DBB; 255 | const COMPRESSED_SIGNED_RED_RGTC1_EXT = 0x8DBC; 256 | const COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD; 257 | const COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 0x8DBE; 258 | 259 | const compressedTextureFunctions = new Map([ 260 | [ COMPRESSED_RGB_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 261 | [ COMPRESSED_RGBA_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 262 | [ COMPRESSED_RGBA_S3TC_DXT3_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 263 | [ COMPRESSED_RGBA_S3TC_DXT5_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 264 | 265 | [ COMPRESSED_RGB_ETC1_WEBGL, makeComputeBlockRectSizeFunction(4, 4, 8) ], 266 | 267 | [ COMPRESSED_RGB_PVRTC_4BPPV1_IMG, makeComputePaddedRectSizeFunction(8, 8, 2) ], 268 | [ COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, makeComputePaddedRectSizeFunction(8, 8, 2) ], 269 | [ COMPRESSED_RGB_PVRTC_2BPPV1_IMG, makeComputePaddedRectSizeFunction(16, 8, 4) ], 270 | [ COMPRESSED_RGBA_PVRTC_2BPPV1_IMG, makeComputePaddedRectSizeFunction(16, 8, 4) ], 271 | 272 | [ COMPRESSED_R11_EAC, makeComputeBlockRectSizeFunction(4, 4, 8) ], 273 | [ COMPRESSED_SIGNED_R11_EAC, makeComputeBlockRectSizeFunction(4, 4, 8) ], 274 | [ COMPRESSED_RGB8_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 275 | [ COMPRESSED_SRGB8_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 276 | [ COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 277 | [ COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, makeComputeBlockRectSizeFunction(4, 4, 8) ], 278 | 279 | [ COMPRESSED_RG11_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 280 | [ COMPRESSED_SIGNED_RG11_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 281 | [ COMPRESSED_RGBA8_ETC2_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 282 | [ COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, makeComputeBlockRectSizeFunction(4, 4, 16) ], 283 | 284 | [ COMPRESSED_RGBA_ASTC_4x4_KHR, makeComputeBlockRectSizeFunction(4, 4, 16) ], 285 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR, makeComputeBlockRectSizeFunction(4, 4, 16) ], 286 | [ COMPRESSED_RGBA_ASTC_5x4_KHR, makeComputeBlockRectSizeFunction(5, 4, 16) ], 287 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR, makeComputeBlockRectSizeFunction(5, 4, 16) ], 288 | [ COMPRESSED_RGBA_ASTC_5x5_KHR, makeComputeBlockRectSizeFunction(5, 5, 16) ], 289 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR, makeComputeBlockRectSizeFunction(5, 5, 16) ], 290 | [ COMPRESSED_RGBA_ASTC_6x5_KHR, makeComputeBlockRectSizeFunction(6, 5, 16) ], 291 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR, makeComputeBlockRectSizeFunction(6, 5, 16) ], 292 | [ COMPRESSED_RGBA_ASTC_6x6_KHR, makeComputeBlockRectSizeFunction(6, 6, 16) ], 293 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR, makeComputeBlockRectSizeFunction(6, 6, 16) ], 294 | [ COMPRESSED_RGBA_ASTC_8x5_KHR, makeComputeBlockRectSizeFunction(8, 5, 16) ], 295 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR, makeComputeBlockRectSizeFunction(8, 5, 16) ], 296 | [ COMPRESSED_RGBA_ASTC_8x6_KHR, makeComputeBlockRectSizeFunction(8, 6, 16) ], 297 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR, makeComputeBlockRectSizeFunction(8, 6, 16) ], 298 | [ COMPRESSED_RGBA_ASTC_8x8_KHR, makeComputeBlockRectSizeFunction(8, 8, 16) ], 299 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR, makeComputeBlockRectSizeFunction(8, 8, 16) ], 300 | [ COMPRESSED_RGBA_ASTC_10x5_KHR, makeComputeBlockRectSizeFunction(10, 5, 16) ], 301 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR, makeComputeBlockRectSizeFunction(10, 5, 16) ], 302 | [ COMPRESSED_RGBA_ASTC_10x6_KHR, makeComputeBlockRectSizeFunction(10, 6, 16) ], 303 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR, makeComputeBlockRectSizeFunction(10, 6, 16) ], 304 | [ COMPRESSED_RGBA_ASTC_10x8_KHR, makeComputeBlockRectSizeFunction(10, 8, 16) ], 305 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR, makeComputeBlockRectSizeFunction(10, 8, 16) ], 306 | [ COMPRESSED_RGBA_ASTC_10x10_KHR, makeComputeBlockRectSizeFunction(10, 10, 16) ], 307 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR, makeComputeBlockRectSizeFunction(10, 10, 16) ], 308 | [ COMPRESSED_RGBA_ASTC_12x10_KHR, makeComputeBlockRectSizeFunction(12, 10, 16) ], 309 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR, makeComputeBlockRectSizeFunction(12, 10, 16) ], 310 | [ COMPRESSED_RGBA_ASTC_12x12_KHR, makeComputeBlockRectSizeFunction(12, 12, 16) ], 311 | [ COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR, makeComputeBlockRectSizeFunction(12, 12, 16) ], 312 | 313 | [ COMPRESSED_SRGB_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 314 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, makeComputeBlockRectSizeFunction(4, 4, 8) ], 315 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 316 | [ COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, makeComputeBlockRectSizeFunction(4, 4, 16) ], 317 | 318 | [ COMPRESSED_RGBA_BPTC_UNORM_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 319 | [ COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 320 | [ COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 321 | [ COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 322 | 323 | [ COMPRESSED_RED_RGTC1_EXT, makeComputeBlockRectSizeFunction( 4, 4, 8 ) ], 324 | [ COMPRESSED_SIGNED_RED_RGTC1_EXT, makeComputeBlockRectSizeFunction( 4, 4, 8 ) ], 325 | [ COMPRESSED_RED_GREEN_RGTC2_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 326 | [ COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT, makeComputeBlockRectSizeFunction( 4, 4, 16 ) ], 327 | ]); 328 | 329 | /** 330 | * Gets the number of bytes per element for a given internalFormat / type 331 | * @param {number} internalFormat The internalFormat parameter from texImage2D etc.. 332 | * @param {number} type The type parameter for texImage2D etc.. 333 | * @return {number} the number of bytes per element for the given internalFormat, type combo 334 | * @memberOf module:twgl/textures 335 | */ 336 | function getBytesPerElementForInternalFormat(internalFormat, type) { 337 | const info = getTextureInternalFormatInfo(internalFormat); 338 | if (!info) { 339 | throw "unknown internal format"; 340 | } 341 | if (info.type) { 342 | const ndx = info.type.indexOf(type); 343 | if (ndx < 0) { 344 | throw new Error(`unsupported type ${type} for internalformat ${internalFormat}`); 345 | } 346 | return info.bytesPerElement[ndx]; 347 | } 348 | return info.bytesPerElement[0]; 349 | } 350 | 351 | function getBytesForMipUncompressed(internalFormat, width, height, depth, type) { 352 | const bytesPerElement = getBytesPerElementForInternalFormat(internalFormat, type); 353 | return width * height * depth * bytesPerElement; 354 | } 355 | 356 | function getBytesForMip(internalFormat, width, height, depth, type) { 357 | const fn = compressedTextureFunctions.get(internalFormat); 358 | return fn ? fn(width, height, depth) : getBytesForMipUncompressed(internalFormat, width, height, depth, type); 359 | } 360 | 361 | function isTypedArray(v) { 362 | return v && v.buffer && v.buffer instanceof ArrayBuffer; 363 | } 364 | 365 | function isBufferSource(v) { 366 | return isTypedArray(v) || v instanceof ArrayBuffer; 367 | } 368 | 369 | function getDrawingbufferInfo(gl) { 370 | return { 371 | samples: gl.getParameter(gl.SAMPLES) || 1, 372 | depthBits: gl.getParameter(gl.DEPTH_BITS), 373 | stencilBits: gl.getParameter(gl.STENCIL_BITS), 374 | contextAttributes: gl.getContextAttributes(), 375 | }; 376 | } 377 | 378 | function computeDepthStencilSize(drawingBufferInfo) { 379 | const {depthBits, stencilBits} = drawingBufferInfo; 380 | const depthSize = (depthBits + stencilBits + 7) / 8 | 0; 381 | return depthSize === 3 ? 4 : depthSize; 382 | } 383 | 384 | function computeDrawingbufferSize(gl, drawingBufferInfo) { 385 | if (gl.isContextLost()) { 386 | return 0; 387 | } 388 | const {samples} = drawingBufferInfo; 389 | // this will need to change for hi-color support 390 | const colorSize = 4; 391 | const size = gl.drawingBufferWidth * gl.drawingBufferHeight; 392 | const depthStencilSize = computeDepthStencilSize(drawingBufferInfo); 393 | return size * colorSize + size * samples * colorSize + size * depthStencilSize; 394 | } 395 | 396 | // I know this is not a full check 397 | function isNumber(v) { 398 | return typeof v === 'number'; 399 | } 400 | 401 | function collectObjects(state, type) { 402 | const list = [...state.webglObjectToMemory.keys()] 403 | .filter(obj => obj instanceof type) 404 | .map((obj) => state.webglObjectToMemory.get(obj)); 405 | 406 | return list; 407 | } 408 | 409 | function getStackTrace() { 410 | const stack = (new Error()).stack; 411 | const lines = stack.split('\n'); 412 | // Remove the first two entries, the error message and this function itself, or the webgl-memory itself. 413 | const userLines = lines.slice(2).filter((l) => !l.includes('webgl-memory.js')); 414 | return userLines.join('\n'); 415 | } 416 | 417 | /* 418 | The MIT License (MIT) 419 | 420 | Copyright (c) 2021 Gregg Tavares 421 | 422 | Permission is hereby granted, free of charge, to any person obtaining a copy of 423 | this software and associated documentation files (the "Software"), to deal in 424 | the Software without restriction, including without limitation the rights to 425 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 426 | the Software, and to permit persons to whom the Software is furnished to do so, 427 | subject to the following conditions: 428 | 429 | The above copyright notice and this permission notice shall be included in all 430 | copies or substantial portions of the Software. 431 | 432 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 433 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 434 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 435 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 436 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 437 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 438 | */ 439 | 440 | //------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ] 441 | 442 | /* 443 | ** Copyright (c) 2012 The Khronos Group Inc. 444 | ** 445 | ** Permission is hereby granted, free of charge, to any person obtaining a 446 | ** copy of this software and/or associated documentation files (the 447 | ** "Materials"), to deal in the Materials without restriction, including 448 | ** without limitation the rights to use, copy, modify, merge, publish, 449 | ** distribute, sublicense, and/or sell copies of the Materials, and to 450 | ** permit persons to whom the Materials are furnished to do so, subject to 451 | ** the following conditions: 452 | ** 453 | ** The above copyright notice and this permission notice shall be included 454 | ** in all copies or substantial portions of the Materials. 455 | ** 456 | ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 457 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 458 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 459 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 460 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 461 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 462 | ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. 463 | */ 464 | 465 | 466 | const augmentedSet = new Set(); 467 | 468 | /** 469 | * Given a WebGL context replaces all the functions with wrapped functions 470 | * that call gl.getError after every command 471 | * 472 | * @param {WebGLRenderingContext|Extension} ctx The webgl context to wrap. 473 | * @param {string} nameOfClass (eg, webgl, webgl2, OES_texture_float) 474 | */ 475 | // eslint-disable-next-line consistent-return 476 | function augmentAPI(ctx, nameOfClass, options = {}) { 477 | 478 | if (augmentedSet.has(ctx)) { 479 | return ctx; 480 | } 481 | augmentedSet.add(ctx); 482 | 483 | const origGLErrorFn = options.origGLErrorFn || ctx.getError; 484 | 485 | function createSharedState(ctx) { 486 | const drawingBufferInfo = getDrawingbufferInfo(ctx); 487 | const sharedState = { 488 | baseContext: ctx, 489 | config: options, 490 | apis: { 491 | // custom extension 492 | gman_webgl_memory: { 493 | ctx: { 494 | getMemoryInfo() { 495 | const drawingbuffer = computeDrawingbufferSize(ctx, drawingBufferInfo); 496 | return { 497 | memory: { 498 | ...memory, 499 | drawingbuffer, 500 | total: drawingbuffer + memory.buffer + memory.texture + memory.renderbuffer, 501 | }, 502 | resources: { 503 | ...resources, 504 | }, 505 | }; 506 | }, 507 | getResourcesInfo(type) { 508 | return collectObjects(sharedState, type); 509 | }, 510 | }, 511 | }, 512 | }, 513 | resources: {}, 514 | memory: { 515 | texture: 0, 516 | buffer: 0, 517 | renderbuffer: 0, 518 | }, 519 | bindings: new Map(), 520 | defaultVertexArray: {}, 521 | webglObjectToMemory: new Map(), 522 | }; 523 | 524 | const unRestorableAPIs = new Set([ 525 | 'webgl', 526 | 'webgl2', 527 | 'webgl_lose_context', 528 | ]); 529 | 530 | function resetSharedState() { 531 | sharedState.bindings.clear(); 532 | sharedState.webglObjectToMemory.clear(); 533 | sharedState.webglObjectToMemory.set(sharedState.defaultVertexArray, {}); 534 | sharedState.currentVertexArray = sharedState.defaultVertexArray; 535 | [sharedState.resources, sharedState.memory].forEach(function(obj) { 536 | for (const prop in obj) { 537 | obj[prop] = 0; 538 | } 539 | }); 540 | } 541 | 542 | function handleContextLost() { 543 | // Issues: 544 | // * all resources are lost. 545 | // Solution: handled by resetSharedState 546 | // * all functions are no-op 547 | // Solutions: 548 | // * swap all functions for noop 549 | // (not so easy because some functions return values) 550 | // * wrap all functions is a isContextLost check forwarder 551 | // (slow? and same as above) 552 | // * have each function manually check for context lost 553 | // (simple but repetitive) 554 | // * all extensions are lost 555 | // Solution: For these we go through and restore all the functions 556 | // on each extension 557 | resetSharedState(); 558 | sharedState.isContextLost = true; 559 | 560 | // restore all original functions for extensions since 561 | // user will have to get new extensions. 562 | for (const [name, {ctx, origFuncs}] of [...Object.entries(sharedState.apis)]) { 563 | if (!unRestorableAPIs.has(name) && origFuncs) { 564 | augmentedSet.delete(ctx); 565 | for (const [funcName, origFn] of Object.entries(origFuncs)) { 566 | ctx[funcName] = origFn; 567 | } 568 | delete apis[name]; 569 | } 570 | } 571 | } 572 | 573 | function handleContextRestored() { 574 | sharedState.isContextLost = false; 575 | } 576 | 577 | if (ctx.canvas) { 578 | ctx.canvas.addEventListener('webglcontextlost', handleContextLost); 579 | ctx.canvas.addEventListener('webglcontextrestored', handleContextRestored); 580 | } 581 | 582 | resetSharedState(); 583 | return sharedState; 584 | } 585 | 586 | const sharedState = options.sharedState || createSharedState(ctx); 587 | options.sharedState = sharedState; 588 | 589 | const { 590 | apis, 591 | bindings, 592 | memory, 593 | resources, 594 | webglObjectToMemory, 595 | } = sharedState; 596 | 597 | const origFuncs = {}; 598 | 599 | function noop() { 600 | } 601 | 602 | function makeCreateWrapper(ctx, typeName, _funcName) { 603 | const funcName = _funcName || `create${typeName[0].toUpperCase()}${typeName.substr(1)}`; 604 | if (!ctx[funcName]) { 605 | return null; 606 | } 607 | resources[typeName] = 0; 608 | return function(ctx, funcName, args, webglObj) { 609 | if (sharedState.isContextLost) { 610 | return; 611 | } 612 | ++resources[typeName]; 613 | webglObjectToMemory.set(webglObj, { 614 | size: 0, 615 | stackCreated: getStackTrace(), 616 | }); 617 | }; 618 | } 619 | 620 | function makeDeleteWrapper(typeName, fn = noop, _funcName) { 621 | const funcName = _funcName || `delete${typeName[0].toUpperCase()}${typeName.substr(1)}`; 622 | if (!ctx[funcName]) { 623 | return null; 624 | } 625 | return function(ctx, funcName, args) { 626 | if (sharedState.isContextLost) { 627 | return; 628 | } 629 | const [obj] = args; 630 | const info = webglObjectToMemory.get(obj); 631 | if (info) { 632 | --resources[typeName]; 633 | fn(obj, info); 634 | // TODO: handle resource counts 635 | webglObjectToMemory.delete(obj); 636 | } 637 | }; 638 | } 639 | 640 | function updateRenderbuffer(target, samples, internalFormat, width, height) { 641 | if (sharedState.isContextLost) { 642 | return; 643 | } 644 | const obj = bindings.get(target); 645 | if (!obj) { 646 | throw new Error(`no renderbuffer bound to ${target}`); 647 | } 648 | const info = webglObjectToMemory.get(obj); 649 | if (!info) { 650 | throw new Error(`unknown renderbuffer ${obj}`); 651 | } 652 | 653 | const bytesForMip = getBytesForMip(internalFormat, width, height, 1); 654 | const newSize = bytesForMip * samples; 655 | 656 | memory.renderbuffer -= info.size; 657 | info.size = newSize; 658 | info.stackUpdated = getStackTrace(); 659 | memory.renderbuffer += newSize; 660 | } 661 | 662 | const ELEMENT_ARRAY_BUFFER = 0x8893; 663 | 664 | const UNSIGNED_BYTE = 0x1401; 665 | const TEXTURE_CUBE_MAP = 0x8513; 666 | const TEXTURE_2D_ARRAY = 0x8C1A; 667 | const TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; 668 | const TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; 669 | const TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; 670 | const TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; 671 | const TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; 672 | const TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A; 673 | 674 | const TEXTURE_BASE_LEVEL = 0x813C; 675 | const TEXTURE_MAX_LEVEL = 0x813D; 676 | 677 | const cubemapTargets = new Set([ 678 | TEXTURE_CUBE_MAP_POSITIVE_X, 679 | TEXTURE_CUBE_MAP_NEGATIVE_X, 680 | TEXTURE_CUBE_MAP_POSITIVE_Y, 681 | TEXTURE_CUBE_MAP_NEGATIVE_Y, 682 | TEXTURE_CUBE_MAP_POSITIVE_Z, 683 | TEXTURE_CUBE_MAP_NEGATIVE_Z, 684 | ]); 685 | 686 | function isCubemapFace(target) { 687 | return cubemapTargets.has(target); 688 | } 689 | 690 | function getTextureInfo(target) { 691 | target = isCubemapFace(target) ? TEXTURE_CUBE_MAP : target; 692 | const obj = bindings.get(target); 693 | if (!obj) { 694 | throw new Error(`no texture bound to ${target}`); 695 | } 696 | const info = webglObjectToMemory.get(obj); 697 | if (!info) { 698 | throw new Error(`unknown texture ${obj}`); 699 | } 700 | return info; 701 | } 702 | 703 | function updateMipLevel(info, target, level, internalFormat, width, height, depth, type) { 704 | const oldSize = info.size; 705 | const newMipSize = getBytesForMip(internalFormat, width, height, depth, type); 706 | 707 | const faceNdx = isCubemapFace(target) 708 | ? target - TEXTURE_CUBE_MAP_POSITIVE_X 709 | : 0; 710 | 711 | info.mips = info.mips || []; 712 | info.mips[level] = info.mips[level] || []; 713 | const mipFaceInfo = info.mips[level][faceNdx] || {}; 714 | info.size -= mipFaceInfo.size || 0; 715 | 716 | mipFaceInfo.size = newMipSize; 717 | mipFaceInfo.internalFormat = internalFormat; 718 | mipFaceInfo.type = type; 719 | mipFaceInfo.width = width; 720 | mipFaceInfo.height = height; 721 | mipFaceInfo.depth = depth; 722 | 723 | info.mips[level][faceNdx] = mipFaceInfo; 724 | info.size += newMipSize; 725 | 726 | memory.texture -= oldSize; 727 | memory.texture += info.size; 728 | 729 | info.stackUpdated = getStackTrace(); 730 | } 731 | 732 | function updateTexStorage(target, levels, internalFormat, width, height, depth) { 733 | const info = getTextureInfo(target); 734 | const numFaces = target === TEXTURE_CUBE_MAP ? 6 : 1; 735 | const baseFaceTarget = target === TEXTURE_CUBE_MAP ? TEXTURE_CUBE_MAP_POSITIVE_X : target; 736 | for (let level = 0; level < levels; ++level) { 737 | for (let face = 0; face < numFaces; ++face) { 738 | updateMipLevel(info, baseFaceTarget + face, level, internalFormat, width, height, depth); 739 | } 740 | width = Math.ceil(Math.max(width / 2, 1)); 741 | height = Math.ceil(Math.max(height / 2, 1)); 742 | depth = target === TEXTURE_2D_ARRAY ? depth : Math.ceil(Math.max(depth / 2, 1)); 743 | } 744 | } 745 | 746 | function handleBindVertexArray(gl, funcName, args) { 747 | if (sharedState.isContextLost) { 748 | return; 749 | } 750 | const [va] = args; 751 | sharedState.currentVertexArray = va ? va : sharedState.defaultVertexArray; 752 | } 753 | 754 | function handleBufferBinding(target, obj) { 755 | if (sharedState.isContextLost) { 756 | return; 757 | } 758 | switch (target) { 759 | case ELEMENT_ARRAY_BUFFER: { 760 | const info = webglObjectToMemory.get(sharedState.currentVertexArray); 761 | info.elementArrayBuffer = obj; 762 | break; 763 | } 764 | default: 765 | bindings.set(target, obj); 766 | break; 767 | } 768 | } 769 | 770 | const preChecks = {}; 771 | const postChecks = { 772 | // WebGL1 773 | // void bufferData(GLenum target, GLsizeiptr size, GLenum usage); 774 | // void bufferData(GLenum target, [AllowShared] BufferSource? srcData, GLenum usage); 775 | // WebGL2: 776 | // void bufferData(GLenum target, [AllowShared] ArrayBufferView srcData, GLenum usage, GLuint srcOffset, 777 | // optional GLuint length = 0); 778 | bufferData(gl, funcName, args) { 779 | if (sharedState.isContextLost) { 780 | return; 781 | } 782 | const [target, src, /* usage */, /*srcOffset = 0*/, length = undefined] = args; 783 | let obj; 784 | switch (target) { 785 | case ELEMENT_ARRAY_BUFFER: 786 | { 787 | const info = webglObjectToMemory.get(sharedState.currentVertexArray); 788 | obj = info.elementArrayBuffer; 789 | } 790 | break; 791 | default: 792 | obj = bindings.get(target); 793 | break; 794 | } 795 | if (!obj) { 796 | throw new Error(`no buffer bound to ${target}`); 797 | } 798 | let newSize = 0; 799 | if (length !== undefined) { 800 | newSize = length * src.BYTES_PER_ELEMENT; 801 | } else if (isBufferSource(src)) { 802 | newSize = src.byteLength; 803 | } else if (isNumber(src)) { 804 | newSize = src; 805 | } else { 806 | throw new Error(`unsupported bufferData src type ${src}`); 807 | } 808 | 809 | const info = webglObjectToMemory.get(obj); 810 | if (!info) { 811 | throw new Error(`unknown buffer ${obj}`); 812 | } 813 | 814 | memory.buffer -= info.size; 815 | info.size = newSize; 816 | info.stackUpdated = getStackTrace(); 817 | memory.buffer += newSize; 818 | }, 819 | 820 | bindVertexArray: handleBindVertexArray, 821 | bindVertexArrayOES: handleBindVertexArray, 822 | 823 | bindBuffer(gl, funcName, args) { 824 | const [target, obj] = args; 825 | handleBufferBinding(target, obj); 826 | }, 827 | 828 | bindBufferBase(gl, funcName, args) { 829 | const [target, /*ndx*/, obj] = args; 830 | handleBufferBinding(target, obj); 831 | }, 832 | 833 | bindBufferRange(gl, funcName, args) { 834 | const [target, /*ndx*/, obj, /*offset*/, /*size*/] = args; 835 | handleBufferBinding(target, obj); 836 | }, 837 | 838 | bindRenderbuffer(gl, funcName, args) { 839 | if (sharedState.isContextLost) { 840 | return; 841 | } 842 | const [target, obj] = args; 843 | bindings.set(target, obj); 844 | }, 845 | 846 | bindTexture(gl, funcName, args) { 847 | if (sharedState.isContextLost) { 848 | return; 849 | } 850 | const [target, obj] = args; 851 | bindings.set(target, obj); 852 | }, 853 | 854 | // void gl.copyTexImage2D(target, level, internalformat, x, y, width, height, border); 855 | copyTexImage2D(ctx, funcName, args) { 856 | if (sharedState.isContextLost) { 857 | return; 858 | } 859 | const [target, level, internalFormat, /*x*/, /*y*/, width, height, /*border*/] = args; 860 | const info = getTextureInfo(target); 861 | updateMipLevel(info, target, level, internalFormat, width, height, 1, UNSIGNED_BYTE); 862 | }, 863 | 864 | createBuffer: makeCreateWrapper(ctx, 'buffer'), 865 | createFramebuffer: makeCreateWrapper(ctx, 'framebuffer'), 866 | createRenderbuffer: makeCreateWrapper(ctx, 'renderbuffer'), 867 | createProgram: makeCreateWrapper(ctx, 'program'), 868 | createQuery: makeCreateWrapper(ctx, 'query'), 869 | createShader: makeCreateWrapper(ctx, 'shader'), 870 | createSampler: makeCreateWrapper(ctx, 'sampler'), 871 | createTexture: makeCreateWrapper(ctx, 'texture'), 872 | createTransformFeedback: makeCreateWrapper(ctx, 'transformFeedback'), 873 | createVertexArray: makeCreateWrapper(ctx, 'vertexArray'), 874 | createVertexArrayOES: makeCreateWrapper(ctx, 'vertexArray', 'createVertexArrayOES'), 875 | 876 | // WebGL 1: 877 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, ArrayBufferView? pixels); 878 | // 879 | // Additionally available in WebGL 2: 880 | // read from buffer bound to gl.PIXEL_UNPACK_BUFFER 881 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, GLsizei imageSize, GLintptr offset); 882 | // void gl.compressedTexImage2D(target, level, internalformat, width, height, border, 883 | // ArrayBufferView srcData, optional srcOffset, optional srcLengthOverride); 884 | compressedTexImage2D(ctx, funcName, args) { 885 | if (sharedState.isContextLost) { 886 | return; 887 | } 888 | const [target, level, internalFormat, width, height] = args; 889 | const info = getTextureInfo(target); 890 | updateMipLevel(info, target, level, internalFormat, width, height, 1, UNSIGNED_BYTE); 891 | }, 892 | 893 | // read from buffer bound to gl.PIXEL_UNPACK_BUFFER 894 | // void gl.compressedTexImage3D(target, level, internalformat, width, height, depth, border, GLsizei imageSize, GLintptr offset); 895 | // void gl.compressedTexImage3D(target, level, internalformat, width, height, depth, border, 896 | // ArrayBufferView srcData, optional srcOffset, optional srcLengthOverride); 897 | compressedTexImage3D(ctx, funcName, args) { 898 | if (sharedState.isContextLost) { 899 | return; 900 | } 901 | const [target, level, internalFormat, width, height, depth] = args; 902 | const info = getTextureInfo(target); 903 | updateMipLevel(info, target, level, internalFormat, width, height, depth, UNSIGNED_BYTE); 904 | }, 905 | 906 | deleteBuffer: makeDeleteWrapper('buffer', function(obj, info) { 907 | memory.buffer -= info.size; 908 | }), 909 | deleteFramebuffer: makeDeleteWrapper('framebuffer'), 910 | deleteProgram: makeDeleteWrapper('program'), 911 | deleteQuery: makeDeleteWrapper('query'), 912 | deleteRenderbuffer: makeDeleteWrapper('renderbuffer', function(obj, info) { 913 | memory.renderbuffer -= info.size; 914 | }), 915 | deleteSampler: makeDeleteWrapper('sampler'), 916 | deleteShader: makeDeleteWrapper('shader'), 917 | deleteSync: makeDeleteWrapper('sync'), 918 | deleteTexture: makeDeleteWrapper('texture', function(obj, info) { 919 | memory.texture -= info.size; 920 | }), 921 | deleteTransformFeedback: makeDeleteWrapper('transformFeedback'), 922 | deleteVertexArray: makeDeleteWrapper('vertexArray'), 923 | deleteVertexArrayOES: makeDeleteWrapper('vertexArray', noop, 'deleteVertexArrayOES'), 924 | 925 | fenceSync: function(ctx) { 926 | if (sharedState.isContextLost) { 927 | return undefined; 928 | } 929 | if (!ctx.fenceSync) { 930 | return undefined; 931 | } 932 | resources.sync = 0; 933 | return function(ctx, funcName, args, webglObj) { 934 | ++resources.sync; 935 | 936 | webglObjectToMemory.set(webglObj, { 937 | size: 0, 938 | }); 939 | }; 940 | }(ctx), 941 | 942 | generateMipmap(ctx, funcName, args) { 943 | if (sharedState.isContextLost) { 944 | return; 945 | } 946 | const [target] = args; 947 | const info = getTextureInfo(target); 948 | const baseMipNdx = info.parameters ? info.parameters.get(TEXTURE_BASE_LEVEL) || 0 : 0; 949 | const maxMipNdx = info.parameters ? info.parameters.get(TEXTURE_MAX_LEVEL) || 1024 : 1024; 950 | const mipInfo = info.mips[baseMipNdx][0]; 951 | let {width, height, depth} = mipInfo; 952 | const {internalFormat, type} = mipInfo; 953 | let level = baseMipNdx + 1; 954 | 955 | const numFaces = target === TEXTURE_CUBE_MAP ? 6 : 1; 956 | const baseFaceTarget = target === TEXTURE_CUBE_MAP ? TEXTURE_CUBE_MAP_POSITIVE_X : target; 957 | while (level <= maxMipNdx && !(width === 1 && height === 1 && (depth === 1 || target === TEXTURE_2D_ARRAY))) { 958 | width = Math.ceil(Math.max(width / 2, 1)); 959 | height = Math.ceil(Math.max(height / 2, 1)); 960 | depth = target === TEXTURE_2D_ARRAY ? depth : Math.ceil(Math.max(depth / 2, 1)); 961 | for (let face = 0; face < numFaces; ++face) { 962 | updateMipLevel(info, baseFaceTarget + face, level, internalFormat, width, height, depth, type); 963 | } 964 | ++level; 965 | } 966 | }, 967 | 968 | getSupportedExtensions(ctx, funcName, args, result) { 969 | if (sharedState.isContextLost) { 970 | return; 971 | } 972 | result.push('GMAN_webgl_memory'); 973 | }, 974 | 975 | // void gl.renderbufferStorage(target, internalFormat, width, height); 976 | // gl.RGBA4: 4 red bits, 4 green bits, 4 blue bits 4 alpha bits. 977 | // gl.RGB565: 5 red bits, 6 green bits, 5 blue bits. 978 | // gl.RGB5_A1: 5 red bits, 5 green bits, 5 blue bits, 1 alpha bit. 979 | // gl.DEPTH_COMPONENT16: 16 depth bits. 980 | // gl.STENCIL_INDEX8: 8 stencil bits. 981 | // gl.DEPTH_STENCIL 982 | renderbufferStorage(ctx, funcName, args) { 983 | const [target, internalFormat, width, height] = args; 984 | updateRenderbuffer(target, 1, internalFormat, width, height); 985 | }, 986 | 987 | // void gl.renderbufferStorageMultisample(target, samples, internalFormat, width, height); 988 | renderbufferStorageMultisample(ctx, funcName, args) { 989 | const [target, samples, internalFormat, width, height] = args; 990 | updateRenderbuffer(target, samples, internalFormat, width, height); 991 | }, 992 | 993 | texImage2D(ctx, funcName, args) { 994 | if (sharedState.isContextLost) { 995 | return; 996 | } 997 | // WebGL1: 998 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView? pixels); 999 | // void gl.texImage2D(target, level, internalformat, format, type, ImageData? pixels); 1000 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels); 1001 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels); 1002 | // void gl.texImage2D(target, level, internalformat, format, type, HTMLVideoElement? pixels); 1003 | // void gl.texImage2D(target, level, internalformat, format, type, ImageBitmap? pixels// ); 1004 | 1005 | // WebGL2: 1006 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, GLintptr offset); 1007 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLCanvasElement source); 1008 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLImageElement source); 1009 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLVideoElement source); 1010 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageBitmap source); 1011 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageData source); 1012 | // void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView srcData, srcOffset); 1013 | const [target, level, internalFormat] = args; 1014 | let width; 1015 | let height; 1016 | let type; 1017 | if (args.length === 6) { 1018 | const src = args[5]; 1019 | width = src.width; 1020 | height = src.height; 1021 | type = args[4]; 1022 | } else { 1023 | width = args[3]; 1024 | height = args[4]; 1025 | type = args[7]; 1026 | } 1027 | 1028 | const info = getTextureInfo(target); 1029 | updateMipLevel(info, target, level, internalFormat, width, height, 1, type); 1030 | }, 1031 | 1032 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, GLintptr offset); 1033 | // 1034 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLCanvasElement source); 1035 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLImageElement source); 1036 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, HTMLVideoElement source); 1037 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ImageBitmap source); 1038 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ImageData source); 1039 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ArrayBufferView? srcData); 1040 | // void gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, ArrayBufferView srcData, srcOffset); 1041 | 1042 | texImage3D(ctx, funcName, args) { 1043 | if (sharedState.isContextLost) { 1044 | return; 1045 | } 1046 | const [target, level, internalFormat, width, height, depth, /*border*/, /*format*/, type] = args; 1047 | const info = getTextureInfo(target); 1048 | updateMipLevel(info, target, level, internalFormat, width, height, depth, type); 1049 | }, 1050 | 1051 | texParameteri(ctx, funcName, args) { 1052 | if (sharedState.isContextLost) { 1053 | return; 1054 | } 1055 | const [target, pname, value] = args; 1056 | const info = getTextureInfo(target); 1057 | info.parameters = info.parameters || new Map(); 1058 | info.parameters.set(pname, value); 1059 | }, 1060 | 1061 | // void gl.texStorage2D(target, levels, internalformat, width, height); 1062 | texStorage2D(ctx, funcName, args) { 1063 | const [target, levels, internalFormat, width, height] = args; 1064 | updateTexStorage(target, levels, internalFormat, width, height, 1); 1065 | }, 1066 | 1067 | // void gl.texStorage3D(target, levels, internalformat, width, height, depth); 1068 | texStorage3D(ctx, funcName, args) { 1069 | const [target, levels, internalFormat, width, height, depth] = args; 1070 | updateTexStorage(target, levels, internalFormat, width, height, depth); 1071 | }, 1072 | }; 1073 | 1074 | const extraWrappers = { 1075 | getExtension(ctx, propertyName) { 1076 | if (sharedState.isContextLost) { 1077 | return; 1078 | } 1079 | const origFn = ctx[propertyName]; 1080 | ctx[propertyName] = function(...args) { 1081 | const extensionName = args[0].toLowerCase(); 1082 | const api = apis[extensionName]; 1083 | if (api) { 1084 | return api.ctx; 1085 | } 1086 | const ext = origFn.call(ctx, ...args); 1087 | if (ext) { 1088 | augmentAPI(ext, extensionName, {...options, origGLErrorFn}); 1089 | } 1090 | return ext; 1091 | }; 1092 | }, 1093 | }; 1094 | 1095 | // Makes a function that calls a WebGL function and then calls getError. 1096 | function makeErrorWrapper(ctx, funcName) { 1097 | const origFn = ctx[funcName]; 1098 | const preCheck = preChecks[funcName] || noop; 1099 | const postCheck = postChecks[funcName] || noop; 1100 | if (preCheck === noop && postChecks === noop) { 1101 | return; 1102 | } 1103 | ctx[funcName] = function(...args) { 1104 | preCheck(ctx, funcName, args); 1105 | const result = origFn.call(ctx, ...args); 1106 | postCheck(ctx, funcName, args, result); 1107 | return result; 1108 | }; 1109 | const extraWrapperFn = extraWrappers[funcName]; 1110 | if (extraWrapperFn) { 1111 | extraWrapperFn(ctx, funcName, origGLErrorFn); 1112 | } 1113 | } 1114 | 1115 | // Wrap each function 1116 | for (const propertyName in ctx) { 1117 | if (typeof ctx[propertyName] === 'function') { 1118 | origFuncs[propertyName] = ctx[propertyName]; 1119 | makeErrorWrapper(ctx, propertyName); 1120 | } 1121 | } 1122 | 1123 | apis[nameOfClass.toLowerCase()] = { ctx, origFuncs }; 1124 | } 1125 | 1126 | /* 1127 | The MIT License (MIT) 1128 | 1129 | Copyright (c) 2021 Gregg Tavares 1130 | 1131 | Permission is hereby granted, free of charge, to any person obtaining a copy of 1132 | this software and associated documentation files (the "Software"), to deal in 1133 | the Software without restriction, including without limitation the rights to 1134 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 1135 | the Software, and to permit persons to whom the Software is furnished to do so, 1136 | subject to the following conditions: 1137 | 1138 | The above copyright notice and this permission notice shall be included in all 1139 | copies or substantial portions of the Software. 1140 | 1141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 1143 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 1144 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 1145 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 1146 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1147 | */ 1148 | 1149 | function wrapGetContext(Ctor) { 1150 | const oldFn = Ctor.prototype.getContext; 1151 | Ctor.prototype.getContext = function(type, ...args) { 1152 | const ctx = oldFn.call(this, type, ...args); 1153 | // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext 1154 | // but that might fail if wrapped by debugging extension 1155 | if (ctx && ctx.bindTexture) { 1156 | const config = {}; 1157 | augmentAPI(ctx, type, config); 1158 | ctx.getExtension('GMAN_webgl_memory'); 1159 | } 1160 | return ctx; 1161 | }; 1162 | } 1163 | 1164 | if (typeof HTMLCanvasElement !== 'undefined') { 1165 | wrapGetContext(HTMLCanvasElement); 1166 | } 1167 | if (typeof OffscreenCanvas !== 'undefined') { 1168 | wrapGetContext(OffscreenCanvas); 1169 | } 1170 | 1171 | })); 1172 | -------------------------------------------------------------------------------- /webgl-memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgl-memory/ce1db62ed4b639591a20952e52b86ff1d1f6f9fa/webgl-memory.png --------------------------------------------------------------------------------