From f1f63daf2746e55aa525584cd53df343e882a42a Mon Sep 17 00:00:00 2001 From: greggman Date: Tue, 11 Jun 2024 03:21:34 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20greggman?= =?UTF-8?q?/webgpu-debug-helper@8a6d0fc2f919c09294dc9d7e2254eec62c9ed003?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintignore | 4 + .eslintrc.cjs | 150 + README.md | 15 +- TODO.md | 12 +- dist/0.x/webgpu-debug-helper.d.ts | 1 - dist/0.x/webgpu-debug-helper.js | 7938 ++++++++++++++++- dist/0.x/webgpu-debug-helper.js.map | 2 +- package-lock.json | 1393 +-- package.json | 38 +- rollup.config.js | 2 +- src/binding-mixin.ts | 344 + src/canvas-context.ts | 22 + src/command-encoder.ts | 286 + src/compute-pass-encoder.ts | 87 + src/device.ts | 254 + src/encoder-utils.ts | 72 + src/error-reporting.ts | 0 src/error-scope-wrapper.ts | 134 + src/format-info.ts | 1681 ++++ src/pipeline.ts | 232 + src/query-support.ts | 22 + src/queue.ts | 3 + src/render-bundle-encoder.ts | 73 + src/render-commands-mixin.ts | 289 + src/render-pass-encoder.ts | 348 + src/shared-state.ts | 73 + src/texture.ts | 43 + src/utils.ts | 138 + src/validation.ts | 53 + src/webgpu-debug-helper.ts | 203 +- src/wrap-api.ts | 40 + test/assert.js | 4 +- test/index.html | 55 + test/index.js | 7 + test/js/utils.js | 52 +- test/mocha-support.js | 1 - test/mocha.js | 268 +- test/tests/binding-mixin-tests.js | 459 + test/tests/canvas-context-tests.js | 44 + test/tests/command-encoder-tests.js | 129 + test/tests/command-encoder/copy-utils.js | 476 + .../copyBufferToBuffer-tests.js | 115 + .../copyBufferToTexture-tests.js | 17 + .../copyTextureToBuffer-tests.js | 17 + .../copyTextureToTexture-tests.js | 142 + test/tests/compute-pass-tests.js | 209 + test/tests/device-tests.js | 84 + test/tests/push-pop-error-tests.js | 90 + test/tests/render-bundle-tests.js | 97 + test/tests/render-mixin-tests.js | 890 ++ test/tests/render-pass-tests.js | 691 ++ test/tests/timestamp-tests.js | 114 + test/tests/webgpu-debug-helper-tests.js | 77 +- tsconfig.json | 6 +- 54 files changed, 16830 insertions(+), 1166 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs delete mode 100644 dist/0.x/webgpu-debug-helper.d.ts create mode 100644 src/binding-mixin.ts create mode 100644 src/canvas-context.ts create mode 100644 src/command-encoder.ts create mode 100644 src/compute-pass-encoder.ts create mode 100644 src/device.ts create mode 100644 src/encoder-utils.ts create mode 100644 src/error-reporting.ts create mode 100644 src/error-scope-wrapper.ts create mode 100644 src/format-info.ts create mode 100644 src/pipeline.ts create mode 100644 src/query-support.ts create mode 100644 src/queue.ts create mode 100644 src/render-bundle-encoder.ts create mode 100644 src/render-commands-mixin.ts create mode 100644 src/render-pass-encoder.ts create mode 100644 src/shared-state.ts create mode 100644 src/texture.ts create mode 100644 src/utils.ts create mode 100644 src/validation.ts create mode 100644 src/wrap-api.ts create mode 100644 test/index.html create mode 100644 test/tests/binding-mixin-tests.js create mode 100644 test/tests/canvas-context-tests.js create mode 100644 test/tests/command-encoder-tests.js create mode 100644 test/tests/command-encoder/copy-utils.js create mode 100644 test/tests/command-encoder/copyBufferToBuffer-tests.js create mode 100644 test/tests/command-encoder/copyBufferToTexture-tests.js create mode 100644 test/tests/command-encoder/copyTextureToBuffer-tests.js create mode 100644 test/tests/command-encoder/copyTextureToTexture-tests.js create mode 100644 test/tests/compute-pass-tests.js create mode 100644 test/tests/device-tests.js create mode 100644 test/tests/push-pop-error-tests.js create mode 100644 test/tests/render-bundle-tests.js create mode 100644 test/tests/render-mixin-tests.js create mode 100644 test/tests/render-pass-tests.js create mode 100644 test/tests/timestamp-tests.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..90e8e60 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +dist/**/* +src/3rdParty/**/* +examples/*.html +test/mocha* diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..611a260 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,150 @@ +/* global module, __dirname */ +module.exports = { + parser: '@typescript-eslint/parser', + env: { + browser: true, + es2022: true, + }, + globals: { + "GPU": "readonly", + "GPUAdapter": "readonly", + "GPUAdapterInfo": "readonly", + "GPUBindGroup": "readonly", + "GPUBindGroupLayout": "readonly", + "GPUBuffer": "readonly", + "GPUCanvasContext": "readonly", + "GPUCommandBuffer": "readonly", + "GPUCommandEncoder": "readonly", + "GPUCompilationInfo": "readonly", + "GPUCompilationMessage": "readonly", + "GPUComputePassEncoder": "readonly", + "GPUComputePipeline": "readonly", + "GPUDevice": "readonly", + "GPUDeviceLostInfo": "readonly", + "GPUError": "readonly", + "GPUExternalTexture": "readonly", + "GPUInternalError": "readonly", + "GPUOutOfMemoryError": "readonly", + "GPUPipelineError": "readonly", + "GPUPipelineLayout": "readonly", + "GPUQuerySet": "readonly", + "GPUQueue": "readonly", + "GPURenderBundle": "readonly", + "GPURenderBundleEncoder": "readonly", + "GPURenderPassEncoder": "readonly", + "GPURenderPipeline": "readonly", + "GPUSampler": "readonly", + "GPUShaderModule": "readonly", + "GPUSupportedLimits": "readonly", + "GPUTexture": "readonly", + "GPUTextureView": "readonly", + "GPUUncapturedErrorEvent": "readonly", + "GPUValidationError": "readonly", + "GPUBufferUsage": "readonly", + "GPUColorWrite": "readonly", + "GPUMapMode": "readonly", + "GPUShaderStage": "readonly", + "GPUTextureUsage": "readonly", + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + extraFileExtensions: ['.html'], + }, + root: true, + plugins: [ + '@typescript-eslint', + 'eslint-plugin-html', + 'eslint-plugin-optional-comma-spacing', + 'eslint-plugin-one-variable-per-var', + 'eslint-plugin-require-trailing-comma', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + ], + rules: { + 'brace-style': [2, '1tbs', { allowSingleLine: false }], + camelcase: [0], + 'comma-dangle': 0, + 'comma-spacing': 0, + 'comma-style': [2, 'last'], + 'consistent-return': 2, + curly: [2, 'all'], + 'dot-notation': 0, + 'eol-last': [0], + eqeqeq: 2, + 'global-strict': [0], + 'key-spacing': [0], + 'keyword-spacing': [1, { before: true, after: true, overrides: {} }], + 'new-cap': 2, + 'new-parens': 2, + 'no-alert': 2, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-catch-shadow': 2, + 'no-comma-dangle': [0], + 'no-const-assign': 2, + 'no-eval': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-parens': [2, 'functions'], + 'no-implied-eval': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': 2, + 'no-lone-blocks': 2, + 'no-loop-func': 2, + 'no-multi-spaces': [0], + 'no-multi-str': 2, + 'no-native-reassign': 2, + 'no-new-func': 2, + 'no-new-object': 2, + 'no-new-wrappers': 2, + 'no-new': 2, + 'no-obj-calls': 2, + 'no-octal-escape': 2, + 'no-process-exit': 2, + 'no-proto': 2, + 'no-return-assign': 2, + 'no-script-url': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-shadow': [0], + 'no-spaced-func': 2, + 'no-trailing-spaces': 2, + 'no-undef-init': 2, + //'no-undef': 2, // ts recommends this be off: https://typescript-eslint.io/linting/troubleshooting + 'no-underscore-dangle': 2, + 'no-unreachable': 2, + 'no-unused-expressions': 2, + 'no-use-before-define': 0, + 'no-var': 2, + 'no-with': 2, + 'one-variable-per-var/one-variable-per-var': [2], + 'optional-comma-spacing/optional-comma-spacing': [2, { after: true }], + 'prefer-const': 2, + 'require-trailing-comma/require-trailing-comma': [2], + 'semi-spacing': [2, { before: false, after: true }], + semi: [2, 'always'], + 'space-before-function-paren': [ + 2, + { + anonymous: 'always', + named: 'never', + asyncArrow: 'always', + }, + ], + 'space-infix-ops': 2, + 'space-unary-ops': [2, { words: true, nonwords: false }], + strict: [2, 'function'], + yoda: [2, 'never'], + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-explicit-any': 'off', // TODO: Reenable this and figure out how to fix code. + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-unused-vars': 2, + }, +}; diff --git a/README.md b/README.md index edcbc3d..02d7e8e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This script makes it easier to debug WebGPU apps. +note: this script is a work in progress. + You can use it in your own projects via a script OR, you can [use it as an extension](https://github.com/greggman/webgpu-dev-extension). @@ -19,11 +21,12 @@ to use this script version. This is in contrast to normal WebGPU where errors are returned asynchronously and so the command that caused the error is long forgotten. -* It adds errors to command encoders and pass encoders +* It adds errors to command encoders and pass encoders (these throw) In normal WebGPU, command encoders and pass encoders often do not report errors. - Rather, they record the error, make the encoder as *invalid*, and then only report - the error when the encoder is ended/finished. This can make it hard to find errors. + Rather, they record the error, mark the encoder as *invalid*, and then only report + the error when the encoder is ended/finished. This can make it hard to find errors + as they might have happened hundreds or thousands of calls ago. With this script, many of these types of errors will be generated immediately. @@ -48,6 +51,12 @@ or import 'https://greggman.github.io/webgpu-debug-helper/dist/0.x/webgpu-debug-helper.js'; ``` +or + +```html + + + + + + diff --git a/test/index.js b/test/index.js index f13473f..4cf6597 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,11 @@ /* global mocha */ +import './tests/canvas-context-tests.js'; +import './tests/command-encoder-tests.js'; +import './tests/compute-pass-tests.js'; +import './tests/device-tests.js'; +import './tests/push-pop-error-tests.js'; +import './tests/render-bundle-tests.js'; +import './tests/render-pass-tests.js'; import './tests/webgpu-debug-helper-tests.js'; const settings = Object.fromEntries(new URLSearchParams(window.location.search).entries()); diff --git a/test/js/utils.js b/test/js/utils.js index f3ba43e..ed3b09b 100644 --- a/test/js/utils.js +++ b/test/js/utils.js @@ -1,7 +1,8 @@ function saveFunctionsOfClass(obj) { const desc = Object.getOwnPropertyDescriptors(obj); return Object.entries(desc) - .filter(([name, {writable, enumerable, configurable}]) => writable && enumerable && configurable && typeof value === 'function') + .filter(([, {writable, enumerable, configurable}]) => + writable && enumerable && configurable && typeof value === 'function'); } function restoreFunctionsOfClass(obj, funcEntries) { @@ -10,14 +11,59 @@ function restoreFunctionsOfClass(obj, funcEntries) { } } -function saveFunctionsOfClasses(classes) { +export function saveFunctionsOfClasses(classes) { return classes.map(c => { return [c, saveFunctionsOfClass(c.prototype)]; }); } -function restoreFunctionsOfClasses(savedClasses) { +export function restoreFunctionsOfClasses(savedClasses) { for (const [c, savedFuncs] of savedClasses) { restoreFunctionsOfClass(c.prototype, savedFuncs); } } + +function bitmaskToString(bitNames/*: Record*/, mask/*: number*/) { + const names = []; + for (const [k, v] of Object.entries(bitNames)) { + if (mask & v) { + names.push(k); + } + } + return names.join('|'); +} + +export function bufferUsageToString(mask/*: number*/) { + return bitmaskToString(GPUBufferUsage/* as unknown as Record*/, mask); +} + +export function textureUsageToString(mask/*: number*/) { + return bitmaskToString(GPUTextureUsage/* as unknown as Record*/, mask); +} + +export async function expectValidationError(expectError, fn) { + let error = false; + try { + await fn(); + } catch (e) { + error = e; + } + if (expectError) { + if (!error) { + throw new Error('expected error, no error thrown'); + } + if (expectError instanceof RegExp) { + if (!expectError.test(error)) { + throw new Error(`expected error to match /${expectError}/ but was ${error}`); + } + } else if (typeof expectError === 'string') { + if (!error.toString().includes(expectError)) { + throw new Error(`expected error to contain '${expectError}' but was ${error}`); + } + } + } else { + if (error) { + throw error; + } + } +} \ No newline at end of file diff --git a/test/mocha-support.js b/test/mocha-support.js index a4ee094..66e266a 100644 --- a/test/mocha-support.js +++ b/test/mocha-support.js @@ -5,4 +5,3 @@ export const before = globalThis.before; export const after = globalThis.after; export const beforeEach = globalThis.beforeEach; export const afterEach = globalThis.afterEach; - diff --git a/test/mocha.js b/test/mocha.js index fe5dbcf..c3e0c12 100644 --- a/test/mocha.js +++ b/test/mocha.js @@ -1,4 +1,4 @@ -// mocha@10.0.0 in javascript ES2018 +// mocha@10.4.0 in javascript ES2018 (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : @@ -972,6 +972,11 @@ ? global$2.TYPED_ARRAY_SUPPORT : true; + /* + * Export kMaxLength after typed array support is determined. + */ + kMaxLength$1(); + function kMaxLength$1 () { return Buffer$1.TYPED_ARRAY_SUPPORT ? 0x7fffffff @@ -8722,6 +8727,11 @@ ? global$1.TYPED_ARRAY_SUPPORT : true; + /* + * Export kMaxLength after typed array support is determined. + */ + kMaxLength(); + function kMaxLength () { return Buffer.TYPED_ARRAY_SUPPORT ? 0x7fffffff @@ -8813,6 +8823,8 @@ if (Buffer.TYPED_ARRAY_SUPPORT) { Buffer.prototype.__proto__ = Uint8Array.prototype; Buffer.__proto__ = Uint8Array; + if (typeof Symbol !== 'undefined' && Symbol.species && + Buffer[Symbol.species] === Buffer) ; } function assertSize (size) { @@ -10456,28 +10468,6 @@ var utils$3 = {}; - let urlAlphabet = - 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; - let customAlphabet = (alphabet, defaultSize = 21) => { - return (size = defaultSize) => { - let id = ''; - let i = size; - while (i--) { - id += alphabet[(Math.random() * alphabet.length) | 0]; - } - return id - } - }; - let nanoid = (size = 21) => { - let id = ''; - let i = size; - while (i--) { - id += urlAlphabet[(Math.random() * 64) | 0]; - } - return id - }; - var nonSecure = { nanoid, customAlphabet }; - var he = {exports: {}}; /*! https://mths.be/he v1.2.0 by @mathias | MIT license */ @@ -10831,8 +10821,6 @@ /** * Module dependencies. */ - - const {nanoid} = nonSecure; var path = require$$1; var util = require$$0$1; var he$1 = he.exports; @@ -10899,7 +10887,7 @@ .replace(/^\uFEFF/, '') // (traditional)-> space/name parameters body (lambda)-> parameters body multi-statement/single keep body content .replace( - /^function(?:\s*|\s+[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\s*\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\s*\}|((?:.|\n)*))$/, + /^function(?:\s*|\s[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\}|((?:.|\n)*))$/, '$1$2$3' ); @@ -11438,11 +11426,22 @@ MOCHA_ID_PROP_NAME }); + const uniqueIDBase = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'; + /** * Creates a new unique identifier + * Does not create cryptographically safe ids. + * Trivial copy of nanoid/non-secure * @returns {string} Unique identifier */ - exports.uniqueID = () => nanoid(); + exports.uniqueID = () => { + let id = ''; + for (let i = 0; i < 21; i++) { + id += uniqueIDBase[(Math.random() * 64) | 0]; + } + return id; + }; exports.assignNewMochaID = obj => { const id = exports.uniqueID(); @@ -12969,7 +12968,7 @@ * * @memberof Mocha.Runnable * @public - * @return {string} + * @return {string[]} */ Runnable$3.prototype.titlePath = function () { return this.parent.titlePath().concat([this.title]); @@ -13712,7 +13711,7 @@ * * @memberof Suite * @public - * @return {string} + * @return {string[]} */ Suite.prototype.titlePath = function () { var result = []; @@ -14434,11 +14433,22 @@ err = thrown2Error(err); } - try { - err.stack = - this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack); - } catch (ignore) { - // some environments do not take kindly to monkeying with the stack + // Filter the stack traces + if (!this.fullStackTrace) { + const alreadyFiltered = new Set(); + let currentErr = err; + + while (currentErr && currentErr.stack && !alreadyFiltered.has(currentErr)) { + alreadyFiltered.add(currentErr); + + try { + currentErr.stack = stackFilter(currentErr.stack); + } catch (ignore) { + // some environments do not take kindly to monkeying with the stack + } + + currentErr = currentErr.cause; + } } this.emit(constants$1.EVENT_TEST_FAIL, test, err); @@ -15469,6 +15479,56 @@ } }); + /** + * Traverses err.cause and returns all stack traces + * + * @private + * @param {Error} err + * @param {Set} [seen] + * @return {FullErrorStack} + */ + var getFullErrorStack = function (err, seen) { + if (seen && seen.has(err)) { + return { message: '', msg: '', stack: '' }; + } + + var message; + + if (typeof err.inspect === 'function') { + message = err.inspect() + ''; + } else if (err.message && typeof err.message.toString === 'function') { + message = err.message + ''; + } else { + message = ''; + } + + var msg; + var stack = err.stack || message; + var index = message ? stack.indexOf(message) : -1; + + if (index === -1) { + msg = message; + } else { + index += message.length; + msg = stack.slice(0, index); + // remove msg from stack + stack = stack.slice(index + 1); + + if (err.cause) { + seen = seen || new Set(); + seen.add(err); + const causeStack = getFullErrorStack(err.cause, seen); + stack += '\n Caused by: ' + causeStack.msg + (causeStack.stack ? '\n' + causeStack.stack : ''); + } + } + + return { + message, + msg, + stack + }; + }; + /** * Outputs the given `failures` as a list. * @@ -15489,7 +15549,6 @@ color('error stack', '\n%s\n'); // msg - var msg; var err; if (test.err && test.err.multiple) { if (multipleTest !== test) { @@ -15500,25 +15559,8 @@ } else { err = test.err; } - var message; - if (typeof err.inspect === 'function') { - message = err.inspect() + ''; - } else if (err.message && typeof err.message.toString === 'function') { - message = err.message + ''; - } else { - message = ''; - } - var stack = err.stack || message; - var index = message ? stack.indexOf(message) : -1; - if (index === -1) { - msg = message; - } else { - index += message.length; - msg = stack.slice(0, index); - // remove msg from stack - stack = stack.slice(index + 1); - } + var { message, msg, stack } = getFullErrorStack(err); // uncaught if (err.uncaught) { @@ -15796,6 +15838,15 @@ Base.consoleLog = consoleLog; Base.abstract = true; + + /** + * An object with all stack traces recursively mounted from each err.cause + * @memberof module:lib/reporters/base + * @typedef {Object} FullErrorStack + * @property {string} message + * @property {string} msg + * @property {string} stack + */ }(base$1, base$1.exports)); var dot = {exports: {}}; @@ -17296,7 +17347,7 @@ runner.once(EVENT_RUN_END, function () { Base.cursor.show(); for (var i = 0; i < self.numberOfLines; i++) { - write('\n'); + process.stdout.write('\n'); } self.epilogue(); }); @@ -17332,15 +17383,15 @@ var stats = this.stats; function draw(type, n) { - write(' '); - write(Base.color(type, n)); - write('\n'); + process.stdout.write(' '); + process.stdout.write(Base.color(type, n)); + process.stdout.write('\n'); } draw('green', stats.passes); draw('fail', stats.failures); draw('pending', stats.pending); - write('\n'); + process.stdout.write('\n'); this.cursorUp(this.numberOfLines); }; @@ -17374,9 +17425,9 @@ var self = this; this.trajectories.forEach(function (line) { - write('\u001b[' + self.scoreboardWidth + 'C'); - write(line.join('')); - write('\n'); + process.stdout.write('\u001b[' + self.scoreboardWidth + 'C'); + process.stdout.write(line.join('')); + process.stdout.write('\n'); }); this.cursorUp(this.numberOfLines); @@ -17393,25 +17444,25 @@ var dist = '\u001b[' + startWidth + 'C'; var padding = ''; - write(dist); - write('_,------,'); - write('\n'); + process.stdout.write(dist); + process.stdout.write('_,------,'); + process.stdout.write('\n'); - write(dist); + process.stdout.write(dist); padding = self.tick ? ' ' : ' '; - write('_|' + padding + '/\\_/\\ '); - write('\n'); + process.stdout.write('_|' + padding + '/\\_/\\ '); + process.stdout.write('\n'); - write(dist); + process.stdout.write(dist); padding = self.tick ? '_' : '__'; var tail = self.tick ? '~' : '^'; - write(tail + '|' + padding + this.face() + ' '); - write('\n'); + process.stdout.write(tail + '|' + padding + this.face() + ' '); + process.stdout.write('\n'); - write(dist); + process.stdout.write(dist); padding = self.tick ? ' ' : ' '; - write(padding + '"" "" '); - write('\n'); + process.stdout.write(padding + '"" "" '); + process.stdout.write('\n'); this.cursorUp(this.numberOfLines); }; @@ -17443,7 +17494,7 @@ */ NyanCat.prototype.cursorUp = function (n) { - write('\u001b[' + n + 'A'); + process.stdout.write('\u001b[' + n + 'A'); }; /** @@ -17454,7 +17505,7 @@ */ NyanCat.prototype.cursorDown = function (n) { - write('\u001b[' + n + 'B'); + process.stdout.write('\u001b[' + n + 'B'); }; /** @@ -17494,15 +17545,6 @@ return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; }; - /** - * Stdout helper. - * - * @param {string} string A message to write to stdout. - */ - function write(string) { - process.stdout.write(string); - } - NyanCat.description = '"nyan cat"'; }(nyan)); @@ -17668,6 +17710,7 @@ var attrs = { classname: test.parent.fullTitle(), name: test.title, + file: test.file, time: test.duration / 1000 || 0 }; @@ -17781,7 +17824,7 @@ var ret = obj; var key = SUITE_PREFIX + suite.title; - obj = obj[key] = obj[key] || {suite: suite}; + obj = obj[key] = obj[key] || {suite}; suite.suites.forEach(function (suite) { mapTOC(suite, obj); }); @@ -18111,7 +18154,7 @@ var total = runner.total; runner.once(EVENT_RUN_BEGIN, function () { - writeEvent(['start', {total: total}]); + writeEvent(['start', {total}]); }); runner.on(EVENT_TEST_PASS, function (test) { @@ -18643,9 +18686,9 @@ context.describe = context.context = function (title, fn) { return common$1.suite.create({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18658,9 +18701,9 @@ context.describe.skip = function (title, fn) { return common$1.suite.skip({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18670,9 +18713,9 @@ context.describe.only = function (title, fn) { return common$1.suite.only({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18765,9 +18808,9 @@ */ context.suite = function (title, fn) { return common$1.suite.create({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18776,9 +18819,9 @@ */ context.suite.skip = function (title, fn) { return common$1.suite.skip({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18787,9 +18830,9 @@ */ context.suite.only = function (title, fn) { return common$1.suite.only({ - title: title, - file: file, - fn: fn + title, + file, + fn }); }; @@ -18874,8 +18917,8 @@ suites.shift(); } return common$1.suite.create({ - title: title, - file: file, + title, + file, fn: false }); }; @@ -18889,8 +18932,8 @@ suites.shift(); } return common$1.suite.only({ - title: title, - file: file, + title, + file, fn: false }); }; @@ -19076,7 +19119,7 @@ }; var name = "mocha"; - var version = "10.0.0"; + var version = "10.4.0"; var homepage = "https://mochajs.org/"; var notifyLogo = "https://ibin.co/4QuRuGjXvl36.png"; var require$$17 = { @@ -19517,6 +19560,8 @@ * @see {@link Mocha#addFile} * @see {@link Mocha#run} * @see {@link Mocha#unloadFiles} + * @param {Object} [options] - Settings object. + * @param {Function} [options.esmDecorator] - Function invoked on esm module name right before importing it. By default will passthrough as is. * @returns {Promise} * @example * @@ -19525,7 +19570,7 @@ * .then(() => mocha.run(failures => process.exitCode = failures ? 1 : 0)) * .catch(() => process.exitCode = 1); */ - Mocha.prototype.loadFilesAsync = function () { + Mocha.prototype.loadFilesAsync = function ({esmDecorator} = {}) { var self = this; var suite = this.suite; this.lazyLoadFiles(true); @@ -19538,7 +19583,8 @@ function (file, resultModule) { suite.emit(EVENT_FILE_REQUIRE, resultModule, file, self); suite.emit(EVENT_FILE_POST_REQUIRE, commonjsGlobal, file, self); - } + }, + esmDecorator ); }; diff --git a/test/tests/binding-mixin-tests.js b/test/tests/binding-mixin-tests.js new file mode 100644 index 0000000..8764eb4 --- /dev/null +++ b/test/tests/binding-mixin-tests.js @@ -0,0 +1,459 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; + +async function createBindGroup(device, buffer) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: {}, + }, + ], + }); + buffer = buffer || device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer } }, + ], + }); + return bindGroup; +} + +async function createBindGroupWithDynamicOffsets(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + // entries are intentional not in binding order. + const bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { hasDynamicOffset: true, minBindingSize: 512 }, + }, + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: {}, + }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage', hasDynamicOffset: true, minBindingSize: 1024 }, + }, + ], + }); + const buffer2 = device.createBuffer({size: 1024, usage: GPUBufferUsage.UNIFORM}); + const buffer0 = device.createBuffer({size: 128, usage: GPUBufferUsage.UNIFORM}); + const buffer1 = device.createBuffer({size: 2048, usage: GPUBufferUsage.STORAGE}); + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 2, resource: { buffer: buffer2 } }, + { binding: 1, resource: { buffer: buffer1 } }, + { binding: 0, resource: { buffer: buffer0 } }, + ], + }); + return bindGroup; +} + +export function addBindingMixinTests({ + makePass, + endPass, +}) { + + describe('check errors on setBindGroup', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroup(device); + await expectValidationError(false, () => { + pass.setBindGroup(0, bindGroup); + }); + }); + + it('fails if ended', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroup(device); + endPass(pass); + await expectValidationError(true, () => { + pass.setBindGroup(0, bindGroup); + }); + }); + + it('bindGroup from different device', async () => { + const pass = await makePass(); + const bindGroup = await createBindGroup(); + await expectValidationError(true, () => { + pass.setBindGroup(0, bindGroup); + }); + }); + + it('index < 0', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroup(device); + await expectValidationError(true, () => { + pass.setBindGroup(-1, bindGroup); + }); + }); + + it('index > max', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroup(device); + await expectValidationError(true, () => { + pass.setBindGroup(device.limits.maxBindGroups, bindGroup); + }); + }); + + it('fails if buffer destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroup = await createBindGroup(device, buffer); + buffer.destroy(); + await expectValidationError(true, () => { + pass.setBindGroup(0, bindGroup); + }); + }); + + const addDynamicOffsetTests = (fn) => { + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroupWithDynamicOffsets(device); + await expectValidationError(false, () => { + pass.setBindGroup(0, bindGroup, ...fn([2048 - 1024, 1024 - 512])); + }); + }); + + it('fails if wrong number of dynamic offsets', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroupWithDynamicOffsets(device); + await expectValidationError('same number of dynamicOffsets', () => { + pass.setBindGroup(0, bindGroup, ...fn([0, 0, 0])); + }); + }); + + it('fails if dynamic offset out of range', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroupWithDynamicOffsets(device); + await expectValidationError('dynamic offset is out of range', () => { + pass.setBindGroup(0, bindGroup, ...fn([2048, 0])); + }); + }); + + it('fails if dynamic offset is not storage aligned', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroupWithDynamicOffsets(device); + await expectValidationError('device.limits.minStorageBufferOffsetAlignment', () => { + pass.setBindGroup(0, bindGroup, ...fn([128, 0])); + }); + }); + + it('fails if dynamic offset is not uniform aligned', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const bindGroup = await createBindGroupWithDynamicOffsets(device); + await expectValidationError('device.limits.minUniformBufferOffsetAlignment', () => { + pass.setBindGroup(0, bindGroup, ...fn([0, 128])); + }); + }); + }; + + describe('dynamic offsets (array)', () => { + + addDynamicOffsetTests(v => [v]); + + }); + + describe('dynamic offsets (Uint32Array)', () => { + + addDynamicOffsetTests(v => { + const a = new Uint32Array(v); + return [a, 0, a.length]; + }); + + }); + + }); + +} + +async function createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline, device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const { pass, pipeline } = await makePassAndPipeline(device, { + resourceWGSL: ` + @group(0) @binding(0) var u00: f32; + @group(0) @binding(1) var u01: f32; + @group(1) @binding(0) var u10: vec4f; + @group(2) @binding(0) var u20: vec4f; + `, + usageWGSL: ` + _ = u00; + _ = u01; + _ = u10; + _ = u20; + `, + }); + const u00Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u01Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u10Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u20Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroup0 = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: u00Buffer }}, + { binding: 1, resource: { buffer: u01Buffer }}, + ], + }); + const bindGroup1 = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(1), + entries: [ + { binding: 0, resource: { buffer: u10Buffer }}, + ], + }); + const bindGroup2 = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(2), + entries: [ + { binding: 0, resource: { buffer: u20Buffer }}, + ], + }); + return { + device, + pass, + pipeline, + u00Buffer, + u01Buffer, + u10Buffer, + u20Buffer, + bindGroup0, + bindGroup1, + bindGroup2, + }; +} + +async function createResourcesForExplicitLayoutBindGroupTests({ + makePassAndPipeline, + device, + visibility, +}) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + + const bindGroupLayouts = [ + { + entries: [ + { binding: 0, visibility, buffer: {} }, + { binding: 1, visibility, buffer: {} }, + ], + }, + { + entries: [ + { binding: 0, visibility, buffer: {} }, + ], + }, + { + entries: [ + { binding: 0, visibility, buffer: {} }, + ], + }, + ].map(bglDesc => device.createBindGroupLayout(bglDesc)); + + const layout = device.createPipelineLayout({ + bindGroupLayouts, + }); + const { pass, pipeline } = await makePassAndPipeline(device, { + layout, + resourceWGSL: ` + @group(0) @binding(0) var u00: f32; + @group(0) @binding(1) var u01: f32; + @group(1) @binding(0) var u10: vec4f; + @group(2) @binding(0) var u20: vec4f; + `, + usageWGSL: ` + _ = u00; + _ = u01; + _ = u10; + _ = u20; + `, + }); + const u00Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u01Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u10Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const u20Buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + + const bindGroup0 = device.createBindGroup({ + layout: bindGroupLayouts[0], + entries: [ + { binding: 0, resource: { buffer: u00Buffer }}, + { binding: 1, resource: { buffer: u01Buffer }}, + ], + }); + const bindGroup1 = device.createBindGroup({ + layout: bindGroupLayouts[1], + entries: [ + { binding: 0, resource: { buffer: u10Buffer }}, + ], + }); + const bindGroup2 = device.createBindGroup({ + layout: bindGroupLayouts[2], + entries: [ + { binding: 0, resource: { buffer: u20Buffer }}, + ], + }); + return { + device, + pass, + pipeline, + u00Buffer, + u01Buffer, + u10Buffer, + u20Buffer, + bindGroup0, + bindGroup1, + bindGroup2, + }; +} + +export function addValidateBindGroupTests({ + makePassAndPipeline, + execute, + visibility, +}) { + + describe('validate bindGroups tests', () => { + + describe('auto layout', () => { + + it('works with auto layout', async () => { + const { pass, bindGroup0, bindGroup1, bindGroup2 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(false, async () => { + await execute(pass); + }); + }); + + it('fails if missing bindGroup', async () => { + const { pass, bindGroup1, bindGroup2 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(true, async () => { + await execute(pass); + }); + }); + + it('fails if resource is destroyed', async () => { + const { pass, u01Buffer, bindGroup0, bindGroup1, bindGroup2 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + u01Buffer.destroy(); + + await expectValidationError(true, async () => { + await execute(pass); + }); + }); + + it('fails if layout is incompatible (auto layout)', async () => { + const { device, pass, bindGroup0, bindGroup2 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + const { bindGroup1 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline, device); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(true, async () => { + await execute(pass); + }); + }); + + it('works if layout is compatible (auto layout)', async () => { + const { pass, bindGroup0, bindGroup1, bindGroup2 } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup2); // 2 and 1 are swapped but + pass.setBindGroup(2, bindGroup1); // they should be compatible + + await expectValidationError(false, async () => { + await execute(pass); + }); + }); + + it('false if layout is incompatible (auto layout + manual bindGroupLayout)', async () => { + const { device, pass, bindGroup0, bindGroup1, u20Buffer } = await createResourcesForAutoLayoutBindGroupTests(makePassAndPipeline); + const bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: {}, + }, + ], + }); + const bindGroup2 = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: u20Buffer }}, + ], + }); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(true, async () => { + await execute(pass); + }); + }); + + }); + + describe('explicit layout', () => { + + it('works with explicit layout', async () => { + const { pass, bindGroup0, bindGroup1, bindGroup2 } = await createResourcesForExplicitLayoutBindGroupTests({ makePassAndPipeline, visibility }); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(false, async () => { + await execute(pass); + }); + }); + + it('works with different explicit layout if they are compatible', async () => { + const { device, pass, bindGroup0, bindGroup1 } = await createResourcesForExplicitLayoutBindGroupTests({ makePassAndPipeline, visibility }); + const { bindGroup2 } = await createResourcesForExplicitLayoutBindGroupTests({ makePassAndPipeline, device, visibility }); + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(false, async () => { + await execute(pass); + }); + }); + + it('fails with incompatible bindGroup', async () => { + const { pass, bindGroup1, bindGroup2 } = await createResourcesForExplicitLayoutBindGroupTests({ makePassAndPipeline, visibility }); + pass.setBindGroup(0, bindGroup1); // incompatible + pass.setBindGroup(1, bindGroup1); + pass.setBindGroup(2, bindGroup2); + + await expectValidationError(true, async () => { + await execute(pass); + }); + }); + + }); + + }); + +} \ No newline at end of file diff --git a/test/tests/canvas-context-tests.js b/test/tests/canvas-context-tests.js new file mode 100644 index 0000000..14e915e --- /dev/null +++ b/test/tests/canvas-context-tests.js @@ -0,0 +1,44 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +async function createRenderPass(device, encoder, texture) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + encoder = encoder || await createCommandEncoder(device); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + return pass; +} + +describe('test canvas context', () => { + + describe('test getCurrentTexture', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const context = new OffscreenCanvas(1, 1).getContext('webgpu'); + context.configure({ + device, + format: 'rgba8unorm', + }); + const texture = context.getCurrentTexture(); + await expectValidationError(false, async () => { + await createRenderPass(device, undefined, texture); + }); + }); + + }); + +}); diff --git a/test/tests/command-encoder-tests.js b/test/tests/command-encoder-tests.js new file mode 100644 index 0000000..a020d5c --- /dev/null +++ b/test/tests/command-encoder-tests.js @@ -0,0 +1,129 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; +import copyBufferToBufferTests from './command-encoder/copyBufferToBuffer-tests.js'; +import copyBufferToTextureTests from './command-encoder/copyBufferToTexture-tests.js'; +import copyTextureToBufferTests from './command-encoder/copyTextureToBuffer-tests.js'; +import copyTextureToTextureTests from './command-encoder/copyTextureToTexture-tests.js'; + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +describe('test command encoder', () => { + + describe('test finish', () => { + + it('can not finish twice', async () => { + const encoder = await createCommandEncoder(); + encoder.finish(); + await expectValidationError(true, async () => { + encoder.finish(); + }); + }); + + it('can not finish if locked', async () => { + const encoder = await createCommandEncoder(); + encoder.beginComputePass(); + await expectValidationError(true, async () => { + encoder.finish(); + }); + }); + + }); + + describe('test clearBuffer', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(false, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if encoder is locked', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + encoder.beginComputePass(); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if encoder is finished', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + encoder.finish(); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if buffer is destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + buffer.destroy(); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if buffer is from a different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const device2 = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device2.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if buffer.usage missing COPY_DST', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer); + }); + }); + + it('fails if size is not multiple of 4', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer, 0, 3); + }); + }); + + it('fails if offset is not multiple of 4', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer, 1, 4); + }); + }); + + it('fails if offset + size > buffer.size', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.clearBuffer(buffer, 12, 8); + }); + }); + + }); + + copyBufferToBufferTests(); + copyBufferToTextureTests(); + copyTextureToBufferTests(); + copyTextureToTextureTests(); + +}); diff --git a/test/tests/command-encoder/copy-utils.js b/test/tests/command-encoder/copy-utils.js new file mode 100644 index 0000000..4c16e66 --- /dev/null +++ b/test/tests/command-encoder/copy-utils.js @@ -0,0 +1,476 @@ +import { + assertTruthy, +} from '../../assert.js'; +import {it} from '../../mocha-support.js'; +import {expectValidationError, bufferUsageToString, textureUsageToString } from '../../js/utils.js'; + +export async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +export async function createDeviceWith4x4Format16BytesPerPixel() { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice({requiredFeatures: adapter.features}); + + // Pick a format with 4x4 blockSize and 16 bytes per pixel + const format = [ + { feature: 'texture-compression-bc', format: 'bc2-rgba-unorm' }, + { feature: 'texture-compression-etc2', format: 'etc2-rgba8unorm"' }, + { feature: 'texture-compression-astc', format: 'astc-4x4-unorm' }, + ].find(({feature}) => device.features.has(feature)).format; + + assertTruthy(format); + + return { device, format }; +} + +export function addCopyTests({ + doTest, + bufferUsage, + textureUsage, +}) { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(false, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if encoder is locked', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + encoder.beginComputePass(); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if encoder is finished', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + encoder.finish(); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if buffer is destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + buffer.destroy(); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if buffer is from a different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const device2 = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device2.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if texture is from a different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const device2 = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device2.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if bytesPerRow is not multiple of 256', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 257 }, + { texture }, + [4, 4], + ); + }); + }); + + it(`fails if texture.usage does not include ${textureUsageToString(textureUsage)}`, async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it(`fails if buffer.usage does not include ${bufferUsageToString(bufferUsage)}`, async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: GPUBufferUsage.UNIFORM}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if sampleCount not 1', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage | GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: 4, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if depth and incorrect aspect', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'depth24plus-stencil8', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if stencil and incorrect aspect', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'depth24plus-stencil8', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails if not write copyable', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'depth24plus', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture, aspect: 'depth-only' }, + [4, 4], + ); + }); + }); + + it('fails if origin.x + copySize.width > width', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture, origin: [3] }, + [2, 2], + ); + }); + }); + + it('fails if origin.y + copySize.height > height', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture, origin: [0, 3] }, + [2, 2], + ); + }); + }); + + it('fails if origin.z + copySize.depthOrArrayLayers > depthOrArrayLayers', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256, rowsPerImage: 2 }, + { texture, origin: [0, 0, 3] }, + [2, 2, 2], + ); + }); + }); + + it('fails copySize.width not multiple of blockWidth', async () => { + const { device, format } = await createDeviceWith4x4Format16BytesPerPixel(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format, + size: [8, 8], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [5, 4], + ); + }); + }); + + it('fails copySize.height not multiple of blockHeight', async () => { + const { device, format } = await createDeviceWith4x4Format16BytesPerPixel(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format, + size: [8, 8], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256 }, + { texture }, + [4, 3], + ); + }); + }); + + it('fails src offset is not multiple of blockSize in bytes', async () => { + const { device, format } = await createDeviceWith4x4Format16BytesPerPixel(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format, + size: [8, 8], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256, offset: 12 }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails copy.height > 1 and bytesPerRow not set', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer }, + { texture }, + [4, 4], + ); + }); + }); + + it('fails copy.height = 1 and copySize.depthOrArrayLayers > 1 and bytesPerRow not set', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, rowsPerImage: 2 }, + { texture }, + [4, 1, 2], + ); + }); + }); + + it('fails copy.height = 1 and copySize.depthOrArrayLayers > 1 and rowsPerImage not set', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerImage: 256 }, + { texture }, + [4, 1, 2], + ); + }); + }); + + it('fails if buffer range out of bounds', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const buffer = device.createBuffer({size: 2048, usage: bufferUsage}); + const texture = device.createTexture({ + format: 'rgba8unorm', + size: [4, 4], + usage: textureUsage, + }); + await expectValidationError(true, async () => { + doTest( + encoder, + { buffer, bytesPerRow: 256, offset: 1536 }, + { texture }, + [4, 4], + ); + }); + }); +} diff --git a/test/tests/command-encoder/copyBufferToBuffer-tests.js b/test/tests/command-encoder/copyBufferToBuffer-tests.js new file mode 100644 index 0000000..a243d72 --- /dev/null +++ b/test/tests/command-encoder/copyBufferToBuffer-tests.js @@ -0,0 +1,115 @@ +import {describe, it} from '../../mocha-support.js'; +import {expectValidationError} from '../../js/utils.js'; + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +export default function () { + describe('test copyBufferToBuffer', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(false, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if src not same device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const device2 = await (await navigator.gpu.requestAdapter()).requestDevice(); + const src = device2.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if dst not same device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const device2 = await (await navigator.gpu.requestAdapter()).requestDevice(); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device2.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if src = dst', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, src, 0, 16); + }); + }); + + it('fails if src destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + src.destroy(); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if dst destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + dst.destroy(); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if src.usage missing COPY_SRC', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + it('fails if dst.usage missing COPY_DST', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.UNIFORM}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, 0, dst, 0, 16); + }); + }); + + const tests = [ + { srcOffset: 24, desc: 'srcOffset + size > src.size' }, + { dstOffset: 24, desc: 'dstOffset + size > dst.size' }, + { size: 15, desc: 'size not multiple of 4' }, + { size: 8, srcOffset: 1, desc: 'srcOffset not multiple of 4' }, + { size: 8, dstOffset: 1, desc: 'dstOffset not multiple of 4' }, + ]; + for (const {srcOffset = 0, dstOffset = 0, size = 16, desc} of tests) { + it(desc, async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_SRC}); + const dst = device.createBuffer({size: 32, usage: GPUBufferUsage.COPY_DST}); + await expectValidationError(true, async () => { + encoder.copyBufferToBuffer(src, srcOffset, dst, dstOffset, size); + }); + }); + } + + }); +} \ No newline at end of file diff --git a/test/tests/command-encoder/copyBufferToTexture-tests.js b/test/tests/command-encoder/copyBufferToTexture-tests.js new file mode 100644 index 0000000..f32621f --- /dev/null +++ b/test/tests/command-encoder/copyBufferToTexture-tests.js @@ -0,0 +1,17 @@ +import {describe} from '../../mocha-support.js'; +import {addCopyTests} from './copy-utils.js'; + +export default function () { + describe('test copyBufferToTexture', () => { + + addCopyTests({ + doTest: (encoder, buffer, texture, copySize) => { + encoder.copyBufferToTexture(buffer, texture, copySize); + }, + bufferUsage: GPUBufferUsage.COPY_SRC, + textureUsage: GPUTextureUsage.COPY_DST, + }); + + }); + +} diff --git a/test/tests/command-encoder/copyTextureToBuffer-tests.js b/test/tests/command-encoder/copyTextureToBuffer-tests.js new file mode 100644 index 0000000..9a0a5dd --- /dev/null +++ b/test/tests/command-encoder/copyTextureToBuffer-tests.js @@ -0,0 +1,17 @@ +import {describe} from '../../mocha-support.js'; +import {addCopyTests} from './copy-utils.js'; + +export default function () { + describe('test copyTextureToBuffer', () => { + + addCopyTests({ + doTest: (encoder, buffer, texture, copySize) => { + encoder.copyTextureToBuffer(texture, buffer, copySize); + }, + bufferUsage: GPUBufferUsage.COPY_DST, + textureUsage: GPUTextureUsage.COPY_SRC, + }); + + }); + +} diff --git a/test/tests/command-encoder/copyTextureToTexture-tests.js b/test/tests/command-encoder/copyTextureToTexture-tests.js new file mode 100644 index 0000000..bf0feb7 --- /dev/null +++ b/test/tests/command-encoder/copyTextureToTexture-tests.js @@ -0,0 +1,142 @@ +import {describe, it} from '../../mocha-support.js'; +import {expectValidationError } from '../../js/utils.js'; +import {createCommandEncoder, createDeviceWith4x4Format16BytesPerPixel} from './copy-utils.js'; + +export default function () { + describe('test copyTextureToTexture', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_SRC }); + const dst = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_DST }); + await expectValidationError(false, async () => { + encoder.copyTextureToTexture( + { texture: src }, + { texture: dst }, + [4, 4], + ); + }); + }); + + it('fails if encoder is locked', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + encoder.beginComputePass(); + const src = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_SRC }); + const dst = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_DST }); + await expectValidationError(true, async () => { + encoder.copyTextureToTexture( + { texture: src }, + { texture: dst }, + [4, 4], + ); + }); + }); + + it('fails if encoder is finished', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + encoder.finish(); + const src = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_SRC }); + const dst = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_DST }); + await expectValidationError(true, async () => { + encoder.copyTextureToTexture( + { texture: src }, + { texture: dst }, + [4, 4], + ); + }); + }); + + it('fails if src sampleCount != dst sampleCount', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const src = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_SRC }); + const dst = device.createTexture({ format: 'rgba8unorm', size: [4, 4], usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, sampleCount: 4 }); + await expectValidationError(true, async () => { + encoder.copyTextureToTexture( + { texture: src }, + { texture: dst }, + [4, 4], + ); + }); + }); + + describe('fails if src = dst and box overlaps', () => { + const overlapTests = [ + { fail: false, origin: [2, 2, 2], desc: 'no overlap'}, + { fail: true, origin: [1, 2, 2], desc: 'overlap x'}, + { fail: true, origin: [2, 1, 2], desc: 'overlap y'}, + { fail: true, origin: [2, 1, 1], desc: 'overlap z'}, + ]; + + for (const {fail, origin, desc} of overlapTests) { + it(desc, async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + const texture = device.createTexture({ format: 'rgba8unorm', size: [4, 4, 4], usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST }); + await expectValidationError(fail, async () => { + encoder.copyTextureToTexture( + { texture, origin }, + { texture }, + [2, 2, 2], + ); + }); + }); + } + }); + + const sdTests = [ + { options: { mipLevel: 1 }, desc: 'fails if $.mipLevel > $.textureMipLevelCount' }, + { options: { origin: [1] }, desc: 'fails if $.originX is not a multiple of blockWidth' }, + { options: { origin: [0, 1] }, desc: 'fails if $.originY is not a multiple of blockWidth' }, + { options: { origin: [8]}, desc: 'fails if $.originX + copySize.width > texture.width' }, + { options: { origin: [0, 8]}, desc: 'fails if $.originY + copySize.height > texture.width' }, + { destroy: true, desc: 'fails if $ destroyed' }, + { usage: true, desc: 'fails if $ usage incorrect' }, + { otherDevice: true, desc: 'fails if $ from different device' }, + ]; + + // test depth-stencil must be entire texture if sampleCount > 1 + // test depth-stencil must be aspect "all" + + ['src', 'dst'].forEach((sd, i) => { + describe(sd, () => { + for (const {options, destroy, usage, otherDevice, desc} of sdTests) { + it(desc.replaceAll('$', sd), async () => { + const { device, format } = await createDeviceWith4x4Format16BytesPerPixel(); + const encoder = await createCommandEncoder(device); + const usages = [GPUTextureUsage.COPY_SRC, GPUTextureUsage.COPY_DST]; + if (usage) { + usages[i] = GPUTextureUsage.TEXTURE_BINDING; + } + const devices = [device, device]; + if (otherDevice) { + devices[i] = (await createDeviceWith4x4Format16BytesPerPixel()).device; + } + const [src, dst] = usages.map((usage, i) => devices[i].createTexture({ format, size: [8, 8], usage })); + if (destroy) { + [src, dst][i].destroy(); + } + + const args = [ + { texture: src }, + { texture: dst }, + [4, 4], + ]; + Object.assign(args[i], options); + + await expectValidationError(true, async () => { + encoder.copyTextureToTexture(...args); + }); + + }); + } + }); + }); + + }); + +} + diff --git a/test/tests/compute-pass-tests.js b/test/tests/compute-pass-tests.js new file mode 100644 index 0000000..9e652ff --- /dev/null +++ b/test/tests/compute-pass-tests.js @@ -0,0 +1,209 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; +import {addValidateBindGroupTests} from './binding-mixin-tests.js'; +import {addTimestampWriteTests} from './timestamp-tests.js'; + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +async function createComputePass(device, encoder, { timestampWrites } = {}) { + encoder = encoder || await createCommandEncoder(device); + const pass = encoder.beginComputePass({ + ...(timestampWrites && { timestampWrites }), + }); + return pass; +} + +async function createComputePipeline(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const module = device.createShaderModule({ + code: ` + @compute @workgroup_size(1) fn csMain() { } + `, + }); + const pipeline = device.createComputePipeline({ + layout: 'auto', + compute: { module }, + }); + return pipeline; +} + +async function createComputeBindGroupPipeline(device, { + resourceWGSL, + usageWGSL, + layout = 'auto', +}) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const module = device.createShaderModule({ + code: ` + ${resourceWGSL} + @compute @workgroup_size(1) fn csMain() { + ${usageWGSL}; + } + `, + }); + const pipeline = device.createComputePipeline({ + layout, + compute: { module }, + }); + const indirectBuffer = device.createBuffer({size: 12, usage: GPUBufferUsage.INDIRECT }); + return { pipeline, indirectBuffer }; +} + +describe('test compute pass encoder', () => { + + describe('check errors on beginComputePass', () => { + + it('errors if 2 passes are started', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + await createComputePass(device, encoder); + await expectValidationError(true, async () => { + await createComputePass(device, encoder); + }); + }); + + it('can not end twice', async () => { + const pass = await createComputePass(); + pass.end(); + await expectValidationError(true, async () => { + pass.end(); + }); + }); + + addTimestampWriteTests({ + makePass(device, {timestampWrites}) { + return createComputePass(device, undefined, { timestampWrites }); + }, + }); + + }); + + describe('check errors on setPipeline', () => { + + it('pipeline from different device', async () => { + const pipeline = await createComputePipeline(); + const pass = await createComputePass(); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if ended', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const pass = await createComputePass(device); + pass.end(); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + }); + + describe('dispatchWorkgroups', () => { + + const tests = [ + { expectError: false, args: [1], desc: 'works' }, + { expectError: true, args: [100000000] , desc: 'x too big' }, + { expectError: true, args: [1, 100000000] , desc: 'y too big' }, + { expectError: true, args: [1, 1, 100000000] , desc: 'z too big' }, + ]; + for (const {expectError, args, desc} of tests) { + it(desc, async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const pass = await createComputePass(device); + pass.setPipeline(pipeline); + await expectValidationError(expectError, () => { + pass.dispatchWorkgroups(...args); + }); + }); + } + + addValidateBindGroupTests({ + makePassAndPipeline: async (device, options) => { + const { pipeline } = await createComputeBindGroupPipeline(device, options); + const encoder = device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + return {pass, pipeline}; + }, + execute(pass) { + pass.dispatchWorkgroups(1); + }, + visibility: GPUShaderStage.COMPUTE, + }); + + }); + + describe('dispatchWorkgroupsIndirect', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const indirectBuffer = device.createBuffer({size: 12, usage: GPUBufferUsage.INDIRECT}); + const pass = await createComputePass(device); + pass.setPipeline(pipeline); + await expectValidationError(false, () => { + pass.dispatchWorkgroupsIndirect(indirectBuffer, 0); + }); + }); + + it('fails if indirectBuffer destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const indirectBuffer = device.createBuffer({size: 12, usage: GPUBufferUsage.INDIRECT}); + const pass = await createComputePass(device); + pass.setPipeline(pipeline); + indirectBuffer.destroy(); + await expectValidationError(true, () => { + pass.dispatchWorkgroupsIndirect(indirectBuffer, 0); + }); + }); + + it('fails if indirect offset outside data', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const indirectBuffer = device.createBuffer({size: 12, usage: GPUBufferUsage.INDIRECT}); + const pass = await createComputePass(device); + pass.setPipeline(pipeline); + await expectValidationError(true, () => { + pass.dispatchWorkgroupsIndirect(indirectBuffer, 4); + }); + }); + + it('fails if indirect size too small', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createComputePipeline(device); + const indirectBuffer = device.createBuffer({size: 8, usage: GPUBufferUsage.INDIRECT}); + const pass = await createComputePass(device); + pass.setPipeline(pipeline); + await expectValidationError(true, () => { + pass.dispatchWorkgroupsIndirect(indirectBuffer, 0); + }); + }); + + addValidateBindGroupTests((() => { + let ib; + return { + makePassAndPipeline: async (device, options) => { + const { pipeline, indirectBuffer } = await createComputeBindGroupPipeline(device, options); + ib = indirectBuffer; + const encoder = device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + return {pass, pipeline}; + }, + execute(pass) { + pass.dispatchWorkgroupsIndirect(ib, 0); + }, + visibility: GPUShaderStage.COMPUTE, + }; + })()); + + }); + +}); diff --git a/test/tests/device-tests.js b/test/tests/device-tests.js new file mode 100644 index 0000000..22ae8a2 --- /dev/null +++ b/test/tests/device-tests.js @@ -0,0 +1,84 @@ + +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; + +async function createBindGroupLayout(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: {}, + }, + ], + }); +} + +async function createBindGroup(device, buffer) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + buffer = buffer || await device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroupLayout = createBindGroupLayout(device); + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer } }, + ], + }); + return bindGroup; +} + +describe('test device', () => { + + describe('test createBindGroup', () => { + + it('fails if resource buffer is destroyed', async () => { + + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + buffer.destroy(); + await expectValidationError(true, async () => { + await createBindGroup(device, buffer); + }); + + }); + + /* TODO: finish buffer tests + it('fails if size > buffer.size', async () => { + + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroupLayout = await createBindGroupLayout(device); + + await expectValidationError(true, async () => { + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer, size: 32 } }, + ], + }); + }); + + }); + + it('fails if offset + size > buffer.size', async () => { + + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 16, usage: GPUBufferUsage.UNIFORM}); + const bindGroupLayout = await createBindGroupLayout(device); + + await expectValidationError(true, async () => { + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer, offset: 16, size: 16 } }, + ], + }); + }); + + }); + */ + + }); + +}); diff --git a/test/tests/push-pop-error-tests.js b/test/tests/push-pop-error-tests.js new file mode 100644 index 0000000..32c60e7 --- /dev/null +++ b/test/tests/push-pop-error-tests.js @@ -0,0 +1,90 @@ +import {describe, it} from '../mocha-support.js'; +import { assertFalsy, assertInstanceOf } from '../assert.js'; + +describe('test push/pop error scope', () => { + + it('test we get error (because they are being captured)', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + + device.pushErrorScope('validation'); + device.createSampler({maxAnisotropy: 0}); + const err = await device.popErrorScope(); + assertInstanceOf(err, GPUValidationError); + }); + + it('test we get errors nested', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + + device.pushErrorScope('validation'); + device.pushErrorScope('validation'); + device.createSampler({maxAnisotropy: 0}); + device.pushErrorScope('validation'); + device.createSampler({maxAnisotropy: 0}); + device.pushErrorScope('validation'); + const innerErr = await device.popErrorScope(); + const middleErr = await device.popErrorScope(); + const outerErr = await device.popErrorScope(); + const rootErr = await device.popErrorScope(); + assertFalsy(innerErr, GPUValidationError); + assertInstanceOf(middleErr, GPUValidationError); + assertInstanceOf(outerErr, GPUValidationError); + assertFalsy(rootErr); + }); + + it('test we get errors nested - more', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + + device.pushErrorScope('validation'); + device.pushErrorScope('validation'); + device.pushErrorScope('validation'); + device.pushErrorScope('validation'); + device.createSampler({maxAnisotropy: 0}); + device.createSampler({maxAnisotropy: 0}); + device.createSampler({maxAnisotropy: 0}); + const innerErr = await device.popErrorScope(); + const middleErr = await device.popErrorScope(); + const outerErr = await device.popErrorScope(); + device.createSampler({maxAnisotropy: 0}); + const rootErr = await device.popErrorScope(); + assertInstanceOf(innerErr, GPUValidationError); + assertFalsy(middleErr); + assertFalsy(outerErr); + assertInstanceOf(rootErr, GPUValidationError); + }); + + it('test we get uncaught errors', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + + const promise = new Promise(resolve => { + device.addEventListener('uncapturederror', (e) => { + resolve(e.error); + }); + }); + + //device.pushErrorScope('validation'); + // device.pushErrorScope('validation'); + // device.pushErrorScope('validation'); + // device.pushErrorScope('validation'); + // device.createSampler({maxAnisotropy: 0}); + // device.createSampler({maxAnisotropy: 0}); + // device.createSampler({maxAnisotropy: 0}); + // const innerErr = await device.popErrorScope(); + // const middleErr = await device.popErrorScope(); + // const outerErr = await device.popErrorScope(); + // device.createSampler({maxAnisotropy: 0}); + //const rootErr = await device.popErrorScope(); + device.createSampler({maxAnisotropy: 0}); + + // we need to do something to flush the commands. + device.queue.submit([]); + + const uncapturedError = await promise; + + //assertInstanceOf(innerErr, GPUValidationError); + //assertFalsy(middleErr); + //assertFalsy(outerErr); + //assertInstanceOf(rootErr, GPUValidationError); + assertInstanceOf(uncapturedError, GPUValidationError); + }); + +}); diff --git a/test/tests/render-bundle-tests.js b/test/tests/render-bundle-tests.js new file mode 100644 index 0000000..74edd33 --- /dev/null +++ b/test/tests/render-bundle-tests.js @@ -0,0 +1,97 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; +import {addBindingMixinTests} from './binding-mixin-tests.js'; +import {addRenderMixinTests} from './render-mixin-tests.js'; + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +async function createRenderPass(device, encoder) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + encoder = encoder || await createCommandEncoder(device); + const texture = device.createTexture({ + size: [2, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + return pass; +} + +async function createRenderPipeline(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const module = device.createShaderModule({ + code: ` + @vertex fn vsMain() -> @builtin(position) vec4f { return vec4f(0); } + @fragment fn fsMain() -> @location(0) vec4f { return vec4f(0); } + `, + }); + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { module, targets: [ { format: 'rgba8unorm' } ]}, + }); + return pipeline; +} + +async function createRenderBundleEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + }); +} + +describe('test render bundle encoder', () => { + + addRenderMixinTests({ + makePass: async (device) => { + return await createRenderBundleEncoder(device); + }, + endPass(pass) { + pass.finish(); + }, + }); + + describe('check errors on setPipeline', () => { + + it('pipeline from different device', async () => { + const pipeline = await createRenderPipeline(); + const pass = await createRenderPass(); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if ended', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device); + const pass = await createRenderPass(device); + pass.end(); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + }); + + addBindingMixinTests({ + makePass: async (device) => { + return await createRenderBundleEncoder(device); + }, + endPass(pass) { + pass.finish(); + }, + }); + +}); diff --git a/test/tests/render-mixin-tests.js b/test/tests/render-mixin-tests.js new file mode 100644 index 0000000..6872cba --- /dev/null +++ b/test/tests/render-mixin-tests.js @@ -0,0 +1,890 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; +import {addValidateBindGroupTests} from './binding-mixin-tests.js'; + +async function createRenderPipeline(device, { + format = 'rgba8unorm', + sampleCount, + depthStencilFormat, +} = {}) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const module = device.createShaderModule({ + code: ` + @vertex fn vsMain( + @location(0) a0 : vec4f, + @location(1) a1 : vec4f, + @location(2) a2 : vec4f, + ) -> @builtin(position) vec4f { + return a0 + a1 + a2; + } + @fragment fn fsMain() -> @location(0) vec4f { return vec4f(0); } + `, + }); + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module, + buffers: [ + { + arrayStride: 6 * 4, // 6 floats, 4 bytes each + attributes: [ + {shaderLocation: 0, offset: 0, format: 'float32x3'}, + {shaderLocation: 1, offset: 12, format: 'float32x3'}, + ], + }, + { + stepMode: 'instance', + arrayStride: 3 * 4, // 3 floats, 4 bytes each + attributes: [ + {shaderLocation: 2, offset: 0, format: 'float32x3'}, + ], + }, + ], + }, + fragment: { module, targets: [ { format } ]}, + ...(depthStencilFormat && { + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: depthStencilFormat, + }, + }), + ...(sampleCount && { + multisample: { + count: sampleCount, + }, + }), + }); + return pipeline; +} + +const kVertexSize = 24; +const kNumVertices = 4; +const kNumInstances = 4; +const kInstanceSize = 12; +const kNumIndices = 20; +const kIndexSize = 2; +const kIndexFormat = 'uint16'; + +async function createRenderPipelineAndAttribResources(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device); + const vertexBuffer0 = device.createBuffer({size: kVertexSize * kNumVertices, usage: GPUBufferUsage.VERTEX}); + const vertexBuffer1 = device.createBuffer({size: kInstanceSize * kNumInstances, usage: GPUBufferUsage.VERTEX}); + const indexBuffer = device.createBuffer({size: kNumIndices * kIndexSize, usage: GPUBufferUsage.INDEX}); + const indirectBuffer = device.createBuffer({size: 40, usage: GPUBufferUsage.INDIRECT}); + + return { + pipeline, + device, + vertexBuffer0, + vertexBuffer1, + indexBuffer, + indirectBuffer, + }; +} + +async function createRenderBindGroupPipeline(device, { + resourceWGSL, + usageWGSL, + layout = 'auto', +}) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + const module = device.createShaderModule({ + code: ` + ${resourceWGSL} + @vertex fn vsMain() -> @builtin(position) vec4f { + ${usageWGSL}; + return vec4f(0); + } + @fragment fn fsMain() -> @location(0) vec4f { return vec4f(0); } + `, + }); + const pipeline = device.createRenderPipeline({ + layout, + vertex: { module }, + fragment: { module, targets: [ { format: 'rgba8unorm' } ]}, + }); + const indexBuffer = device.createBuffer({size: kNumIndices * kIndexSize, usage: GPUBufferUsage.INDEX}); + const indirectBuffer = device.createBuffer({size: 40, usage: GPUBufferUsage.INDIRECT}); + return { pipeline, indexBuffer, indirectBuffer }; +} + +export function addRenderMixinTests({ + makePass, + endPass, +}) { + + describe('check errors on setPipeline', () => { + + it('pipeline from different device', async () => { + const pipeline = await createRenderPipeline(); + const pass = await makePass(); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if ended', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device); + const pass = await makePass(device); + endPass(pass); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if pipeline colorFormats do not match', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device, {format: 'r8unorm'}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if pipeline sampleCount does not match', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device, {sampleCount: 4}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + it('fails if pipeline depthStencilFormat does not match', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pipeline = await createRenderPipeline(device, {depthStencilFormat: 'depth24plus'}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setPipeline(pipeline); + }); + }); + + }); + + + describe('check errors on setVertexBuffer', () => { + + it('works with null', async () => { + const pass = await makePass(); + await expectValidationError(false, () => { + pass.setVertexBuffer(0, null); + }); + }); + + it('fails with null if ended', async () => { + const pass = await makePass(); + endPass(pass); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, null); + }); + }); + + it('works with buffer', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.VERTEX}); + await expectValidationError(false, () => { + pass.setVertexBuffer(0, buffer); + }); + }); + + it('errors if buffer is destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.VERTEX}); + buffer.destroy(); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, buffer); + }); + }); + + it('slot < 0', async () => { + const pass = await makePass(); + await expectValidationError(true, () => { + pass.setVertexBuffer(-1, null); + }); + }); + + it('slot > max', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(device.limits.maxVertexBuffers, null); + }); + }); + + it('offset is not multiple of 4', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, null, 3); + }); + }); + + it('offset + size > bufferSize (no buffer)', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, null, 4); + }); + }); + + it('offset + size > bufferSize (w/buffer via size)', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.VERTEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, buffer, 0, 5); + }); + }); + + it('offset + size > bufferSize (w/buffer via offset + size)', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 8, usage: GPUBufferUsage.VERTEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, buffer, 4, 5); + }); + }); + + it('buffer from different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.VERTEX}); + const pass = await makePass(); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, buffer); + }); + }); + + it('buffer not VERTEX', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setVertexBuffer(0, buffer); + }); + }); + + }); + + describe('check errors on setIndexBuffer', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(false, () => { + pass.setIndexBuffer(buffer, 'uint16'); + }); + }); + + it('errors if buffer is destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + buffer.destroy(); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16'); + }); + }); + + it('fails if ended', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + endPass(pass); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16'); + }); + }); + + it('offset is not multiple of 2 when format is uint16', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16', 1); + }); + }); + + it('offset is not multiple of 4 when format is uint32', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint32', 2); + }); + }); + + it('offset + size > buffer.size', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16', 0, 5); + }); + }); + + it('offset + size > buffer.size (offset + size)', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 8, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16', 4, 5); + }); + }); + + it('buffer from different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.INDEX}); + const pass = await makePass(); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16'); + }); + }); + + it('buffer not INDEX', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const buffer = device.createBuffer({size: 4, usage: GPUBufferUsage.VERTEX}); + const pass = await makePass(device); + await expectValidationError(true, () => { + pass.setIndexBuffer(buffer, 'uint16'); + }); + }); + + }); + + describe('check errors on draw', () => { + + it('works', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(false, () => { + pass.draw(kNumVertices, kNumInstances); + }); + endPass(pass); + }); + + it('fails if pass ended', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + endPass(pass); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + }); + + it('fails if no pipeline', async () => { + const { device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + endPass(pass); + }); + + it('fails if missing vertexBuffer', async () => { + const { pipeline, device, vertexBuffer0 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + endPass(pass); + }); + + it('fails if vertexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + endPass(pass); + }); + + it('fails if vertexCount exceeds data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.draw(kNumVertices + 1); + }); + endPass(pass); + }); + + it('fails if count exceeds data via binding size', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0, 0, 20); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + endPass(pass); + }); + + it('fails if count exceeds data via binding offset', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0, 4); + pass.setVertexBuffer(1, vertexBuffer1); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.draw(kNumVertices); + }); + endPass(pass); + }); + + it('fails if instanceCount exceeds data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.draw(kNumVertices, kNumInstances + 1); + }); + endPass(pass); + }); + + addValidateBindGroupTests({ + makePassAndPipeline: async (device, options) => { + const { pipeline } = await createRenderBindGroupPipeline(device, options); + const pass = await makePass(device); + pass.setPipeline(pipeline); + return {pass, pipeline}; + }, + execute(pass) { + pass.draw(3); + }, + visibility: GPUShaderStage.VERTEX, + }); + + }); + + describe('check errors on drawIndexed', () => { + + it('works', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(false, () => { + pass.drawIndexed(kNumVertices, kNumInstances); + }); + endPass(pass); + }); + + it('fails if pass ended', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + endPass(pass); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + }); + + it('fails if no pipeline', async () => { + const { device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + endPass(pass); + }); + + it('fails if missing vertexBuffer', async () => { + const { pipeline, device, vertexBuffer0, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + endPass(pass); + }); + + it('fails if vertexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + endPass(pass); + }); + + it('fails if missing indexBuffer', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1 } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + endPass(pass); + }); + + it('fails if indexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + indexBuffer.destroy(); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices); + }); + endPass(pass); + }); + + it('fails if indexedCount > data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices + 1, kNumInstances); + }); + endPass(pass); + }); + + it('fails if indexedCount > data via size', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat, 0, indexBuffer.size - 4); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices, kNumInstances); + }); + endPass(pass); + }); + + it('fails if indexedCount > data via offset', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat, 4); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices, kNumInstances); + }); + endPass(pass); + }); + + it('fails if instanceCount > data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexed(kNumIndices, kNumInstances + 1); + }); + endPass(pass); + }); + + addValidateBindGroupTests({ + makePassAndPipeline: async (device, options) => { + const { pipeline, indexBuffer } = await createRenderBindGroupPipeline(device, options); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + return {pass, pipeline}; + }, + execute(pass) { + pass.drawIndexed(3); + }, + visibility: GPUShaderStage.VERTEX, + }); + + }); + + describe('check errors on drawIndirect', () => { + + it('works', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(false, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if pass ended', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + endPass(pass); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + }); + + it('fails if no pipeline', async () => { + const { device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if missing vertexBuffer', async () => { + const { pipeline, device, vertexBuffer0, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if vertexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if indirectBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + indirectBuffer.destroy(); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if indirect offset outside data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.drawIndirect(indirectBuffer, indirectBuffer.size - 12); + }); + endPass(pass); + }); + + addValidateBindGroupTests((() => { + let ib; // kind of hacky but at least we don't have to pass indirect buffer through? + return { + makePassAndPipeline: async (device, options) => { + const { pipeline, indirectBuffer } = await createRenderBindGroupPipeline(device, options); + ib = indirectBuffer; + const pass = await makePass(device); + pass.setPipeline(pipeline); + return {pass, pipeline}; + }, + execute(pass) { + pass.drawIndirect(ib, 0); + }, + visibility: GPUShaderStage.VERTEX, + }; + })()); + + }); + + describe('check errors on drawIndexedIndirect', () => { + + it('works', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(false, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if pass ended', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + endPass(pass); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + }); + + it('fails if no pipeline', async () => { + const { device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if missing vertexBuffer', async () => { + const { pipeline, device, vertexBuffer0, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if vertexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + vertexBuffer0.destroy(); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if missing indexBuffer', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if indexBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + indexBuffer.destroy(); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if indirectBuffer destroyed', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + indirectBuffer.destroy(); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, 0); + }); + endPass(pass); + }); + + it('fails if indirect offset outside data', async () => { + const { pipeline, device, vertexBuffer0, vertexBuffer1, indexBuffer, indirectBuffer } = await createRenderPipelineAndAttribResources(); + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer0); + pass.setVertexBuffer(1, vertexBuffer1); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + await expectValidationError(true, () => { + pass.drawIndexedIndirect(indirectBuffer, indirectBuffer.size - 16); + }); + endPass(pass); + }); + + addValidateBindGroupTests((() => { + let ib; // kind of hacky but at least we don't have to pass indirect buffer through? + return { + makePassAndPipeline: async (device, options) => { + const { pipeline, indexBuffer, indirectBuffer } = await createRenderBindGroupPipeline(device, options); + ib = indirectBuffer; + const pass = await makePass(device); + pass.setPipeline(pipeline); + pass.setIndexBuffer(indexBuffer, kIndexFormat); + return {pass, pipeline}; + }, + execute(pass) { + pass.drawIndexedIndirect(ib, 0); + }, + visibility: GPUShaderStage.VERTEX, + }; + })()); + + }); +} \ No newline at end of file diff --git a/test/tests/render-pass-tests.js b/test/tests/render-pass-tests.js new file mode 100644 index 0000000..39692e4 --- /dev/null +++ b/test/tests/render-pass-tests.js @@ -0,0 +1,691 @@ +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; +import {addRenderMixinTests} from './render-mixin-tests.js'; +import {addTimestampWriteTests, getDeviceWithTimestamp} from './timestamp-tests.js'; + + +async function createCommandEncoder(device) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + return device.createCommandEncoder(); +} + +async function createRenderPass(device, encoder, { + timestampWrites, + occlusionQuerySet, +} = {}) { + device = device || await (await navigator.gpu.requestAdapter()).requestDevice(); + encoder = encoder || await createCommandEncoder(device); + const texture = device.createTexture({ + size: [2, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + ...(timestampWrites && { timestampWrites }), + ...(occlusionQuerySet && { occlusionQuerySet }), + }); + return pass; +} + +describe('test render pass encoder', () => { + + describe('check errors on beginRenderPass', () => { + + it('errors if 2 passes are started', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const encoder = await createCommandEncoder(device); + await createRenderPass(device, encoder); + await expectValidationError(true, async () => { + await createRenderPass(device, encoder); + }); + }); + + it('can not end twice', async () => { + const pass = await createRenderPass(); + pass.end(); + await expectValidationError(true, async () => { + pass.end(); + }); + }); + + it('errors when colorAttachments are not the same size', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [2, 3].map(width => device.createTexture({ + size: [width, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + }); + + it('errors when colorAttachments are not the same sampleCount', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [1, 4].map(sampleCount => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + sampleCount, + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + }); + + it('works with resolveTarget', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [4, 1].map(sampleCount => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + sampleCount, + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(false, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: textures[0].createView(), + resolveTarget: textures[1].createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + + it('errors when resolveTarget is not sampleCount 1', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [4, 4].map(sampleCount => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + sampleCount, + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: textures[0].createView(), + resolveTarget: textures[1].createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + + it('errors when resolveTarget is not same size', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [4, 1].map((sampleCount, i) => device.createTexture({ + size: [3, 3 + i], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + sampleCount, + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: textures[0].createView(), + resolveTarget: textures[1].createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + + it('errors when resolveTarget is not same format', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [4, 1].map((sampleCount, i) => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: ['rgba8unorm', 'r8unorm'][i], + sampleCount, + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: textures[0].createView(), + resolveTarget: textures[1].createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + it('errors when no attachments', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [], + }); + }); + }); + + it('no error when max bytes per sample', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [1, 1, 1, 1].map(() => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba16float', + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(false, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + }); + + it('error when > max bytes per sample', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = new Array(Math.ceil(device.limits.maxColorAttachmentBytesPerSample / 16) + 1).fill(1).map(() => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba32float', + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + }); + + it('error when > max attachments', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = new Array(device.limits.maxColorAttachments + 1).fill(1).map(() => device.createTexture({ + size: [3, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'r8unorm', + })); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + }); + + it('errors when depthStencilAttachment is a different size than the colorAttachments', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const colorTexture = device.createTexture({ + size: [2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const depthTexture = device.createTexture({ + size: [2, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'depth24plus', + }); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }); + }); + }); + + it('fails when the sample layer/level is used more than once', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const colorTexture = device.createTexture({ + size: [2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + { + view: colorTexture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + + it('passes when the sample different layers on the same texture', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const colorTexture = device.createTexture({ + size: [2, 2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const encoder = device.createCommandEncoder(); + await expectValidationError(false, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView({dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1}), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + { + view: colorTexture.createView({dimension: '2d', baseArrayLayer: 1, arrayLayerCount: 1}), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + }); + }); + + it('errors when colorAttachments are destroyed', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const textures = [3, 3].map(width => device.createTexture({ + size: [width, 3], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + })); + textures[1].destroy(); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + })), + }); + }); + + }); + + it('errors when depthStencilAttachment is destroyed', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const colorTexture = device.createTexture({ + size: [2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const depthTexture = device.createTexture({ + size: [2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'depth24plus', + }); + depthTexture.destroy(); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }); + }); + }); + + it('fails if occlusionQuerySet.type is not occlusion', async function () { + const device = await getDeviceWithTimestamp(this); + const colorTexture = device.createTexture({ + size: [2, 2], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + const occlusionQuerySet = device.createQuerySet({type: 'timestamp', count: 2}); + const encoder = device.createCommandEncoder(); + await expectValidationError(true, () => { + encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + clearColor: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + occlusionQuerySet, + }); + }); + }); + + addTimestampWriteTests({ + makePass(device, {timestampWrites}) { + return createRenderPass(device, undefined, { timestampWrites }); + }, + }); + + }); + + addRenderMixinTests({ + makePass: async (device) => { + return await createRenderPass(device); + }, + endPass(pass) { + pass.end(); + }, + }); + + describe('check errors on executeBundles', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await createRenderPass(device); + const bundleEncoder = device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + }); + const bundle = bundleEncoder.finish(); + + await expectValidationError(false, () => { + pass.executeBundles([bundle]); + }); + + }); + + it('fails if bundle is from different device', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await createRenderPass(); + const bundleEncoder = device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + }); + const bundle = bundleEncoder.finish(); + + await expectValidationError(true, () => { + pass.executeBundles([bundle]); + }); + + }); + + it('fails if bundle incompatible', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await createRenderPass(device); + const bundleEncoder = device.createRenderBundleEncoder({ + colorFormats: ['r8unorm'], + }); + const bundle = bundleEncoder.finish(); + + await expectValidationError(true, () => { + pass.executeBundles([bundle]); + }); + + }); + + }); + + describe('beginOcclusionQuery', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + + await expectValidationError(false, () => { + pass.beginOcclusionQuery(0); + }); + }); + + it('fails if no occlusionQuerySet on pass', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const pass = await createRenderPass(device); + await expectValidationError(true, () => { + pass.beginOcclusionQuery(0); + }); + }); + + it('fails if querySet destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + occlusionQuerySet.destroy(); + + await expectValidationError(true, () => { + pass.beginOcclusionQuery(0); + }); + }); + + it('fails if queryIndex out of range', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + + await expectValidationError(true, () => { + pass.beginOcclusionQuery(2); + }); + }); + + it('fails if query in progress', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + + pass.beginOcclusionQuery(1); + await expectValidationError(true, () => { + pass.beginOcclusionQuery(0); + }); + }); + + it('fails if queryIndex already used', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + pass.beginOcclusionQuery(0); + pass.endOcclusionQuery(); + + await expectValidationError(true, () => { + pass.beginOcclusionQuery(0); + }); + }); + + }); + + describe('endOcclusionQuery', () => { + + it('works', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + pass.beginOcclusionQuery(0); + + await expectValidationError(false, () => { + pass.endOcclusionQuery(); + }); + }); + + it('fails if querySet destroyed', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + pass.beginOcclusionQuery(0); + occlusionQuerySet.destroy(); + + await expectValidationError(true, () => { + pass.endOcclusionQuery(); + }); + }); + + + it('fails if no query in progress', async () => { + const device = await (await navigator.gpu.requestAdapter()).requestDevice(); + const occlusionQuerySet = device.createQuerySet({ type: 'occlusion', count: 2 }); + const pass = await createRenderPass(device, undefined, { + occlusionQuerySet, + }); + + await expectValidationError(true, () => { + pass.endOcclusionQuery(); + }); + }); + + }); + + describe('check errors on setViewport', () => { + + const tests = [ + { success: true, args: [0, 0, 2, 3, 0, 1], desc: 'valid' }, + { success: false, args: [0, 0, 2, 3, 0, 1], desc: 'pass ended', end: true }, + { success: false, args: [-1, 0, 1, 1, 0, 1], desc: 'x < 0' }, + { success: false, args: [ 0, -1, 1, 1, 0, 1], desc: 'y < 0' }, + { success: false, args: [ 0, 0, 3, 1, 0, 1], desc: 'x + width > targetWidth' }, + { success: false, args: [ 1, 0, 2, 1, 0, 1], desc: 'x + width > targetWidth' }, + { success: false, args: [ 0, 0, 1, 4, 0, 1], desc: 'y + height > targetHeight' }, + { success: false, args: [ 0, 1, 1, 3, 0, 1], desc: 'y + height > targetHeight' }, + { success: false, args: [ 0, 0, 2, 3, -1, 1], desc: 'minDepth < 0' }, + { success: false, args: [ 0, 0, 2, 3, 2, 1], desc: 'minDepth > 1' }, + { success: false, args: [ 0, 0, 2, 3, 0, -1], desc: 'maxDepth < 0' }, + { success: false, args: [ 0, 0, 2, 3, 0, 2], desc: 'maxDepth > 1' }, + { success: false, args: [ 0, 0, 2, 3, 0.5, 0.4], desc: 'minDepth > maxDepth' }, + ]; + + for (const {success, args, desc, end} of tests) { + it(desc, async () => { + const pass = await createRenderPass(); + if (end) { + pass.end(); + } + await expectValidationError(!success, () => { + pass.setViewport(...args); + }); + }); + } + + }); + + describe('check errors on setScissorRect', () => { + + const tests = [ + { success: true, args: [0, 0, 2, 3, 0, 1], desc: 'valid' }, + { success: false, args: [0, 0, 2, 3, 0, 1], desc: 'valid', end: true }, + { success: false, args: [-1, 0, 1, 1, 0, 1], desc: 'x < 0' }, + { success: false, args: [ 0, -1, 1, 1, 0, 1], desc: 'y < 0' }, + { success: false, args: [ 0, 0, 3, 1, 0, 1], desc: 'x + width > targetWidth' }, + { success: false, args: [ 1, 0, 2, 1, 0, 1], desc: 'x + width > targetWidth' }, + { success: false, args: [ 0, 0, 1, 4, 0, 1], desc: 'y + height > targetHeight' }, + { success: false, args: [ 0, 1, 1, 3, 0, 1], desc: 'y + height > targetHeight' }, + ]; + + for (const {success, args, desc, end} of tests) { + it(desc, async () => { + const pass = await createRenderPass(); + if (end) { + pass.end(); + } + await expectValidationError(!success, () => { + pass.setViewport(...args); + }); + }); + } + + }); + +}); diff --git a/test/tests/timestamp-tests.js b/test/tests/timestamp-tests.js new file mode 100644 index 0000000..308dff1 --- /dev/null +++ b/test/tests/timestamp-tests.js @@ -0,0 +1,114 @@ + +import {describe, it} from '../mocha-support.js'; +import {expectValidationError} from '../js/utils.js'; + +export async function getDeviceWithTimestamp(test) { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter.features.has('timestamp-query')) { + test.skip('timestamp-writes feature not available'); + return null; + } + return await adapter.requestDevice({ + requiredFeatures: ['timestamp-query'], + }); +} + +export function addTimestampWriteTests({ + makePass, +}) { + describe('timestampWrites', () => { + + it('works', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + await expectValidationError(false, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }); + }); + }); + + it('fails if query destroyed', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + querySet.destroy(); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }); + }); + }); + + it('fails if query from different device', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + const device2 = await getDeviceWithTimestamp(this); + + await expectValidationError(true, async () => { + await makePass(device2, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }); + }); + }); + + it('fails if query wrong type', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'occlusion'}); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }); + }); + }); + + it('fails if both begin and end not set', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet }, + }); + }); + }); + + it('fails if begin === end', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 0 }, + }); + }); + }); + + it('fails if begin out of range', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 2, endOfPassWriteIndex: 1 }, + }); + }); + }); + + it('fails if end out of range', async function () { + const device = await getDeviceWithTimestamp(this); + const querySet = device.createQuerySet({count: 2, type: 'timestamp'}); + + await expectValidationError(true, async () => { + await makePass(device, { + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 2 }, + }); + }); + }); + + + }); +} \ No newline at end of file diff --git a/test/tests/webgpu-debug-helper-tests.js b/test/tests/webgpu-debug-helper-tests.js index 963e152..16fc208 100644 --- a/test/tests/webgpu-debug-helper-tests.js +++ b/test/tests/webgpu-debug-helper-tests.js @@ -1,82 +1,7 @@ import '../../dist/0.x/webgpu-debug-helper.js'; -import { - assertEqual, - assertFalsy, - assertIsArray, - assertInstanceOf, - assertStrictEqual, - assertStrictNotEqual, - assertTruthy, -} from '../assert.js'; -import {describe, it, beforeEach, afterEach} from '../mocha-support.js'; - -async function createRenderPass() { - const adapter = await navigator.gpu.requestAdapter(); - const device = await adapter.requestDevice(); - const texture = device.createTexture({ - size: [2, 3], - usage: GPUTextureUsage.RENDER_ATTACHMENT, - format: 'rgba8unorm', - }); - const encoder = device.createCommandEncoder(); - const pass = encoder.beginRenderPass({ - colorAttachments: [{ - view: texture.createView(), - clearColor: [0, 0, 0, 0], - loadOp: 'clear', - storeOp: 'store', - }], - }); - return pass; -} - -function expectValidationError(expectError, fn) { - let error = false; - try { - fn(); - } catch (e) { - error = e; - } - if (expectError) { - if (!error) { - throw new Error('expected error, no error thrown'); - } - } else { - if (error) { - throw error; - } - } -} +import {describe} from '../mocha-support.js'; describe('test webgpu-debug-helper', () => { - describe('test render pass encoder', () => { - - describe('should generate error on setViewport', () => { - - const tests = [ - { success: true, args: [0, 0, 2, 3, 0, 1], desc: 'valid' }, - { success: false, args: [-1, 0, 1, 1, 0, 1], desc: 'x < 0' }, - { success: false, args: [ 0, -1, 1, 1, 0, 1], desc: 'y < 0' }, - { success: false, args: [ 0, 0, 3, 1, 0, 1], desc: 'x + width > targetWidth' }, - { success: false, args: [ 1, 0, 2, 1, 0, 1], desc: 'x + width > targetWidth' }, - { success: false, args: [ 0, 0, 1, 4, 0, 1], desc: 'y + height > targetHeight' }, - { success: false, args: [ 0, 1, 1, 3, 0, 1], desc: 'y + height > targetHeight' }, - ]; - - for (let {success, args, desc} of tests) { - it(desc, async () => { - const pass = await createRenderPass(); - expectValidationError(!success, () => { - pass.setViewport(...args); - }); - }); - - } - - }); - - }); - }); diff --git a/tsconfig.json b/tsconfig.json index ce4ed3e..19a4fa3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "module": "NodeNext", "outDir": "dist/0.x", "moduleResolution": "NodeNext", - "declaration": true, + "allowJs": true, + "declaration": false, "typeRoots": [ "./node_modules/@webgpu/types", "./node_modules/@types", @@ -24,4 +25,7 @@ "examples/3rdparty/**/*.js", "test/mocha.js", ], + "typeAcquisition": { + "include": ["@webgpu/types"] + } } \ No newline at end of file