├── .eslintrc.js ├── .github └── workflows │ └── build-and-deploy.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── build ├── lib │ └── spawn.js └── tools │ ├── prep-for-deploy.js │ └── serve.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── capabilities-info.ts └── webgpu-memory.ts ├── test ├── assert.js ├── index.html ├── index.js ├── mocha-support.js ├── mocha.css ├── mocha.js ├── puppeteer.js ├── src │ └── js │ │ └── example-inject.js └── tests │ ├── buffer-tests.js │ ├── canvas-tests.js │ ├── device-tests.js │ ├── query-set-tests.js │ └── texture-tests.js ├── tsconfig.json └── webgpu-memory.png /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | /* global __dirname */ 3 | module.exports = { 4 | env: { 5 | browser: true, 6 | }, 7 | // REMOVE THIS! 8 | globals: { 9 | 'GPUBufferUsage': 'readonly', 10 | 'GPUTextureUsage': 'readonly', 11 | 'GPUMapMode': 'readonly', 12 | }, 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | ecmaVersion: 11, 17 | tsconfigRootDir: __dirname, 18 | project: ['./jsconfig.json'], 19 | extraFileExtensions: ['.html'], 20 | }, 21 | plugins: [ 22 | '@typescript-eslint', 23 | 'eslint-plugin-optional-comma-spacing', 24 | 'eslint-plugin-one-variable-per-var', 25 | 'eslint-plugin-require-trailing-comma', 26 | ], 27 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 28 | rules: { 29 | // 'no-alert': 2, 30 | 'no-array-constructor': 2, 31 | 'no-caller': 2, 32 | 'no-catch-shadow': 2, 33 | 'no-const-assign': 2, 34 | 'no-eval': 2, 35 | 'no-extend-native': 2, 36 | 'no-extra-bind': 2, 37 | 'no-implied-eval': 2, 38 | 'no-inner-declarations': 0, 39 | 'no-iterator': 2, 40 | 'no-label-var': 2, 41 | 'no-labels': 2, 42 | 'no-lone-blocks': 0, 43 | 'no-multi-str': 2, 44 | 'no-native-reassign': 2, 45 | 'no-new': 2, 46 | 'no-new-func': 2, 47 | 'no-new-object': 2, 48 | 'no-new-wrappers': 2, 49 | 'no-octal-escape': 2, 50 | 'no-process-exit': 2, 51 | 'no-proto': 2, 52 | 'no-return-assign': 2, 53 | 'no-script-url': 2, 54 | 'no-self-assign': 0, 55 | 'no-sequences': 2, 56 | 'no-shadow-restricted-names': 2, 57 | 'no-spaced-func': 2, 58 | 'no-trailing-spaces': 2, 59 | 'no-undef-init': 2, 60 | 'no-unused-expressions': 2, 61 | 'no-use-before-define': 0, 62 | 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], 63 | '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], 64 | 'no-var': 2, 65 | 'no-with': 2, 66 | 'prefer-const': 2, 67 | 'consistent-return': 2, 68 | 'curly': [2, 'all'], 69 | 'no-extra-parens': [2, 'functions'], 70 | 'eqeqeq': 2, 71 | 'new-cap': 2, 72 | 'new-parens': 2, 73 | 'semi-spacing': [2, {'before': false, 'after': true}], 74 | 'space-infix-ops': 2, 75 | 'space-unary-ops': [2, { 'words': true, 'nonwords': false }], 76 | 'yoda': [2, 'never'], 77 | 78 | 'brace-style': [2, '1tbs', { 'allowSingleLine': false }], 79 | 'camelcase': [0], 80 | 'comma-spacing': 0, 81 | 'comma-dangle': 0, 82 | 'comma-style': [2, 'last'], 83 | 'optional-comma-spacing/optional-comma-spacing': [2, {'after': true}], 84 | 'dot-notation': 0, 85 | 'eol-last': [0], 86 | 'global-strict': [0], 87 | 'key-spacing': [0], 88 | 'no-comma-dangle': [0], 89 | 'no-irregular-whitespace': 2, 90 | 'no-multi-spaces': [0], 91 | 'no-loop-func': 0, 92 | 'no-obj-calls': 2, 93 | 'no-redeclare': [0], 94 | 'no-shadow': [0], 95 | 'no-undef': [2], 96 | 'no-unreachable': 2, 97 | 'one-variable-per-var/one-variable-per-var': [2], 98 | 'quotes': [2, 'single'], 99 | 'require-atomic-updates': 0, 100 | 'require-trailing-comma/require-trailing-comma': [2], 101 | 'require-yield': 0, 102 | 'semi': [2, 'always'], 103 | 'strict': [2, 'global'], 104 | 'space-before-function-paren': [2, 'never'], 105 | 'keyword-spacing': [1, {'before': true, 'after': true, 'overrides': {}} ], 106 | }, 107 | overrides: [ 108 | { 109 | files: [ 110 | 'fix.js', 111 | ], 112 | parserOptions: { 113 | sourceType: 'script', 114 | }, 115 | }, 116 | ], 117 | }; 118 | 119 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | permissions: 7 | contents: write 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | environment: deploy 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 20 20 | 21 | - name: Install and Build 🔧 22 | run: | 23 | npm ci 24 | npm run build-ci 25 | 26 | - name: Deploy 🚀 27 | uses: JamesIves/github-pages-deploy-action@v4 28 | with: 29 | folder: . 30 | 31 | - name: Publish to NPM 📖 32 | uses: JS-DevTools/npm-publish@v2 33 | with: 34 | token: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- clip-for-deploy-start -- 2 | 3 | /dist/1.x 4 | 5 | # -- clip-for-deploy-end -- 6 | 7 | *.pyc 8 | .DS_Store 9 | node_modules 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 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 | # WebGPU-Memory 2 | 3 | 4 | 5 | This is a WebGPU-Memory tracker. You add the script to your page 6 | before you initialize WebGPU and then for a given context 7 | you can ask how much WebGPU 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 11 | have alignment requirements. Still, this is likely to give 12 | a reasonable approximation. 13 | 14 | **You can use this without manually adding to your page 15 | via the [webgpu-dev-extension](https://github.com/greggman/webgpu-dev-extension).** 16 | 17 | ## Usage 18 | 19 | ```html 20 | 21 | 34 | 35 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | /* global URLSearchParams */ 3 | /* global window */ 4 | 5 | import './tests/buffer-tests.js'; 6 | import './tests/canvas-tests.js'; 7 | import './tests/device-tests.js'; 8 | import './tests/query-set-tests.js'; 9 | import './tests/texture-tests.js'; 10 | 11 | const settings = Object.fromEntries(new URLSearchParams(window.location.search).entries()); 12 | if (settings.reporter) { 13 | mocha.reporter(settings.reporter); 14 | } 15 | mocha.run((failures) => { 16 | window.testsPromiseInfo.resolve(failures); 17 | }); 18 | -------------------------------------------------------------------------------- /test/mocha-support.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | 3 | export const describe = window.describe; 4 | export const it = window.it; 5 | export const before = window.before; 6 | export const after = window.after; 7 | export const beforeEach = window.beforeEach; 8 | export const afterEach = window.afterEach; 9 | 10 | -------------------------------------------------------------------------------- /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 fs from 'fs'; 6 | import express from 'express'; 7 | import url from 'url'; 8 | const app = express(); 9 | const port = 3000; 10 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); // eslint-disable-line 11 | 12 | app.use(express.static(path.dirname(__dirname))); 13 | const server = app.listen(port, () => { 14 | console.log(`Example app listening on port ${port}!`); 15 | test(port); 16 | }); 17 | 18 | function makePromiseInfo() { 19 | const info = {}; 20 | const promise = new Promise((resolve, reject) => { 21 | Object.assign(info, {resolve, reject}); 22 | }); 23 | info.promise = promise; 24 | return info; 25 | } 26 | 27 | const exampleInjectJS = fs.readFileSync('test/src/js/example-inject.js', {encoding: 'utf-8'}); 28 | 29 | function getExamples(port) { 30 | return []; /*fs.readdirSync('examples') 31 | .filter(f => f.endsWith('.html')) 32 | .map(f => ({ 33 | url: `http://localhost:${port}/examples/${f}`, 34 | js: exampleInjectJS, 35 | screenshot: true, 36 | }));*/ 37 | } 38 | 39 | async function test(port) { 40 | const browser = await puppeteer.launch({ 41 | headless: "new", 42 | args: [ 43 | //'--enable-unsafe-webgpu', 44 | //'--enable-webgpu-developer-features', 45 | //'--use-angle=swiftshader', 46 | '--user-agent=puppeteer', 47 | '--no-sandbox', 48 | ], 49 | }); 50 | const page = await browser.newPage(); 51 | 52 | page.on('console', async e => { 53 | const args = await Promise.all(e.args().map(a => a.jsonValue())); 54 | console.log(...args); 55 | }); 56 | 57 | let totalFailures = 0; 58 | let waitingPromiseInfo; 59 | 60 | // Get the "viewport" of the page, as reported by the page. 61 | page.on('domcontentloaded', async () => { 62 | const failures = await page.evaluate(() => { 63 | return window.testsPromiseInfo.promise; 64 | }); 65 | 66 | totalFailures += failures; 67 | 68 | waitingPromiseInfo.resolve(); 69 | }); 70 | 71 | const testPages = [ 72 | {url: `http://localhost:${port}/test/index.html?reporter=spec` }, 73 | ...getExamples(port), 74 | ]; 75 | 76 | for (const {url, js, screenshot} of testPages) { 77 | waitingPromiseInfo = makePromiseInfo(); 78 | console.log(`===== [ ${url} ] =====`); 79 | const id = js 80 | ? await page.evaluateOnNewDocument(js) 81 | : undefined; 82 | await page.goto(url); 83 | await page.waitForNetworkIdle(); 84 | if (js) { 85 | await page.evaluate(() => { 86 | setTimeout(() => { 87 | window.testsPromiseInfo.resolve(0); 88 | }, 10); 89 | }); 90 | } 91 | await waitingPromiseInfo.promise; 92 | if (screenshot) { 93 | const dir = 'screenshots'; 94 | fs.mkdirSync(dir, { recursive: true }); 95 | const name = /\/([a-z0-9_-]+).html/.exec(url)[1]; 96 | const path = `${dir}/${name}.png`; 97 | await page.screenshot({path}); 98 | } 99 | if (js) { 100 | await page.removeScriptToEvaluateOnNewDocument(id.identifier); 101 | } 102 | } 103 | 104 | await browser.close(); 105 | server.close(); 106 | 107 | process.exit(totalFailures ? 1 : 0); // eslint-disable-line 108 | } 109 | -------------------------------------------------------------------------------- /test/src/js/example-inject.js: -------------------------------------------------------------------------------- 1 | /* global GPUAdapter */ 2 | // eslint-disable-next-line strict 3 | "use strict"; 4 | 5 | function makePromise() { 6 | const info = {}; 7 | const promise = new Promise((resolve, reject) => { 8 | Object.assign(info, {resolve, reject}); 9 | }); 10 | info.promise = promise; 11 | return info; 12 | } 13 | 14 | window.testsPromiseInfo = makePromise(); 15 | 16 | window.addEventListener('error', (event) => { 17 | console.error(event); 18 | window.testsPromiseInfo.reject(1); 19 | }); 20 | 21 | // eslint-disable-next-line no-lone-blocks 22 | { 23 | GPUAdapter.prototype.requestDevice = (function (origFn) { 24 | return async function (...args) { 25 | const device = await origFn.call(this, args); 26 | if (device) { 27 | device.addEventListener('uncapturederror', function (e) { 28 | console.error(e.error.message); 29 | window.testsPromiseInfo.reject(1); 30 | }); 31 | } 32 | return device; 33 | }; 34 | })(GPUAdapter.prototype.requestDevice); 35 | } 36 | -------------------------------------------------------------------------------- /test/tests/buffer-tests.js: -------------------------------------------------------------------------------- 1 | import {assertEqual} from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {getWebGPUMemoryUsage, resetMaxTotal} from '../../dist/1.x/webgpu-memory.module.js'; 4 | 5 | describe('buffer tests', () => { 6 | 7 | beforeEach(() => { 8 | resetMaxTotal(); 9 | }); 10 | 11 | it('tracks buffers', async function() { 12 | const adapter = await navigator.gpu?.requestAdapter(); 13 | const device = await adapter?.requestDevice(); 14 | if (!device) { 15 | this.skip(); 16 | return; 17 | } 18 | 19 | { 20 | const info = getWebGPUMemoryUsage(); 21 | assertEqual(info.memory.buffer, 0); 22 | assertEqual(info.resources.buffer, 0); 23 | } 24 | 25 | const buffer1 = device.createBuffer({ 26 | size: 128, 27 | usage: GPUBufferUsage.COPY_DST, 28 | }); 29 | 30 | { 31 | const info = getWebGPUMemoryUsage(); 32 | assertEqual(info.memory.buffer, 128); 33 | assertEqual(info.resources.buffer, 1); 34 | } 35 | 36 | const buffer2 = device.createBuffer({ 37 | size: 256, 38 | usage: GPUBufferUsage.COPY_DST, 39 | }); 40 | 41 | { 42 | const info = getWebGPUMemoryUsage(); 43 | assertEqual(info.memory.buffer, 128 + 256); 44 | assertEqual(info.resources.buffer, 2); 45 | } 46 | 47 | buffer1.destroy(); 48 | 49 | { 50 | const info = getWebGPUMemoryUsage(); 51 | assertEqual(info.memory.buffer, 256); 52 | assertEqual(info.resources.buffer, 1); 53 | } 54 | 55 | buffer2.destroy(); 56 | 57 | { 58 | const info = getWebGPUMemoryUsage(); 59 | assertEqual(info.memory.buffer, 0); 60 | assertEqual(info.resources.buffer, 0); 61 | } 62 | 63 | device.destroy(); 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /test/tests/canvas-tests.js: -------------------------------------------------------------------------------- 1 | import {assertEqual, assertFalsy} from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {getWebGPUMemoryUsage, resetMaxTotal} from '../../dist/1.x/webgpu-memory.module.js'; 4 | 5 | describe('canvas tests', () => { 6 | 7 | const kInitialCanvasSize = 300 * 150 * 4 * 2; 8 | const kNewCanvasSize1 = 500 * 150 * 4 * 2; 9 | const kNewCanvasSize2 = 400 * 150 * 4 * 2; 10 | 11 | beforeEach(() => { 12 | resetMaxTotal(); 13 | }); 14 | 15 | async function testCanvas(canvas1, canvas2) { 16 | const adapter = await navigator.gpu?.requestAdapter(); 17 | const device = await adapter?.requestDevice(); 18 | if (!device) { 19 | this.skip(); 20 | return; 21 | } 22 | 23 | { 24 | const info = getWebGPUMemoryUsage(); 25 | assertFalsy(info.resources.canvas, 0); 26 | } 27 | 28 | const context1 = canvas1.getContext('webgpu'); 29 | context1.configure({ 30 | format: 'bgra8unorm', 31 | device, 32 | alphaMode: 'opaque', 33 | }); 34 | 35 | { 36 | const info = getWebGPUMemoryUsage(); 37 | assertEqual(info.resources.canvas, 1); 38 | assertEqual(info.memory.total, kInitialCanvasSize); 39 | assertEqual(info.memory.maxTotal, kInitialCanvasSize); 40 | assertEqual(info.memory.canvas, kInitialCanvasSize); 41 | } 42 | 43 | const context2 = canvas2.getContext('webgpu'); 44 | context2.configure({ 45 | format: 'bgra8unorm', 46 | device, 47 | alphaMode: 'opaque', 48 | }); 49 | 50 | { 51 | const info = getWebGPUMemoryUsage(); 52 | assertEqual(info.resources.canvas, 2); 53 | assertEqual(info.memory.total, kInitialCanvasSize * 2); 54 | assertEqual(info.memory.maxTotal, kInitialCanvasSize * 2); 55 | assertEqual(info.memory.canvas, kInitialCanvasSize * 2); 56 | } 57 | 58 | canvas1.width = 500; 59 | context1.getCurrentTexture(); 60 | 61 | { 62 | const info = getWebGPUMemoryUsage(); 63 | assertEqual(info.resources.canvas, 2); 64 | assertEqual(info.memory.total, kNewCanvasSize1 + kInitialCanvasSize); 65 | assertEqual(info.memory.maxTotal, kNewCanvasSize1 + kInitialCanvasSize); 66 | assertEqual(info.memory.canvas, kNewCanvasSize1 + kInitialCanvasSize); 67 | } 68 | 69 | // smaller than before. maxTotal should stay large 70 | canvas1.width = 400; 71 | context1.getCurrentTexture(); 72 | 73 | { 74 | const info = getWebGPUMemoryUsage(); 75 | assertEqual(info.resources.canvas, 2); 76 | assertEqual(info.memory.total, kNewCanvasSize2 + kInitialCanvasSize); 77 | assertEqual(info.memory.maxTotal, kNewCanvasSize1 + kInitialCanvasSize); 78 | assertEqual(info.memory.canvas, kNewCanvasSize2 + kInitialCanvasSize); 79 | } 80 | 81 | context1.unconfigure(); 82 | 83 | { 84 | const info = getWebGPUMemoryUsage(); 85 | assertEqual(info.resources.canvas, 1); 86 | assertEqual(info.memory.total, kInitialCanvasSize); 87 | assertEqual(info.memory.maxTotal, kNewCanvasSize1 + kInitialCanvasSize); 88 | assertEqual(info.memory.canvas, kInitialCanvasSize); 89 | } 90 | 91 | context2.unconfigure(); 92 | 93 | { 94 | const info = getWebGPUMemoryUsage(); 95 | assertFalsy(info.resources.canvas); 96 | assertEqual(info.memory.total, 0); 97 | assertEqual(info.memory.maxTotal, kNewCanvasSize1 + kInitialCanvasSize); 98 | assertEqual(info.memory.canvas, 0); 99 | } 100 | 101 | device.destroy(); 102 | } 103 | 104 | it('tracks HTMLCanvasElement', async function() { 105 | const canvas1 = document.createElement('canvas'); 106 | const canvas2 = document.createElement('canvas'); 107 | await testCanvas(canvas1, canvas2); 108 | }); 109 | 110 | it('tracks OffscreenCanvas', async function() { 111 | const canvas1 = new OffscreenCanvas(300, 150); 112 | const canvas2 = new OffscreenCanvas(300, 150); 113 | await testCanvas(canvas1, canvas2); 114 | }); 115 | 116 | it('swaps canvases when reconfigured to a different device', async function() { 117 | const adapter1 = await navigator.gpu?.requestAdapter(); 118 | const device1 = await adapter1?.requestDevice(); 119 | const adapter2 = await navigator.gpu?.requestAdapter(); 120 | const device2 = await adapter2?.requestDevice(); 121 | if (!device1 || ! device2) { 122 | if (device1) { 123 | device1.destroy(); 124 | } 125 | if (device2) { 126 | device2.destroy(); 127 | } 128 | this.skip(); 129 | return; 130 | } 131 | 132 | const canvas = new OffscreenCanvas(300, 150); 133 | const context = canvas.getContext('webgpu'); 134 | context.configure({ 135 | device: device1, 136 | format: 'bgra8unorm', 137 | alphaMode: 'opaque', 138 | }); 139 | 140 | { 141 | const info = getWebGPUMemoryUsage(device1); 142 | assertEqual(info.resources.canvas, 1); 143 | assertEqual(info.memory.total, kInitialCanvasSize); 144 | assertEqual(info.memory.canvas, kInitialCanvasSize); 145 | } 146 | 147 | { 148 | const info = getWebGPUMemoryUsage(device2); 149 | assertFalsy(info.resources.canvas, 0); 150 | assertEqual(info.memory.total, 0); 151 | assertEqual(info.memory.canvas, 0); 152 | } 153 | 154 | context.configure({ 155 | device: device2, 156 | format: 'bgra8unorm', 157 | alphaMode: 'opaque', 158 | }); 159 | 160 | { 161 | const info = getWebGPUMemoryUsage(device1); 162 | assertFalsy(info.resources.canvas, 0); 163 | assertEqual(info.memory.total, 0); 164 | assertEqual(info.memory.canvas, 0); 165 | } 166 | 167 | { 168 | const info = getWebGPUMemoryUsage(device2); 169 | assertEqual(info.resources.canvas, 1); 170 | assertEqual(info.memory.total, kInitialCanvasSize); 171 | assertEqual(info.memory.canvas, kInitialCanvasSize); 172 | } 173 | 174 | device1.destroy() 175 | device2.destroy(); 176 | 177 | }); 178 | 179 | }); -------------------------------------------------------------------------------- /test/tests/device-tests.js: -------------------------------------------------------------------------------- 1 | import {assertEqual, assertFalsy} from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {getWebGPUMemoryUsage, resetMaxTotal} from '../../dist/1.x/webgpu-memory.module.js'; 4 | 5 | describe('device tests', () => { 6 | 7 | beforeEach(() => { 8 | resetMaxTotal(); 9 | }); 10 | 11 | it('tracks devices separately', async function() { 12 | const adapter1 = await navigator.gpu?.requestAdapter(); 13 | 14 | if (!adapter1) { 15 | this.skip(); 16 | return; 17 | } 18 | 19 | { 20 | const info = getWebGPUMemoryUsage(); 21 | assertFalsy(info.resources.device); 22 | } 23 | 24 | const device1 = await adapter1?.requestDevice(); 25 | 26 | if (!device1) { 27 | this.skip(); 28 | return; 29 | } 30 | 31 | { 32 | const info = getWebGPUMemoryUsage(); 33 | assertEqual(info.resources.device, 1); 34 | } 35 | 36 | const adapter2 = await navigator.gpu?.requestAdapter(); 37 | const device2 = await adapter2?.requestDevice(); 38 | if (!device2) { 39 | device1.destroy(); 40 | this.skip(); 41 | return; 42 | } 43 | 44 | { 45 | const info = getWebGPUMemoryUsage(); 46 | assertEqual(info.memory.buffer, 0); 47 | assertEqual(info.resources.buffer, 0); 48 | assertEqual(info.resources.device, 2); 49 | } 50 | 51 | const buffer1 = device1.createBuffer({ 52 | size: 128, 53 | usage: GPUBufferUsage.COPY_DST, 54 | }); 55 | 56 | { 57 | const info = getWebGPUMemoryUsage(); 58 | assertEqual(info.memory.buffer, 128); 59 | assertEqual(info.memory.maxTotal, 128); 60 | assertEqual(info.resources.buffer, 1); 61 | assertEqual(info.resources.device, 2); 62 | } 63 | 64 | { 65 | const info = getWebGPUMemoryUsage(device1); 66 | assertEqual(info.memory.buffer, 128); 67 | assertEqual(info.memory.maxTotal, 128); 68 | assertEqual(info.resources.buffer, 1); 69 | assertEqual(info.resources.device, 1); 70 | } 71 | 72 | { 73 | const info = getWebGPUMemoryUsage(device2); 74 | assertEqual(info.memory.buffer, 0); 75 | assertEqual(info.memory.maxTotal, 0); 76 | assertEqual(info.resources.buffer, 0); 77 | assertEqual(info.resources.device, 1); 78 | } 79 | 80 | const buffer2 = device2.createBuffer({ 81 | size: 256, 82 | usage: GPUBufferUsage.COPY_DST, 83 | }); 84 | 85 | { 86 | const info = getWebGPUMemoryUsage(); 87 | assertEqual(info.memory.buffer, 128 + 256); 88 | assertEqual(info.memory.maxTotal, 128 + 256); 89 | assertEqual(info.resources.buffer, 2); 90 | } 91 | 92 | { 93 | const info = getWebGPUMemoryUsage(device1); 94 | assertEqual(info.memory.buffer, 128); 95 | assertEqual(info.memory.maxTotal, 128); 96 | assertEqual(info.resources.buffer, 1); 97 | } 98 | 99 | { 100 | const info = getWebGPUMemoryUsage(device2); 101 | assertEqual(info.memory.buffer, 256); 102 | assertEqual(info.memory.maxTotal, 256); 103 | assertEqual(info.resources.buffer, 1); 104 | } 105 | 106 | buffer1.destroy(); 107 | 108 | { 109 | const info = getWebGPUMemoryUsage(); 110 | assertEqual(info.memory.buffer, 256); 111 | assertEqual(info.memory.maxTotal, 256 + 128); 112 | assertEqual(info.resources.buffer, 1); 113 | } 114 | 115 | { 116 | const info = getWebGPUMemoryUsage(device1); 117 | assertEqual(info.memory.buffer, 0); 118 | assertEqual(info.memory.maxTotal, 128); 119 | assertEqual(info.resources.buffer, 0); 120 | } 121 | 122 | { 123 | const info = getWebGPUMemoryUsage(device2); 124 | assertEqual(info.memory.buffer, 256); 125 | assertEqual(info.memory.maxTotal, 256); 126 | assertEqual(info.resources.buffer, 1); 127 | } 128 | 129 | buffer2.destroy(); 130 | 131 | { 132 | const info = getWebGPUMemoryUsage(); 133 | assertEqual(info.memory.buffer, 0); 134 | assertEqual(info.memory.maxTotal, 256 + 128); 135 | assertEqual(info.resources.buffer, 0); 136 | } 137 | 138 | { 139 | const info = getWebGPUMemoryUsage(device1); 140 | assertEqual(info.memory.buffer, 0); 141 | assertEqual(info.memory.maxTotal, 128); 142 | assertEqual(info.resources.buffer, 0); 143 | } 144 | 145 | { 146 | const info = getWebGPUMemoryUsage(device2); 147 | assertEqual(info.memory.buffer, 0); 148 | assertEqual(info.memory.maxTotal, 256); 149 | assertEqual(info.resources.buffer, 0); 150 | } 151 | 152 | device1.destroy(); 153 | 154 | 155 | { 156 | const info = getWebGPUMemoryUsage(); 157 | assertEqual(info.memory.maxTotal, 256 + 128); 158 | assertEqual(info.resources.device, 1); 159 | } 160 | 161 | device2.destroy(); 162 | 163 | { 164 | const info = getWebGPUMemoryUsage(); 165 | assertFalsy(info.resources.device); 166 | assertEqual(info.memory.maxTotal, 256 + 128); 167 | } 168 | }); 169 | 170 | it('frees resources on device destroy', async function() { 171 | const adapter = await navigator.gpu?.requestAdapter(); 172 | const device = await adapter?.requestDevice(); 173 | if (!device) { 174 | this.skip(); 175 | return; 176 | } 177 | 178 | device.createBuffer({ 179 | size: 128, 180 | usage: GPUBufferUsage.COPY_DST, 181 | }); 182 | 183 | { 184 | const info = getWebGPUMemoryUsage(); 185 | assertEqual(info.memory.buffer, 128); 186 | assertEqual(info.resources.buffer, 1); 187 | } 188 | 189 | device.destroy(); 190 | 191 | { 192 | const info = getWebGPUMemoryUsage(); 193 | assertEqual(info.memory.buffer, 0); 194 | assertEqual(info.resources.buffer, 0); 195 | } 196 | 197 | { 198 | const info = getWebGPUMemoryUsage(device); 199 | assertEqual(info.memory.buffer, 0); 200 | assertEqual(info.resources.buffer, 0); 201 | } 202 | }); 203 | 204 | it('resets maxTotal', async () => { 205 | 206 | const adapter1 = await navigator.gpu?.requestAdapter(); 207 | if (!adapter1) { 208 | this.skip(); 209 | return; 210 | } 211 | 212 | const device1 = await adapter1?.requestDevice(); 213 | if (!device1) { 214 | this.skip(); 215 | return; 216 | } 217 | 218 | const adapter2 = await navigator.gpu?.requestAdapter(); 219 | const device2 = await adapter2?.requestDevice(); 220 | if (!device2) { 221 | device1.destroy(); 222 | this.skip(); 223 | return; 224 | } 225 | 226 | const buffer1 = device1.createBuffer({ 227 | size: 128, 228 | usage: GPUBufferUsage.COPY_DST, 229 | }); 230 | const buffer2 = device2.createBuffer({ 231 | size: 256, 232 | usage: GPUBufferUsage.COPY_DST, 233 | }); 234 | 235 | { 236 | const info = getWebGPUMemoryUsage(); 237 | assertEqual(info.memory.buffer, 128 + 256); 238 | assertEqual(info.memory.maxTotal, 128 + 256); 239 | } 240 | 241 | resetMaxTotal(device1); 242 | 243 | { 244 | const info = getWebGPUMemoryUsage(); 245 | assertEqual(info.memory.buffer, 128 + 256); 246 | assertEqual(info.memory.maxTotal, 128 + 256); 247 | } 248 | 249 | buffer1.destroy(); 250 | 251 | { 252 | const info = getWebGPUMemoryUsage(); 253 | assertEqual(info.memory.buffer, 256); 254 | assertEqual(info.memory.maxTotal, 128 + 256); 255 | } 256 | 257 | resetMaxTotal(device1); 258 | 259 | { 260 | const info = getWebGPUMemoryUsage(); 261 | assertEqual(info.memory.buffer, 256); 262 | assertEqual(info.memory.maxTotal, 128 + 256); 263 | } 264 | 265 | { 266 | const info = getWebGPUMemoryUsage(device1); 267 | assertEqual(info.memory.buffer, 0); 268 | assertEqual(info.memory.maxTotal, 0); 269 | } 270 | 271 | { 272 | const info = getWebGPUMemoryUsage(device2); 273 | assertEqual(info.memory.buffer, 256); 274 | assertEqual(info.memory.maxTotal, 256); 275 | } 276 | 277 | buffer2.destroy(); 278 | resetMaxTotal(device2); 279 | 280 | { 281 | const info = getWebGPUMemoryUsage(); 282 | assertEqual(info.memory.buffer, 0); 283 | assertEqual(info.memory.maxTotal, 128 + 256); 284 | } 285 | 286 | { 287 | const info = getWebGPUMemoryUsage(device2); 288 | assertEqual(info.memory.buffer, 0); 289 | assertEqual(info.memory.maxTotal, 0); 290 | } 291 | 292 | const buffer3 = device1.createBuffer({ 293 | size: 128, 294 | usage: GPUBufferUsage.COPY_DST, 295 | }); 296 | const buffer4 = device1.createBuffer({ 297 | size: 256, 298 | usage: GPUBufferUsage.COPY_DST, 299 | }); 300 | 301 | { 302 | const info = getWebGPUMemoryUsage(); 303 | assertEqual(info.memory.buffer, 128 + 256); 304 | assertEqual(info.memory.maxTotal, 128 + 256); 305 | } 306 | 307 | buffer3.destroy(); 308 | buffer4.destroy(); 309 | 310 | { 311 | const info = getWebGPUMemoryUsage(); 312 | assertEqual(info.memory.buffer, 0); 313 | assertEqual(info.memory.maxTotal, 128 + 256); 314 | } 315 | 316 | resetMaxTotal(); 317 | 318 | { 319 | const info = getWebGPUMemoryUsage(); 320 | assertEqual(info.memory.buffer, 0); 321 | assertEqual(info.memory.maxTotal, 0); 322 | } 323 | 324 | { 325 | const info = getWebGPUMemoryUsage(device1); 326 | assertEqual(info.memory.buffer, 0); 327 | assertEqual(info.memory.maxTotal, 0); 328 | } 329 | 330 | { 331 | const info = getWebGPUMemoryUsage(device2); 332 | assertEqual(info.memory.buffer, 0); 333 | assertEqual(info.memory.maxTotal, 0); 334 | } 335 | 336 | }); 337 | 338 | it('tracks maxTotal - regression test 01', async () => { 339 | // bug where maxTotal is reset on allocation 340 | const adapter = await navigator.gpu?.requestAdapter(); 341 | if (!adapter) { 342 | this.skip(); 343 | return; 344 | } 345 | 346 | const device = await adapter?.requestDevice(); 347 | if (!device) { 348 | this.skip(); 349 | return; 350 | } 351 | 352 | const buffer1 = device.createBuffer({ 353 | size: 128, 354 | usage: GPUBufferUsage.COPY_DST, 355 | }); 356 | const buffer2 = device.createBuffer({ 357 | size: 256, 358 | usage: GPUBufferUsage.COPY_DST, 359 | }); 360 | buffer2.destroy(); 361 | const buffer3 = device.createBuffer({ 362 | size: 128, 363 | usage: GPUBufferUsage.COPY_DST, 364 | }); 365 | 366 | { 367 | const info = getWebGPUMemoryUsage(); 368 | assertEqual(info.memory.buffer, 128 + 128); 369 | assertEqual(info.memory.maxTotal, 128 + 256); 370 | } 371 | 372 | buffer1.destroy(); 373 | buffer3.destroy(); 374 | 375 | { 376 | const info = getWebGPUMemoryUsage(); 377 | assertEqual(info.memory.buffer, 0); 378 | assertEqual(info.memory.maxTotal, 128 + 256); 379 | } 380 | 381 | device.destroy(); 382 | 383 | }); 384 | 385 | }); -------------------------------------------------------------------------------- /test/tests/query-set-tests.js: -------------------------------------------------------------------------------- 1 | import {assertEqual} from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {getWebGPUMemoryUsage, resetMaxTotal} from '../../dist/1.x/webgpu-memory.module.js'; 4 | 5 | describe('querySet tests', () => { 6 | 7 | beforeEach(() => { 8 | resetMaxTotal(); 9 | }); 10 | 11 | it('tracks querySets', async function() { 12 | const adapter = await navigator.gpu?.requestAdapter(); 13 | const device = await adapter?.requestDevice(); 14 | if (!device) { 15 | this.skip(); 16 | return; 17 | } 18 | 19 | { 20 | const info = getWebGPUMemoryUsage(); 21 | assertEqual(info.memory.querySet, 0); 22 | assertEqual(info.resources.querySet, 0); 23 | } 24 | 25 | const querySet1 = device.createQuerySet({ 26 | type: 'occlusion', 27 | count: 20, 28 | }); 29 | 30 | { 31 | const info = getWebGPUMemoryUsage(); 32 | assertEqual(info.memory.querySet, 160); 33 | assertEqual(info.resources.querySet, 1); 34 | } 35 | 36 | const querySet2 = device.createQuerySet({ 37 | type: 'occlusion', 38 | count: 10, 39 | }); 40 | 41 | { 42 | const info = getWebGPUMemoryUsage(); 43 | assertEqual(info.memory.querySet, 160 + 80); 44 | assertEqual(info.resources.querySet, 2); 45 | } 46 | 47 | querySet1.destroy(); 48 | 49 | { 50 | const info = getWebGPUMemoryUsage(); 51 | assertEqual(info.memory.querySet, 80); 52 | assertEqual(info.resources.querySet, 1); 53 | } 54 | 55 | querySet2.destroy(); 56 | 57 | { 58 | const info = getWebGPUMemoryUsage(); 59 | assertEqual(info.memory.querySet, 0); 60 | assertEqual(info.resources.querySet, 0); 61 | } 62 | 63 | device.destroy(); 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /test/tests/texture-tests.js: -------------------------------------------------------------------------------- 1 | import {assertEqual} from '../assert.js'; 2 | import {describe, it} from '../mocha-support.js'; 3 | import {getWebGPUMemoryUsage, resetMaxTotal} from '../../dist/1.x/webgpu-memory.module.js'; 4 | 5 | function makeFormatInfo(textureInfo) { 6 | let byteSize = 0; 7 | let width = textureInfo.width; 8 | let height = textureInfo.height; 9 | let depth = textureInfo.depthOrArrayLayers || 1; 10 | const { 11 | bytesPerBlock, 12 | blockWidth = 1, 13 | blockHeight = 1, 14 | } = textureInfo; 15 | const size = [width, height, textureInfo.depthOrArrayLayers || 1]; 16 | 17 | for (let level = 0; level < textureInfo.mipLevelCount; ++level) { 18 | const blocksAcross = Math.ceil(width * textureInfo.sampleCount / blockWidth); 19 | const blocksDown = Math.ceil(height * textureInfo.sampleCount / blockHeight); 20 | const numBlocks = blocksAcross * blocksDown * depth; 21 | const bytesUsed = numBlocks * bytesPerBlock; 22 | byteSize += bytesUsed; 23 | width = Math.max(1, width / 2 | 0); 24 | height = Math.max(1, height / 2 | 0); 25 | depth = textureInfo.dimension === '3d' ? Math.max(1, depth / 2 | 0) : depth; 26 | } 27 | 28 | return { 29 | ...textureInfo, 30 | memSize: byteSize, 31 | size, 32 | }; 33 | } 34 | 35 | 36 | describe('texture tests', () => { 37 | 38 | beforeEach(() => { 39 | resetMaxTotal(); 40 | }); 41 | 42 | const width = 32; 43 | const height = 16; 44 | const formats = [ 45 | makeFormatInfo({ format: 'rgba8unorm', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 46 | makeFormatInfo({ format: 'rgba8unorm', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 5, sampleCount: 1, width, height }), 47 | makeFormatInfo({ format: 'rgba8unorm', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 4, width, height }), 48 | makeFormatInfo({ format: 'rgba8unorm', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height, depthOrArrayLayers: 10 }), 49 | makeFormatInfo({ format: 'rgba8unorm', bytesPerBlock: 4, dimension: '3d', mipLevelCount: 1, sampleCount: 1, width, height, depthOrArrayLayers: 10 }), 50 | makeFormatInfo({ format: 'rgba32float', bytesPerBlock: 16, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 51 | makeFormatInfo({ format: 'depth16unorm', bytesPerBlock: 2, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 52 | makeFormatInfo({ format: 'depth24plus', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 53 | makeFormatInfo({ format: 'depth24plus-stencil8', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 54 | makeFormatInfo({ format: 'depth32float', bytesPerBlock: 4, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height }), 55 | makeFormatInfo({ format: 'depth32float-stencil8', bytesPerBlock: 5, dimension: '2d', mipLevelCount: 1, sampleCount: 1, width, height, feature: 'depth32float-stencil8' }), 56 | ]; 57 | 58 | for (const { 59 | format, 60 | dimension, 61 | mipLevelCount, 62 | sampleCount, 63 | size, 64 | feature, 65 | memSize, 66 | } of formats) { 67 | it( 68 | `tracks textures of format: ${format}, dimension: ${dimension}, sampleCount: ${sampleCount}, mips: ${mipLevelCount}, size: ${size.join('×')}`, 69 | async function() { 70 | const adapter = await navigator.gpu?.requestAdapter(); 71 | const device = await adapter?.requestDevice({ requiredFeatures: adapter.features }); 72 | if (!device) { 73 | this.skip(); 74 | return; 75 | } 76 | 77 | if (feature && !device.features.has(feature)) { 78 | this.skip(`missing feature: ${feature}`); 79 | } 80 | 81 | const usage = GPUTextureUsage.TEXTURE_BINDING | 82 | (sampleCount > 1 ? GPUTextureUsage.RENDER_ATTACHMENT : 0); 83 | 84 | { 85 | const info = getWebGPUMemoryUsage(); 86 | assertEqual(info.memory.texture, 0); 87 | } 88 | 89 | const texture1 = device.createTexture({ 90 | size, 91 | format, 92 | dimension, 93 | mipLevelCount, 94 | sampleCount, 95 | usage, 96 | }); 97 | 98 | { 99 | const info = getWebGPUMemoryUsage(); 100 | assertEqual(info.memory.texture, memSize); 101 | } 102 | 103 | const texture2 = device.createTexture({ 104 | size, 105 | format, 106 | dimension, 107 | mipLevelCount, 108 | sampleCount, 109 | usage, 110 | }); 111 | 112 | { 113 | const info = getWebGPUMemoryUsage(); 114 | assertEqual(info.memory.texture, memSize * 2); 115 | } 116 | 117 | texture1.destroy(); 118 | 119 | { 120 | const info = getWebGPUMemoryUsage(); 121 | assertEqual(info.memory.texture, memSize); 122 | } 123 | 124 | texture2.destroy(); 125 | 126 | { 127 | const info = getWebGPUMemoryUsage(); 128 | assertEqual(info.memory.texture, 0); 129 | } 130 | 131 | device.destroy(); 132 | }); 133 | } 134 | 135 | [ 136 | { 137 | dimension: '2d', 138 | memSize: 4 * 4 * 4 * 4 + 139 | 2 * 2 * 4 * 4 + 140 | 1 * 1 * 4 * 4, 141 | }, 142 | { 143 | dimension: '3d', 144 | memSize: 4 * 4 * 4 * 4 + 145 | 2 * 2 * 2 * 4 + 146 | 1 * 1 * 1 * 4, 147 | }, 148 | ].forEach(({dimension, memSize}) => { 149 | it(`tracks dimension ${dimension} different than others`, async() => { 150 | const adapter = await navigator.gpu?.requestAdapter(); 151 | const device = await adapter?.requestDevice(); 152 | if (!device) { 153 | this.skip(); 154 | return; 155 | } 156 | 157 | { 158 | const info = getWebGPUMemoryUsage(); 159 | assertEqual(info.memory.texture, 0); 160 | } 161 | 162 | const texture = device.createTexture({ 163 | dimension, 164 | size: [4, 4, 4], 165 | mipLevelCount: 3, 166 | format: 'rgba8unorm', 167 | usage: GPUTextureUsage.TEXTURE_BINDING, 168 | }); 169 | 170 | { 171 | const info = getWebGPUMemoryUsage(); 172 | assertEqual(info.memory.texture, memSize); 173 | } 174 | 175 | texture.destroy(); 176 | 177 | { 178 | const info = getWebGPUMemoryUsage(); 179 | assertEqual(info.memory.texture, 0); 180 | } 181 | 182 | device.destroy(); 183 | }); 184 | }); 185 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "outDir": "./dist/1.x", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "typeRoots": [ "./node_modules/@webgpu/types", "./node_modules/@types"] 23 | }, 24 | "include": [ 25 | "build", 26 | ".", 27 | ".eslintrc.js", 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /webgpu-memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-memory/208187848161501fddb03d60fc4b3b34c79ee9cf/webgpu-memory.png --------------------------------------------------------------------------------