| // This file can run with from the dawn folder, after building dawn node, with | |
| // node src/dawn/node/test.mjs | |
| // This assumes you have a symbolic link from out/active to your build folder. | |
| import { dirname, join } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { createRequire } from 'node:module'; | |
| import { describe, it, before, after } from 'node:test'; | |
| import { Worker, } from 'node:worker_threads'; | |
| const require = createRequire(import.meta.url); | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| const dawnNodePath = join(__dirname, '..', '..', '..', 'out', 'active', 'dawn.node'); | |
| const { create, globals } = require(dawnNodePath); | |
| // Save these before they might get replaced by the following assignment | |
| const globalsBeforeGlobalAssignment = { | |
| DOMException, | |
| EventTarget, | |
| Error, | |
| Event, | |
| }; | |
| Object.assign(globalThis, globals); | |
| const assert = { | |
| ok(cond, msg) { | |
| if (typeof cond === 'function') { | |
| assert.ok(cond(), msg ?? cond.toString()); | |
| } else if (!cond) { | |
| throw new Error(msg ?? ''); | |
| } | |
| } | |
| }; | |
| describe('tests', async () => { | |
| let device; | |
| let navigator; | |
| let adapter | |
| before(async () => { | |
| navigator = { gpu: create([]) }; | |
| adapter = await navigator.gpu.requestAdapter(); | |
| if (!adapter) { | |
| return; | |
| } | |
| device = await adapter.requestDevice({label: "test-device"}); | |
| }); | |
| after(async () => { | |
| device.destroy(); | |
| device = undefined; | |
| navigator = undefined; | |
| adapter = undefined; | |
| await new Promise(r => setTimeout(r, 1000)); | |
| }) | |
| await describe('global tests', async () => { | |
| // Check that these globals were not replaced when we added dawn.node's globals | |
| assert.ok(() => globalsBeforeGlobalAssignment.EventTarget === EventTarget); | |
| assert.ok(() => globalsBeforeGlobalAssignment.Event === Event); | |
| assert.ok(() => globalsBeforeGlobalAssignment.Error === Error); | |
| assert.ok(() => globalsBeforeGlobalAssignment.DOMException === DOMException); | |
| }); | |
| await describe('device tests', async () => { | |
| await it('basic device tests', () => { | |
| assert.ok(device.adapterInfo.description !== undefined); | |
| assert.ok(device.label === 'test-device'); | |
| assert.ok(() => device instanceof EventTarget); | |
| }); | |
| await it('can not construct device', () => { | |
| // check we can't make things we're not supposed to be able to. | |
| let err; | |
| try { | |
| const gd = new GPUDevice(); | |
| assert(gd || !gd, 'should never get here'); // use gd | |
| } catch (e) { | |
| err = e; | |
| } | |
| assert.ok(() => err instanceof TypeError); | |
| }); | |
| }); | |
| await describe('popErrorScope ', async () => { | |
| await it('popErrorScope returns error', async () => { | |
| device.pushErrorScope('validation'); | |
| device.createTexture({ | |
| format: 'rgba8unorm', | |
| usage: GPUTextureUsage.TEXTURE_BINDING, | |
| size: [device.limits.maxTextureDimension2D + 1], | |
| }); | |
| const err = await device.popErrorScope(); | |
| assert.ok(() => err instanceof GPUValidationError); | |
| }); | |
| await it('popErrorScope twice returns error', async () => { | |
| device.pushErrorScope('out-of-memory'); | |
| device.pushErrorScope('internal'); | |
| device.pushErrorScope('validation'); | |
| device.pushErrorScope('validation'); | |
| device.createTexture({ | |
| format: 'rgba8unorm', | |
| usage: GPUTextureUsage.TEXTURE_BINDING, | |
| size: [device.limits.maxTextureDimension2D + 1], | |
| }); | |
| const err1 = await device.popErrorScope(); | |
| assert.ok(() => err1 instanceof GPUValidationError); | |
| const err2 = await device.popErrorScope(); | |
| assert.ok(() => !err2); | |
| const err3 = await device.popErrorScope(); | |
| assert.ok(() => !err3); | |
| const err4 = await device.popErrorScope(); | |
| assert.ok(() => !err4); | |
| }); | |
| await it('works when popErrorScope on empty stack', async () => { | |
| try { | |
| await device.popErrorScope(); | |
| assert.ok(false, 'should not get here'); | |
| } catch (e) { | |
| assert.ok(e.name === 'OperationError'); | |
| } | |
| }) | |
| }); | |
| await describe('error tests', { skip: false }, async () => { | |
| await it('basic error tests', () => { | |
| const e = new GPUValidationError('test1'); | |
| assert.ok(() => e instanceof GPUError); | |
| assert.ok(() => e.message === 'test1'); | |
| }); | |
| }); | |
| await describe('GPUPipelineError tests', async () => { | |
| await it('can create a GPUPipelineError', () => { | |
| const e = new GPUPipelineError("msg1", { reason: 'validation' }); | |
| assert.ok(() => e instanceof GPUPipelineError); | |
| assert.ok(() => e instanceof DOMException); | |
| assert.ok(() => e.name === 'GPUPipelineError'); | |
| assert.ok(() => e.message === 'msg1') | |
| assert.ok(() => e.reason === 'validation') | |
| }); | |
| await it('generates GPUPipelineError', { skip: false }, async () => { | |
| const vertexModule = device.createShaderModule({ | |
| code: ` | |
| @group(0) @binding(0) var<uniform> myUniform : vec4f; | |
| @vertex fn vs() -> @builtin(position) vec4f { | |
| return myUniform; | |
| } | |
| `, | |
| }); | |
| const fragmentModule = device.createShaderModule({ | |
| code: ` | |
| @group(0) @binding(0) var tex: texture_2d<f32>; | |
| @fragment fn fs() -> @location(0) vec4f { | |
| return textureLoad(tex, vec2u(0), 0); | |
| } | |
| `, | |
| }); | |
| const promise = device.createRenderPipelineAsync({ | |
| layout: 'auto', | |
| vertex: { module: vertexModule }, | |
| fragment: { module: fragmentModule, targets: [ { format: 'rgba8unorm' } ]}, | |
| }); | |
| try { | |
| const pipeline = await promise; | |
| assert(false, 'should not get here'); | |
| } catch (e) { | |
| //console.log(e); | |
| //console.log('name:', e.name); | |
| //console.log('message:', e.message); | |
| //console.log('reason:', e.reason); | |
| assert.ok(() => e instanceof GPUPipelineError); | |
| assert.ok(() => e instanceof DOMException); | |
| assert.ok(() => e.reason === 'validation'); | |
| } | |
| }); | |
| }); | |
| await describe('event tests', { skip: false }, async () => { | |
| await it('can pass Event through device', async () => { | |
| const e = await new Promise(resolve => { | |
| device.addEventListener('uncapturedFoo', resolve); | |
| device.dispatchEvent(new Event('uncapturedFoo', { error: { message: 'yo (1)!'} } )); | |
| }); | |
| assert.ok(!!e, 'Event worked'); | |
| }); | |
| await it('can pass CustomEvent through device', async () => { | |
| const e = await new Promise(resolve => { | |
| device.addEventListener('uncapturedEvent', resolve, { once: true }); | |
| device.dispatchEvent(new CustomEvent('uncapturedEvent', { detail: {error: { message: 'yo!'} } })); | |
| }); | |
| assert.ok(e?.detail.error.message === 'yo!', 'CustomEvent worked'); | |
| }); | |
| await it('can pass extended Event through device', async () => { | |
| class MyEvent extends Event { | |
| constructor(type, msg) { | |
| super(type); | |
| this.message = msg; | |
| } | |
| } | |
| const target = new EventTarget(); | |
| const e = await new Promise(resolve => { | |
| target.addEventListener('custom', resolve, { once: true }); | |
| target.dispatchEvent(new MyEvent('custom', 'hello')); | |
| }); | |
| assert.ok(() => e.message === 'hello'); | |
| }); | |
| await it('can pass dispatch user create GPUUncapturedErrorEvent', async () => { | |
| const gEvent = new GPUUncapturedErrorEvent( | |
| 'uncapturederror', | |
| { error: new GPUValidationError('test2') } | |
| ); | |
| assert.ok(() => gEvent instanceof Event); | |
| assert.ok(() => gEvent.error.message === 'test2'); | |
| const e = await new Promise(resolve => { | |
| device.addEventListener('uncapturederror', resolve, { once: true } ); | |
| device.dispatchEvent(gEvent); | |
| }); | |
| assert.ok(e?.error.message === 'test2'); | |
| }); | |
| await it('receives uncaptured events', async () => { | |
| const e = await new Promise(resolve => { | |
| device.addEventListener('uncapturederror', (e) => { | |
| e.preventDefault(); | |
| resolve(e); | |
| }, { once: true, passive: false }); | |
| const texture = device.createTexture({ | |
| format: 'rgba8unorm', | |
| usage: GPUTextureUsage.TEXTURE_BINDING, | |
| size: [device.limits.maxTextureDimension2D + 1, 1], | |
| }); | |
| texture.destroy(); | |
| }); | |
| assert.ok(() => e?.error.message.includes('exceeded maximum texture size')); | |
| }); | |
| }); | |
| await describe('prototype toStringTag', async () => { | |
| await it('the property is present in the prototype', async () => { | |
| assert.ok(() => GPU.prototype[Symbol.toStringTag] === 'GPU'); | |
| const descriptor = Object.getOwnPropertyDescriptor(GPU.prototype, Symbol.toStringTag); | |
| assert.ok(() => !descriptor.writable); | |
| assert.ok(() => !descriptor.enumerable); | |
| assert.ok(() => descriptor.configurable); | |
| }); | |
| await it('toString.call works on instances', async () => { | |
| assert.ok(() => Object.prototype.toString.call(navigator.gpu) === '[object GPU]'); | |
| }); | |
| }); | |
| await describe('worker threads tests', async () => { | |
| it('can request adapter in multiple worker threads simultaneously', async () => { | |
| const numWorkers = 4; | |
| const workers = []; | |
| for (let i = 0; i < numWorkers; i++) { | |
| workers.push(new Promise((resolve, reject) => { | |
| const worker = new Worker(join(__dirname, 'worker-test.mjs'), { type: 'module' }); | |
| worker.on('message', (msg) => { | |
| if (msg === 'success') resolve(); | |
| else reject(new Error(`Worker failed: ${msg}`)); | |
| }); | |
| worker.on('error', reject); | |
| worker.on('exit', (code) => { | |
| if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); | |
| }); | |
| })); | |
| } | |
| await Promise.all(workers); | |
| }); | |
| }); | |
| }); |