vitest-environment-webgpu-node 1.0.0 → 1.0.2
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/CHANGELOG.md +2 -10
- package/README.md +33 -2
- package/dist/canvas.d.ts +8 -2
- package/dist/canvas.d.ts.map +1 -1
- package/dist/canvas.js +39 -16
- package/dist/canvas.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -9
- package/dist/index.js.map +1 -1
- package/legacy.d.ts +5 -0
- package/package.json +5 -1
- package/src/canvas.ts +153 -0
- package/src/index.ts +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
## [vitest-environment-webgpu-node-v1.0.
|
|
2
|
-
|
|
3
|
-
### ⚠ BREAKING CHANGES
|
|
4
|
-
|
|
5
|
-
* **screenshot:** the matcher is async and must be awaited; reference paths need an image extension.
|
|
6
|
-
|
|
7
|
-
### Features
|
|
8
|
-
|
|
9
|
-
* **screenshot:** accept canvases, images and any image format in toMatchScreenshot ([edaabc4](https://github.com/bhouston/vitest-gpu/commit/edaabc43ac0ab895601565129a8db38121cfa5d3)), closes [#1](https://github.com/bhouston/vitest-gpu/issues/1)
|
|
1
|
+
## [vitest-environment-webgpu-node-v1.0.2](https://github.com/bhouston/vitest-gpu/compare/vitest-environment-webgpu-node-v1.0.1...vitest-environment-webgpu-node-v1.0.2) (2026-09-21)
|
|
10
2
|
|
|
11
3
|
### Bug Fixes
|
|
12
4
|
|
|
13
|
-
*
|
|
5
|
+
* support public types across TypeScript versions ([0f3df06](https://github.com/bhouston/vitest-gpu/commit/0f3df067f28308e0154045304ecbcaf2ba840fcc)), closes [#24](https://github.com/bhouston/vitest-gpu/issues/24)
|
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Real, headless WebGPU inside Vitest, with no browser. Powered by Google's Dawn t
|
|
|
6
6
|
The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
|
|
7
7
|
`GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
|
|
8
8
|
|
|
9
|
+
Using Jest instead? See the equivalent
|
|
10
|
+
[`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
|
|
11
|
+
in [jest-gpu](https://github.com/bhouston/jest-gpu).
|
|
12
|
+
|
|
9
13
|
## Install
|
|
10
14
|
|
|
11
15
|
```sh
|
|
@@ -24,6 +28,22 @@ export default defineConfig({
|
|
|
24
28
|
});
|
|
25
29
|
```
|
|
26
30
|
|
|
31
|
+
### TypeScript types
|
|
32
|
+
|
|
33
|
+
The package selects WebGPU declarations for your compiler version. TypeScript 5 receives the bundled
|
|
34
|
+
`@webgpu/types` dependency; TypeScript 6 and later use WebGPU declarations from their DOM library. Include
|
|
35
|
+
the DOM library and keep `skipLibCheck` disabled to catch declaration conflicts:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"lib": ["ES2024", "DOM"],
|
|
41
|
+
"types": ["node"],
|
|
42
|
+
"skipLibCheck": false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
27
47
|
```ts
|
|
28
48
|
import { expect, it } from 'vitest';
|
|
29
49
|
|
|
@@ -55,5 +75,16 @@ await expect(canvas).toMatchScreenshot('scene.png');
|
|
|
55
75
|
```
|
|
56
76
|
|
|
57
77
|
`readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
|
|
58
|
-
[`vitest-screenshot`](../vitest-screenshot).
|
|
59
|
-
`
|
|
78
|
+
[`vitest-screenshot`](../vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
|
|
79
|
+
`bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
|
|
80
|
+
`readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
|
|
81
|
+
defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
|
|
82
|
+
explicitly with `canvas.asElement<HTMLCanvasElement>()`.
|
|
83
|
+
|
|
84
|
+
## Author
|
|
85
|
+
|
|
86
|
+
Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/canvas.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type RgbaImage = {
|
|
|
4
4
|
height: number;
|
|
5
5
|
data: Uint8Array;
|
|
6
6
|
};
|
|
7
|
+
type DefaultCanvasElement = typeof globalThis extends {
|
|
8
|
+
HTMLCanvasElement: {
|
|
9
|
+
prototype: infer TElement;
|
|
10
|
+
};
|
|
11
|
+
} ? TElement : HeadlessCanvas;
|
|
7
12
|
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
8
13
|
export declare class HeadlessCanvasContext {
|
|
9
14
|
#private;
|
|
@@ -28,8 +33,8 @@ export declare class HeadlessCanvas {
|
|
|
28
33
|
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
29
34
|
getContext(id: string): HeadlessCanvasContext | null;
|
|
30
35
|
readPixels(): Promise<RgbaImage>;
|
|
31
|
-
/** This same object,
|
|
32
|
-
asElement():
|
|
36
|
+
/** This same object, cast to the element type a rendering library expects. */
|
|
37
|
+
asElement<TElement = DefaultCanvasElement>(): TElement;
|
|
33
38
|
getBoundingClientRect(): {
|
|
34
39
|
x: number;
|
|
35
40
|
y: number;
|
|
@@ -45,4 +50,5 @@ export declare class HeadlessCanvas {
|
|
|
45
50
|
dispatchEvent(): boolean;
|
|
46
51
|
}
|
|
47
52
|
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
53
|
+
export {};
|
|
48
54
|
//# sourceMappingURL=canvas.d.ts.map
|
package/dist/canvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5E,KAAK,oBAAoB,GAAG,OAAO,UAAU,SAAS;IACpD,iBAAiB,EAAE;QAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;KAAE,CAAC;CAClD,GACG,QAAQ,GACR,cAAc,CAAC;AAanB,iIAAiI;AACjI,qBAAa,qBAAqB;;IAIpB,QAAQ,CAAC,MAAM,EAAE,cAAc;IAA3C,YAAqB,MAAM,EAAE,cAAc,EAAI;IAE/C,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAmCrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAMhB,KAAK;IACL,MAAM;IALf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C,YACS,KAAK,SAAM,EACX,MAAM,SAAM,EACjB;IAEJ,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,GAAG,oBAAoB,KAAK,QAAQ,CAErD;IAED,qBAAqB,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAE1G;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,gBAAgB,IAAI,IAAI,CAAG;IAC3B,mBAAmB,IAAI,IAAI,CAAG;IAC9B,aAAa,IAAI,OAAO,CAEvB;CACF;AAED,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
|
package/dist/canvas.js
CHANGED
|
@@ -2,6 +2,12 @@ const COPY_SRC = 0x01;
|
|
|
2
2
|
const RENDER_ATTACHMENT = 0x10;
|
|
3
3
|
const MAP_READ = 0x01;
|
|
4
4
|
const COPY_DST = 0x08;
|
|
5
|
+
const READABLE_FORMATS = new Set([
|
|
6
|
+
'rgba8unorm',
|
|
7
|
+
'rgba8unorm-srgb',
|
|
8
|
+
'bgra8unorm',
|
|
9
|
+
'bgra8unorm-srgb',
|
|
10
|
+
]);
|
|
5
11
|
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
6
12
|
export class HeadlessCanvasContext {
|
|
7
13
|
canvas;
|
|
@@ -38,26 +44,43 @@ export class HeadlessCanvasContext {
|
|
|
38
44
|
}
|
|
39
45
|
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
40
46
|
async readPixels() {
|
|
47
|
+
const config = this.#config;
|
|
48
|
+
if (!config)
|
|
49
|
+
throw new Error('readPixels() called before configure()');
|
|
50
|
+
const { device, format } = config;
|
|
51
|
+
if (!READABLE_FORMATS.has(format)) {
|
|
52
|
+
throw new Error(`readPixels() does not support canvas format "${format}"; supported formats are rgba8unorm, rgba8unorm-srgb, bgra8unorm, and bgra8unorm-srgb`);
|
|
53
|
+
}
|
|
41
54
|
const texture = this.getCurrentTexture();
|
|
42
|
-
const { device, format } = this.#config;
|
|
43
55
|
const { width, height } = texture;
|
|
44
56
|
const bytesPerRow = Math.ceil((width * 4) / 256) * 256;
|
|
45
57
|
const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
let mapped = false;
|
|
59
|
+
try {
|
|
60
|
+
const encoder = device.createCommandEncoder();
|
|
61
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
|
|
62
|
+
device.queue.submit([encoder.finish()]);
|
|
63
|
+
await buffer.mapAsync(MAP_READ);
|
|
64
|
+
mapped = true;
|
|
65
|
+
const padded = new Uint8Array(buffer.getMappedRange());
|
|
66
|
+
const data = new Uint8Array(width * height * 4);
|
|
67
|
+
for (let y = 0; y < height; y++)
|
|
68
|
+
data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
69
|
+
if (format.startsWith('bgra')) {
|
|
70
|
+
for (let i = 0; i < data.length; i += 4)
|
|
71
|
+
[data[i], data[i + 2]] = [data[i + 2], data[i]];
|
|
72
|
+
}
|
|
73
|
+
return { width, height, data };
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
try {
|
|
77
|
+
if (mapped)
|
|
78
|
+
buffer.unmap();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
buffer.destroy();
|
|
82
|
+
}
|
|
59
83
|
}
|
|
60
|
-
return { width, height, data };
|
|
61
84
|
}
|
|
62
85
|
#drop() {
|
|
63
86
|
this.#texture?.destroy();
|
|
@@ -87,7 +110,7 @@ export class HeadlessCanvas {
|
|
|
87
110
|
readPixels() {
|
|
88
111
|
return this.#context.readPixels();
|
|
89
112
|
}
|
|
90
|
-
/** This same object,
|
|
113
|
+
/** This same object, cast to the element type a rendering library expects. */
|
|
91
114
|
asElement() {
|
|
92
115
|
return this;
|
|
93
116
|
}
|
package/dist/canvas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import type { Environment } from 'vitest/environments';
|
|
2
1
|
export { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';
|
|
3
2
|
/** Set under `test.environmentOptions.webgpuNode` in your Vitest config. */
|
|
4
3
|
export type WebgpuNodeOptions = {
|
|
5
4
|
/** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
|
|
6
5
|
dawnOptions?: string[];
|
|
7
6
|
};
|
|
8
|
-
declare const _default:
|
|
7
|
+
declare const _default: {
|
|
8
|
+
name: string;
|
|
9
|
+
transformMode: "ssr";
|
|
10
|
+
viteEnvironment: "ssr";
|
|
11
|
+
setup(global: Record<string, unknown>, { webgpuNode }: {
|
|
12
|
+
webgpuNode?: WebgpuNodeOptions;
|
|
13
|
+
}): {
|
|
14
|
+
teardown(_global?: Record<string, unknown>): void;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
9
17
|
export default _default;
|
|
10
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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;;;;;kBA0Cc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;2BA8BvE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -25,21 +25,52 @@ const domShims = () => {
|
|
|
25
25
|
cancelAnimationFrame: clearTimeout,
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
|
+
const restore = (properties) => {
|
|
29
|
+
for (const { target, key, descriptor } of properties.toReversed()) {
|
|
30
|
+
if (descriptor)
|
|
31
|
+
Object.defineProperty(target, key, descriptor);
|
|
32
|
+
else
|
|
33
|
+
Reflect.deleteProperty(target, key);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
28
36
|
export default {
|
|
29
37
|
name: 'webgpu-node',
|
|
38
|
+
transformMode: 'ssr',
|
|
30
39
|
viteEnvironment: 'ssr',
|
|
31
40
|
setup(global, { webgpuNode = {} }) {
|
|
32
41
|
const shims = { ...globals, ...domShims() };
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
const modified = [];
|
|
43
|
+
try {
|
|
44
|
+
for (const key of Object.keys(shims)) {
|
|
45
|
+
if (key in global)
|
|
46
|
+
continue;
|
|
47
|
+
modified.push({ target: global, key, descriptor: Object.getOwnPropertyDescriptor(global, key) });
|
|
48
|
+
global[key] = shims[key];
|
|
49
|
+
}
|
|
50
|
+
if (global.navigator == null) {
|
|
51
|
+
modified.push({
|
|
52
|
+
target: global,
|
|
53
|
+
key: 'navigator',
|
|
54
|
+
descriptor: Object.getOwnPropertyDescriptor(global, 'navigator'),
|
|
55
|
+
});
|
|
56
|
+
global.navigator = {};
|
|
57
|
+
}
|
|
58
|
+
const navigator = global.navigator;
|
|
59
|
+
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
60
|
+
Object.defineProperty(navigator, 'gpu', {
|
|
61
|
+
value: create(webgpuNode.dawnOptions ?? []),
|
|
62
|
+
writable: false,
|
|
63
|
+
enumerable: false,
|
|
64
|
+
configurable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
restore(modified);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
38
71
|
return {
|
|
39
|
-
teardown(
|
|
40
|
-
|
|
41
|
-
for (const key of added)
|
|
42
|
-
delete g[key];
|
|
72
|
+
teardown(_global) {
|
|
73
|
+
restore(modified);
|
|
43
74
|
},
|
|
44
75
|
};
|
|
45
76
|
},
|
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,oHAAoH;AACpH,MAAM,qBAAqB,GAAG,CAAC,QAAgC,EAAkB,EAAE,CACjF,UAAU,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;AAE5D,MAAM,QAAQ,GAAG,GAA4B,EAAE;IAC7C,MAAM,MAAM,GAAG;QACb,gBAAgB,EAAE,CAAC;QACnB,qBAAqB;QACrB,oBAAoB,EAAE,YAAY;QAClC,gBAAgB,KAAI,CAAC;QACrB,mBAAmB,KAAI,CAAC;KACzB,CAAC;IACF,OAAO;QACL,iBAAiB,EAAE,cAAc;QACjC,QAAQ,EAAE;YACR,aAAa,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,eAAe,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7F,gBAAgB,KAAI,CAAC;YACrB,mBAAmB,KAAI,CAAC;SACzB;QACD,MAAM;QACN,IAAI,EAAE,MAAM;QACZ,qBAAqB;QACrB,oBAAoB,EAAE,YAAY;KACnC,CAAC;AACJ,CAAC,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/legacy.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-environment-webgpu-node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Vitest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dawn",
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
"directory": "packages/vitest-environment-webgpu-node"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
+
"legacy.d.ts",
|
|
25
26
|
"dist/**/*.js",
|
|
26
27
|
"dist/**/*.js.map",
|
|
27
28
|
"dist/**/*.d.ts",
|
|
28
29
|
"dist/**/*.d.ts.map",
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"!src/**/*.test.ts",
|
|
29
32
|
"CHANGELOG.md"
|
|
30
33
|
],
|
|
31
34
|
"type": "module",
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
"types": "./dist/index.d.ts",
|
|
34
37
|
"exports": {
|
|
35
38
|
".": {
|
|
39
|
+
"types@<6.0": "./legacy.d.ts",
|
|
36
40
|
"types": "./dist/index.d.ts",
|
|
37
41
|
"import": "./dist/index.js"
|
|
38
42
|
}
|
package/src/canvas.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
|
|
2
|
+
export type RgbaImage = { width: number; height: number; data: Uint8Array };
|
|
3
|
+
|
|
4
|
+
type DefaultCanvasElement = typeof globalThis extends {
|
|
5
|
+
HTMLCanvasElement: { prototype: infer TElement };
|
|
6
|
+
}
|
|
7
|
+
? TElement
|
|
8
|
+
: HeadlessCanvas;
|
|
9
|
+
|
|
10
|
+
const COPY_SRC = 0x01;
|
|
11
|
+
const RENDER_ATTACHMENT = 0x10;
|
|
12
|
+
const MAP_READ = 0x01;
|
|
13
|
+
const COPY_DST = 0x08;
|
|
14
|
+
const READABLE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
|
|
15
|
+
'rgba8unorm',
|
|
16
|
+
'rgba8unorm-srgb',
|
|
17
|
+
'bgra8unorm',
|
|
18
|
+
'bgra8unorm-srgb',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
22
|
+
export class HeadlessCanvasContext {
|
|
23
|
+
#config: GPUCanvasConfiguration | undefined;
|
|
24
|
+
#texture: GPUTexture | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(readonly canvas: HeadlessCanvas) {}
|
|
27
|
+
|
|
28
|
+
configure(config: GPUCanvasConfiguration): void {
|
|
29
|
+
this.#config = config;
|
|
30
|
+
this.#drop();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
unconfigure(): void {
|
|
34
|
+
this.#config = undefined;
|
|
35
|
+
this.#drop();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getConfiguration(): GPUCanvasConfiguration | null {
|
|
39
|
+
return this.#config ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getCurrentTexture(): GPUTexture {
|
|
43
|
+
const config = this.#config;
|
|
44
|
+
if (!config) throw new Error('getCurrentTexture() called before configure()');
|
|
45
|
+
const { width, height } = this.canvas;
|
|
46
|
+
if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();
|
|
47
|
+
this.#texture ??= config.device.createTexture({
|
|
48
|
+
size: [width, height],
|
|
49
|
+
format: config.format,
|
|
50
|
+
usage: (config.usage ?? RENDER_ATTACHMENT) | RENDER_ATTACHMENT | COPY_SRC,
|
|
51
|
+
viewFormats: config.viewFormats,
|
|
52
|
+
});
|
|
53
|
+
return this.#texture;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
57
|
+
async readPixels(): Promise<RgbaImage> {
|
|
58
|
+
const config = this.#config;
|
|
59
|
+
if (!config) throw new Error('readPixels() called before configure()');
|
|
60
|
+
const { device, format } = config;
|
|
61
|
+
if (!READABLE_FORMATS.has(format)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`readPixels() does not support canvas format "${format}"; supported formats are rgba8unorm, rgba8unorm-srgb, bgra8unorm, and bgra8unorm-srgb`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const texture = this.getCurrentTexture();
|
|
67
|
+
const { width, height } = texture;
|
|
68
|
+
const bytesPerRow = Math.ceil((width * 4) / 256) * 256;
|
|
69
|
+
const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });
|
|
70
|
+
let mapped = false;
|
|
71
|
+
try {
|
|
72
|
+
const encoder = device.createCommandEncoder();
|
|
73
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
|
|
74
|
+
device.queue.submit([encoder.finish()]);
|
|
75
|
+
await buffer.mapAsync(MAP_READ);
|
|
76
|
+
mapped = true;
|
|
77
|
+
const padded = new Uint8Array(buffer.getMappedRange());
|
|
78
|
+
const data = new Uint8Array(width * height * 4);
|
|
79
|
+
for (let y = 0; y < height; y++)
|
|
80
|
+
data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
81
|
+
if (format.startsWith('bgra')) {
|
|
82
|
+
for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2]!, data[i]!];
|
|
83
|
+
}
|
|
84
|
+
return { width, height, data };
|
|
85
|
+
} finally {
|
|
86
|
+
try {
|
|
87
|
+
if (mapped) buffer.unmap();
|
|
88
|
+
} finally {
|
|
89
|
+
buffer.destroy();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#drop(): void {
|
|
95
|
+
this.#texture?.destroy();
|
|
96
|
+
this.#texture = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
101
|
+
export class HeadlessCanvas {
|
|
102
|
+
readonly #context = new HeadlessCanvasContext(this);
|
|
103
|
+
readonly style: Record<string, string> = {};
|
|
104
|
+
readonly #attributes = new Map<string, string>();
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
public width = 300,
|
|
108
|
+
public height = 150,
|
|
109
|
+
) {}
|
|
110
|
+
|
|
111
|
+
get clientWidth(): number {
|
|
112
|
+
return this.width;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get clientHeight(): number {
|
|
116
|
+
return this.height;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
120
|
+
getContext(id: string): HeadlessCanvasContext | null;
|
|
121
|
+
getContext(id: string): HeadlessCanvasContext | null {
|
|
122
|
+
return id === 'webgpu' ? this.#context : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
readPixels(): Promise<RgbaImage> {
|
|
126
|
+
return this.#context.readPixels();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** This same object, cast to the element type a rendering library expects. */
|
|
130
|
+
asElement<TElement = DefaultCanvasElement>(): TElement {
|
|
131
|
+
return this as unknown as TElement;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getBoundingClientRect(): { x: number; y: number; width: number; height: number; top: number; left: number } {
|
|
135
|
+
return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setAttribute(name: string, value: string): void {
|
|
139
|
+
this.#attributes.set(name, value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getAttribute(name: string): string | null {
|
|
143
|
+
return this.#attributes.get(name) ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
addEventListener(): void {}
|
|
147
|
+
removeEventListener(): void {}
|
|
148
|
+
dispatchEvent(): boolean {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Environment } from 'vitest/environments';
|
|
2
|
+
import { create, globals } from 'webgpu';
|
|
3
|
+
import { HeadlessCanvas } from './canvas.js';
|
|
4
|
+
|
|
5
|
+
export { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';
|
|
6
|
+
|
|
7
|
+
/** Set under `test.environmentOptions.webgpuNode` in your Vitest config. */
|
|
8
|
+
export type WebgpuNodeOptions = {
|
|
9
|
+
/** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
|
|
10
|
+
dawnOptions?: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
|
|
14
|
+
const requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout =>
|
|
15
|
+
setTimeout(() => callback(performance.now()), 16).unref();
|
|
16
|
+
|
|
17
|
+
const domShims = (): Record<string, unknown> => {
|
|
18
|
+
const window = {
|
|
19
|
+
devicePixelRatio: 1,
|
|
20
|
+
requestAnimationFrame,
|
|
21
|
+
cancelAnimationFrame: clearTimeout,
|
|
22
|
+
addEventListener() {},
|
|
23
|
+
removeEventListener() {},
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
HTMLCanvasElement: HeadlessCanvas,
|
|
27
|
+
document: {
|
|
28
|
+
createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
|
|
29
|
+
createElementNS: (_ns: string, tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
|
|
30
|
+
addEventListener() {},
|
|
31
|
+
removeEventListener() {},
|
|
32
|
+
},
|
|
33
|
+
window,
|
|
34
|
+
self: window,
|
|
35
|
+
requestAnimationFrame,
|
|
36
|
+
cancelAnimationFrame: clearTimeout,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SavedProperty = { target: object; key: PropertyKey; descriptor?: PropertyDescriptor };
|
|
41
|
+
|
|
42
|
+
const restore = (properties: SavedProperty[]): void => {
|
|
43
|
+
for (const { target, key, descriptor } of properties.toReversed()) {
|
|
44
|
+
if (descriptor) Object.defineProperty(target, key, descriptor);
|
|
45
|
+
else Reflect.deleteProperty(target, key);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
name: 'webgpu-node',
|
|
51
|
+
transformMode: 'ssr',
|
|
52
|
+
viteEnvironment: 'ssr',
|
|
53
|
+
setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
|
|
54
|
+
const shims = { ...globals, ...domShims() } as Record<string, unknown>;
|
|
55
|
+
const modified: SavedProperty[] = [];
|
|
56
|
+
try {
|
|
57
|
+
for (const key of Object.keys(shims)) {
|
|
58
|
+
if (key in global) continue;
|
|
59
|
+
modified.push({ target: global, key, descriptor: Object.getOwnPropertyDescriptor(global, key) });
|
|
60
|
+
global[key] = shims[key];
|
|
61
|
+
}
|
|
62
|
+
if (global.navigator == null) {
|
|
63
|
+
modified.push({
|
|
64
|
+
target: global,
|
|
65
|
+
key: 'navigator',
|
|
66
|
+
descriptor: Object.getOwnPropertyDescriptor(global, 'navigator'),
|
|
67
|
+
});
|
|
68
|
+
global.navigator = {};
|
|
69
|
+
}
|
|
70
|
+
const navigator = global.navigator as object;
|
|
71
|
+
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
72
|
+
Object.defineProperty(navigator, 'gpu', {
|
|
73
|
+
value: create(webgpuNode.dawnOptions ?? []),
|
|
74
|
+
writable: false,
|
|
75
|
+
enumerable: false,
|
|
76
|
+
configurable: true,
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
restore(modified);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
teardown(_global?: Record<string, unknown>) {
|
|
84
|
+
restore(modified);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
} satisfies Environment;
|