vitest-environment-webgpu-node 2.0.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -8
- package/dist/index.js.map +1 -1
- package/package.json +19 -12
- package/src/index.ts +66 -12
- package/CHANGELOG.md +0 -5
package/README.md
CHANGED
|
@@ -3,13 +3,37 @@
|
|
|
3
3
|
[![npm version][npm-badge]][npm-url]
|
|
4
4
|
[![npm downloads][downloads-badge]][npm-url]
|
|
5
5
|
[![Tests][tests-badge]][tests-url]
|
|
6
|
+
[![Coverage][coverage-badge]][coverage-url]
|
|
6
7
|
[![Discord][discord-badge]][discord-url]
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[
|
|
11
|
-
|
|
12
|
-
[
|
|
9
|
+
Run real WebGPU tests in Vitest on Node, without a browser, Playwright or mocks. Headless GPU testing
|
|
10
|
+
for CI: `navigator.gpu` is backed by Google's Dawn through the
|
|
11
|
+
[`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package. Native GPU testing is up to 2.4x faster
|
|
12
|
+
than running the same tests in a browser, see the
|
|
13
|
+
[blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add -D vitest vitest-environment-webgpu-node
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// vitest.config.ts
|
|
21
|
+
import { defineConfig } from 'vitest/config';
|
|
22
|
+
|
|
23
|
+
export default defineConfig({ test: { environment: 'webgpu-node' } });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// gpu.test.ts
|
|
28
|
+
import { expect, it } from 'vitest';
|
|
29
|
+
|
|
30
|
+
it('has a real GPU device', async () => {
|
|
31
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
32
|
+
const device = await adapter!.requestDevice();
|
|
33
|
+
const buffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST });
|
|
34
|
+
expect(buffer.size).toBe(16);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
13
37
|
|
|
14
38
|
The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
|
|
15
39
|
`GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
|
|
@@ -18,13 +42,30 @@ Using Jest instead? See the equivalent
|
|
|
18
42
|
[`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
|
|
19
43
|
in [jest-gpu](https://github.com/bhouston/jest-gpu).
|
|
20
44
|
|
|
21
|
-
##
|
|
45
|
+
## Compared to alternatives
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
WebGPU has no mock equivalent of a canvas stub that runs shaders, so the realistic options are a browser or
|
|
48
|
+
Dawn in Node.
|
|
49
|
+
|
|
50
|
+
| Approach | Real rendering | Notes |
|
|
51
|
+
| --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| Hand-written `navigator.gpu` mock | No | You can assert calls, not shader output or readbacks. |
|
|
53
|
+
| Vitest browser mode or Playwright | Yes | A real browser with WebGPU enabled: heavier to install and slower to start; right when you need browser APIs. |
|
|
54
|
+
| vitest-environment-webgpu-node | Yes | Dawn in the Node process (Metal, Vulkan, D3D12 and others via `dawnOptions`). Not a browser. |
|
|
55
|
+
|
|
56
|
+
## Supported versions and platforms
|
|
57
|
+
|
|
58
|
+
- Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
|
|
59
|
+
- Node 22 or later. CI runs Node 22 and 26 on `macos-latest` and `ubuntu-latest`.
|
|
60
|
+
- Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
|
|
61
|
+
- Linux without a GPU needs Mesa's software Vulkan driver, see [Linux and CI](#linux-and-ci). This repo's CI does
|
|
62
|
+
not run on Windows.
|
|
26
63
|
|
|
27
|
-
|
|
64
|
+
## Useful for
|
|
65
|
+
|
|
66
|
+
three.js `WebGPURenderer`, Babylon Lite, Vercel's vgpu and raw WebGPU compute or render code. The demos exercise
|
|
67
|
+
those three; other WebGPU libraries such as TypeGPU or luma.gl use the same API but are not covered by this
|
|
68
|
+
repo's tests. These are compatible use cases, not endorsements.
|
|
28
69
|
|
|
29
70
|
## Usage
|
|
30
71
|
|
|
@@ -77,7 +118,7 @@ sudo apt-get update && sudo apt-get install -y libegl1 libgles2 libgl1-mesa-dri
|
|
|
77
118
|
export LIBGL_ALWAYS_SOFTWARE=1
|
|
78
119
|
```
|
|
79
120
|
|
|
80
|
-
This repo's own [CI workflow](
|
|
121
|
+
This repo's own [CI workflow](https://github.com/bhouston/vitest-gpu/blob/main/.github/workflows/ci.yml) does exactly this, so WebGPU tests run
|
|
81
122
|
for real (not mocked) on `ubuntu-latest` on every PR. macOS runners use Dawn's Metal backend and need no
|
|
82
123
|
extra setup.
|
|
83
124
|
|
|
@@ -100,12 +141,24 @@ await expect(canvas).toMatchScreenshot('scene.png');
|
|
|
100
141
|
```
|
|
101
142
|
|
|
102
143
|
`readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
|
|
103
|
-
[`vitest-screenshot`](
|
|
144
|
+
[`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
|
|
104
145
|
`bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
|
|
105
146
|
`readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
|
|
106
147
|
defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
|
|
107
148
|
explicitly with `canvas.asElement<HTMLCanvasElement>()`.
|
|
108
149
|
|
|
150
|
+
## Image snapshots
|
|
151
|
+
|
|
152
|
+
`await expect(canvas).toMatchScreenshot('scene.png')` comes from [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot).
|
|
153
|
+
Call `extendMatchers()` from it once, then pass the canvas as in the example above.
|
|
154
|
+
|
|
155
|
+
## See also
|
|
156
|
+
|
|
157
|
+
- [`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node) for headless WebGL.
|
|
158
|
+
- [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) for `toMatchScreenshot()`.
|
|
159
|
+
- [`demo/`](https://github.com/bhouston/vitest-gpu/tree/main/demo/test/webgpu) for device, upload, readback, triangle, three.js, Babylon Lite and vgpu tests,
|
|
160
|
+
and the [repository README](https://github.com/bhouston/vitest-gpu#readme).
|
|
161
|
+
|
|
109
162
|
## Author
|
|
110
163
|
|
|
111
164
|
Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
|
|
@@ -121,3 +174,9 @@ MIT
|
|
|
121
174
|
[tests-url]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml
|
|
122
175
|
[discord-badge]: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
|
|
123
176
|
[discord-url]: https://discord.gg/fwupDN493R
|
|
177
|
+
[coverage-badge]: https://codecov.io/gh/bhouston/vitest-gpu/branch/main/graph/badge.svg
|
|
178
|
+
[coverage-url]: https://codecov.io/gh/bhouston/vitest-gpu
|
|
179
|
+
|
|
180
|
+
## Resource cleanup
|
|
181
|
+
|
|
182
|
+
Environment teardown destroys devices requested through its `navigator.gpu` and cancels pending callbacks scheduled through its animation-frame shim. Device requests that finish after teardown are destroyed and rejected. Devices created through another GPU instance remain the caller's responsibility. Keep disposing renderers, buffers, textures and devices during long-running files to release resources sooner.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;;;;kBA2Dc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;2BA4DvE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import { create, globals } from 'webgpu';
|
|
2
2
|
import { HeadlessCanvas } from './canvas.js';
|
|
3
3
|
export { createCanvas, HeadlessCanvas, HeadlessCanvasContext } from './canvas.js';
|
|
4
|
-
/**
|
|
5
|
-
const requestAnimationFrame = (callback) => setTimeout(() => callback(performance.now()), 16).unref();
|
|
4
|
+
/** Each environment owns its frame callbacks; unref alone does not release them. */
|
|
6
5
|
const domShims = () => {
|
|
6
|
+
const frames = new Set();
|
|
7
|
+
let disposed = false;
|
|
8
|
+
const requestAnimationFrame = (callback) => {
|
|
9
|
+
if (disposed)
|
|
10
|
+
return 0;
|
|
11
|
+
const frame = setTimeout(() => {
|
|
12
|
+
frames.delete(frame);
|
|
13
|
+
callback(performance.now());
|
|
14
|
+
}, 16).unref();
|
|
15
|
+
frames.add(frame);
|
|
16
|
+
return frame;
|
|
17
|
+
};
|
|
18
|
+
const cancelAnimationFrame = (frame) => {
|
|
19
|
+
frames.delete(frame);
|
|
20
|
+
clearTimeout(frame);
|
|
21
|
+
};
|
|
22
|
+
const dispose = () => {
|
|
23
|
+
disposed = true;
|
|
24
|
+
for (const frame of frames)
|
|
25
|
+
clearTimeout(frame);
|
|
26
|
+
frames.clear();
|
|
27
|
+
};
|
|
7
28
|
const window = {
|
|
8
29
|
devicePixelRatio: 1,
|
|
9
30
|
requestAnimationFrame,
|
|
10
|
-
cancelAnimationFrame
|
|
31
|
+
cancelAnimationFrame,
|
|
11
32
|
addEventListener() { },
|
|
12
33
|
removeEventListener() { },
|
|
13
34
|
};
|
|
14
|
-
|
|
35
|
+
const shims = {
|
|
15
36
|
HTMLCanvasElement: HeadlessCanvas,
|
|
16
37
|
document: {
|
|
17
38
|
createElement: (tag) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
|
|
@@ -22,8 +43,9 @@ const domShims = () => {
|
|
|
22
43
|
window,
|
|
23
44
|
self: window,
|
|
24
45
|
requestAnimationFrame,
|
|
25
|
-
cancelAnimationFrame
|
|
46
|
+
cancelAnimationFrame,
|
|
26
47
|
};
|
|
48
|
+
return { shims, dispose };
|
|
27
49
|
};
|
|
28
50
|
const restore = (properties) => {
|
|
29
51
|
for (const { target, key, descriptor } of properties.toReversed()) {
|
|
@@ -37,7 +59,17 @@ export default {
|
|
|
37
59
|
name: 'webgpu-node',
|
|
38
60
|
viteEnvironment: 'ssr',
|
|
39
61
|
setup(global, { webgpuNode = {} }) {
|
|
40
|
-
const
|
|
62
|
+
const dom = domShims();
|
|
63
|
+
const shims = { ...globals, ...dom.shims };
|
|
64
|
+
const devices = new Set();
|
|
65
|
+
let disposed = false;
|
|
66
|
+
const dispose = () => {
|
|
67
|
+
disposed = true;
|
|
68
|
+
dom.dispose();
|
|
69
|
+
for (const device of devices)
|
|
70
|
+
device.destroy();
|
|
71
|
+
devices.clear();
|
|
72
|
+
};
|
|
41
73
|
const modified = [];
|
|
42
74
|
try {
|
|
43
75
|
for (const key of Object.keys(shims)) {
|
|
@@ -56,20 +88,51 @@ export default {
|
|
|
56
88
|
}
|
|
57
89
|
const navigator = global.navigator;
|
|
58
90
|
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
91
|
+
const gpu = create(webgpuNode.dawnOptions ?? []);
|
|
92
|
+
const requestAdapter = gpu.requestAdapter.bind(gpu);
|
|
93
|
+
gpu.requestAdapter = async (options) => {
|
|
94
|
+
if (disposed)
|
|
95
|
+
throw new Error('WebGPU environment has been torn down');
|
|
96
|
+
const adapter = await requestAdapter(options);
|
|
97
|
+
if (adapter) {
|
|
98
|
+
const requestDevice = adapter.requestDevice.bind(adapter);
|
|
99
|
+
adapter.requestDevice = async (descriptor) => {
|
|
100
|
+
if (disposed)
|
|
101
|
+
throw new Error('WebGPU environment has been torn down');
|
|
102
|
+
const device = await requestDevice(descriptor);
|
|
103
|
+
if (disposed) {
|
|
104
|
+
device.destroy();
|
|
105
|
+
throw new Error('WebGPU environment has been torn down');
|
|
106
|
+
}
|
|
107
|
+
devices.add(device);
|
|
108
|
+
return device;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return adapter;
|
|
112
|
+
};
|
|
59
113
|
Object.defineProperty(navigator, 'gpu', {
|
|
60
|
-
value:
|
|
114
|
+
value: gpu,
|
|
61
115
|
writable: false,
|
|
62
116
|
enumerable: false,
|
|
63
117
|
configurable: true,
|
|
64
118
|
});
|
|
65
119
|
}
|
|
66
120
|
catch (error) {
|
|
121
|
+
dispose();
|
|
67
122
|
restore(modified);
|
|
68
123
|
throw error;
|
|
69
124
|
}
|
|
70
125
|
return {
|
|
71
126
|
teardown(_global) {
|
|
72
|
-
|
|
127
|
+
if (disposed)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
dispose();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
restore(modified);
|
|
134
|
+
modified.length = 0;
|
|
135
|
+
}
|
|
73
136
|
},
|
|
74
137
|
};
|
|
75
138
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAkB,MAAM,aAAa,CAAC;AAQlG,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-environment-webgpu-node",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Vitest environment
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Vitest environment for real headless WebGPU tests in Node (Dawn via the webgpu package): no browser, Playwright or mocks",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"ci",
|
|
7
|
+
"compute-shader",
|
|
6
8
|
"dawn",
|
|
9
|
+
"gpu",
|
|
10
|
+
"gpu-testing",
|
|
7
11
|
"headless",
|
|
12
|
+
"node",
|
|
8
13
|
"testing",
|
|
14
|
+
"three.js",
|
|
9
15
|
"vitest",
|
|
10
16
|
"vitest-environment",
|
|
11
|
-
"webgpu"
|
|
17
|
+
"webgpu",
|
|
18
|
+
"webgpu-testing",
|
|
19
|
+
"wgsl"
|
|
12
20
|
],
|
|
13
21
|
"homepage": "https://github.com/bhouston/vitest-gpu#readme",
|
|
14
22
|
"bugs": {
|
|
@@ -27,8 +35,7 @@
|
|
|
27
35
|
"dist/**/*.d.ts",
|
|
28
36
|
"dist/**/*.d.ts.map",
|
|
29
37
|
"src/**/*.ts",
|
|
30
|
-
"!src/**/*.test.ts"
|
|
31
|
-
"CHANGELOG.md"
|
|
38
|
+
"!src/**/*.test.ts"
|
|
32
39
|
],
|
|
33
40
|
"type": "module",
|
|
34
41
|
"main": "./dist/index.js",
|
|
@@ -42,16 +49,12 @@
|
|
|
42
49
|
"publishConfig": {
|
|
43
50
|
"access": "public"
|
|
44
51
|
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "tsc",
|
|
47
|
-
"tsc": "tsc"
|
|
48
|
-
},
|
|
49
52
|
"dependencies": {
|
|
50
53
|
"webgpu": "^0.6.1"
|
|
51
54
|
},
|
|
52
55
|
"peerDependencies": {
|
|
53
56
|
"typescript": ">=6",
|
|
54
|
-
"vitest": ">=
|
|
57
|
+
"vitest": ">=4 <6"
|
|
55
58
|
},
|
|
56
59
|
"peerDependenciesMeta": {
|
|
57
60
|
"typescript": {
|
|
@@ -59,6 +62,10 @@
|
|
|
59
62
|
}
|
|
60
63
|
},
|
|
61
64
|
"engines": {
|
|
62
|
-
"node": ">=
|
|
65
|
+
"node": ">=22"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsc",
|
|
69
|
+
"tsc": "tsc"
|
|
63
70
|
}
|
|
64
|
-
}
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Environment } from 'vitest/
|
|
1
|
+
import type { Environment } from 'vitest/runtime';
|
|
2
2
|
import { create, globals } from 'webgpu';
|
|
3
3
|
import { HeadlessCanvas } from './canvas.js';
|
|
4
4
|
|
|
@@ -10,19 +10,36 @@ export type WebgpuNodeOptions = {
|
|
|
10
10
|
dawnOptions?: string[];
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
13
|
+
/** Each environment owns its frame callbacks; unref alone does not release them. */
|
|
14
|
+
const domShims = () => {
|
|
15
|
+
const frames = new Set<NodeJS.Timeout>();
|
|
16
|
+
let disposed = false;
|
|
17
|
+
const requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout | number => {
|
|
18
|
+
if (disposed) return 0;
|
|
19
|
+
const frame = setTimeout(() => {
|
|
20
|
+
frames.delete(frame);
|
|
21
|
+
callback(performance.now());
|
|
22
|
+
}, 16).unref();
|
|
23
|
+
frames.add(frame);
|
|
24
|
+
return frame;
|
|
25
|
+
};
|
|
26
|
+
const cancelAnimationFrame = (frame: NodeJS.Timeout) => {
|
|
27
|
+
frames.delete(frame);
|
|
28
|
+
clearTimeout(frame);
|
|
29
|
+
};
|
|
30
|
+
const dispose = () => {
|
|
31
|
+
disposed = true;
|
|
32
|
+
for (const frame of frames) clearTimeout(frame);
|
|
33
|
+
frames.clear();
|
|
34
|
+
};
|
|
18
35
|
const window = {
|
|
19
36
|
devicePixelRatio: 1,
|
|
20
37
|
requestAnimationFrame,
|
|
21
|
-
cancelAnimationFrame
|
|
38
|
+
cancelAnimationFrame,
|
|
22
39
|
addEventListener() {},
|
|
23
40
|
removeEventListener() {},
|
|
24
41
|
};
|
|
25
|
-
|
|
42
|
+
const shims = {
|
|
26
43
|
HTMLCanvasElement: HeadlessCanvas,
|
|
27
44
|
document: {
|
|
28
45
|
createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
|
|
@@ -33,8 +50,9 @@ const domShims = (): Record<string, unknown> => {
|
|
|
33
50
|
window,
|
|
34
51
|
self: window,
|
|
35
52
|
requestAnimationFrame,
|
|
36
|
-
cancelAnimationFrame
|
|
53
|
+
cancelAnimationFrame,
|
|
37
54
|
};
|
|
55
|
+
return { shims, dispose };
|
|
38
56
|
};
|
|
39
57
|
|
|
40
58
|
type SavedProperty = { target: object; key: PropertyKey; descriptor?: PropertyDescriptor };
|
|
@@ -50,7 +68,16 @@ export default {
|
|
|
50
68
|
name: 'webgpu-node',
|
|
51
69
|
viteEnvironment: 'ssr',
|
|
52
70
|
setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
|
|
53
|
-
const
|
|
71
|
+
const dom = domShims();
|
|
72
|
+
const shims = { ...globals, ...dom.shims } as Record<string, unknown>;
|
|
73
|
+
const devices = new Set<GPUDevice>();
|
|
74
|
+
let disposed = false;
|
|
75
|
+
const dispose = () => {
|
|
76
|
+
disposed = true;
|
|
77
|
+
dom.dispose();
|
|
78
|
+
for (const device of devices) device.destroy();
|
|
79
|
+
devices.clear();
|
|
80
|
+
};
|
|
54
81
|
const modified: SavedProperty[] = [];
|
|
55
82
|
try {
|
|
56
83
|
for (const key of Object.keys(shims)) {
|
|
@@ -68,19 +95,46 @@ export default {
|
|
|
68
95
|
}
|
|
69
96
|
const navigator = global.navigator as object;
|
|
70
97
|
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
98
|
+
const gpu = create(webgpuNode.dawnOptions ?? []);
|
|
99
|
+
const requestAdapter = gpu.requestAdapter.bind(gpu);
|
|
100
|
+
gpu.requestAdapter = async (options) => {
|
|
101
|
+
if (disposed) throw new Error('WebGPU environment has been torn down');
|
|
102
|
+
const adapter = await requestAdapter(options);
|
|
103
|
+
if (adapter) {
|
|
104
|
+
const requestDevice = adapter.requestDevice.bind(adapter);
|
|
105
|
+
adapter.requestDevice = async (descriptor) => {
|
|
106
|
+
if (disposed) throw new Error('WebGPU environment has been torn down');
|
|
107
|
+
const device = await requestDevice(descriptor);
|
|
108
|
+
if (disposed) {
|
|
109
|
+
device.destroy();
|
|
110
|
+
throw new Error('WebGPU environment has been torn down');
|
|
111
|
+
}
|
|
112
|
+
devices.add(device);
|
|
113
|
+
return device;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return adapter;
|
|
117
|
+
};
|
|
71
118
|
Object.defineProperty(navigator, 'gpu', {
|
|
72
|
-
value:
|
|
119
|
+
value: gpu,
|
|
73
120
|
writable: false,
|
|
74
121
|
enumerable: false,
|
|
75
122
|
configurable: true,
|
|
76
123
|
});
|
|
77
124
|
} catch (error) {
|
|
125
|
+
dispose();
|
|
78
126
|
restore(modified);
|
|
79
127
|
throw error;
|
|
80
128
|
}
|
|
81
129
|
return {
|
|
82
130
|
teardown(_global?: Record<string, unknown>) {
|
|
83
|
-
|
|
131
|
+
if (disposed) return;
|
|
132
|
+
try {
|
|
133
|
+
dispose();
|
|
134
|
+
} finally {
|
|
135
|
+
restore(modified);
|
|
136
|
+
modified.length = 0;
|
|
137
|
+
}
|
|
84
138
|
},
|
|
85
139
|
};
|
|
86
140
|
},
|
package/CHANGELOG.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
## [vitest-environment-webgpu-node-v2.0.1](https://github.com/bhouston/vitest-gpu/compare/vitest-environment-webgpu-node-v2.0.0...vitest-environment-webgpu-node-v2.0.1) (2026-09-25)
|
|
2
|
-
|
|
3
|
-
### Bug Fixes
|
|
4
|
-
|
|
5
|
-
* drop deprecated transformMode from GPU environments ([28f736a](https://github.com/bhouston/vitest-gpu/commit/28f736ad629a2a3ca023839cadce460d0d5c6937)), closes [#54](https://github.com/bhouston/vitest-gpu/issues/54)
|