vitest-environment-webgpu-node 3.0.0 → 3.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/README.md +91 -26
- package/dist/canvas.d.ts +86 -3
- package/dist/canvas.d.ts.map +1 -1
- package/dist/canvas.js +62 -3
- package/dist/canvas.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -9
- package/dist/index.js.map +1 -1
- package/package.json +17 -6
- package/src/canvas.ts +94 -4
- package/src/index.ts +87 -12
package/README.md
CHANGED
|
@@ -1,30 +1,83 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://npmjs.com/package/vitest-environment-webgpu-node"><img src="https://img.shields.io/npm/v/vitest-environment-webgpu-node.svg" alt="npm package"></a>
|
|
3
|
+
<a href="https://www.npmtrends.com/vitest-environment-webgpu-node"><img src="https://img.shields.io/npm/dw/vitest-environment-webgpu-node.svg" alt="npm downloads"></a>
|
|
4
|
+
<a href="https://codecov.io/gh/bhouston/vitest-gpu"><img src="https://codecov.io/gh/bhouston/vitest-gpu/branch/main/graph/badge.svg" alt="coverage"></a>
|
|
5
|
+
<a href="https://discord.gg/fwupDN493R"><img src="https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord" alt="discord chat"></a>
|
|
6
|
+
<a href="https://vitest-gpu.ben3d.ca/"><img src="https://img.shields.io/badge/docs-read-blue?style=flat" alt="documentation"></a>
|
|
7
|
+
</p>
|
|
8
|
+
<br/>
|
|
9
|
+
|
|
1
10
|
# vitest-environment-webgpu-node
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
12
|
+
> Real WebGPU tests in Vitest, with no browser.
|
|
13
|
+
|
|
14
|
+
- 🖥️ Real WebGPU via Dawn
|
|
15
|
+
- 🚫 No Browser or Mocks
|
|
16
|
+
- 🧩 Headless Canvas Included
|
|
17
|
+
- 🔧 Dawn Backend Options
|
|
18
|
+
- ⚡ Up to 2.4x Faster Than Browsers
|
|
19
|
+
|
|
20
|
+
Headless GPU testing for CI: `navigator.gpu` is backed by Google's Dawn through the
|
|
21
|
+
[`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package. Native GPU testing is up to 2.4x faster than
|
|
22
|
+
running the same tests in a browser, see the [blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
|
|
23
|
+
|
|
24
|
+
Part of [vitest-gpu](https://github.com/bhouston/vitest-gpu), which also provides WebGL and image snapshot packages.
|
|
25
|
+
|
|
26
|
+
[Read the Docs to Learn More](https://vitest-gpu.ben3d.ca/docs/webgpu/) about the WebGPU environment: Dawn options, TypeScript types, the headless canvas and CI setup.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm add -D vitest vitest-environment-webgpu-node
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// vitest.config.ts
|
|
34
|
+
import { defineConfig } from 'vitest/config';
|
|
35
|
+
|
|
36
|
+
export default defineConfig({ test: { environment: 'webgpu-node' } });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// gpu.test.ts
|
|
41
|
+
import { expect, it } from 'vitest';
|
|
7
42
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
43
|
+
it('has a real GPU device', async () => {
|
|
44
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
45
|
+
const device = await adapter!.requestDevice();
|
|
46
|
+
const buffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST });
|
|
47
|
+
expect(buffer.size).toBe(16);
|
|
48
|
+
});
|
|
49
|
+
```
|
|
13
50
|
|
|
14
51
|
The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
|
|
15
52
|
`GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
|
|
16
53
|
|
|
17
54
|
Using Jest instead? See the equivalent
|
|
18
55
|
[`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
|
|
19
|
-
in [jest-gpu](https://github.com/bhouston/jest-gpu).
|
|
56
|
+
in [jest-gpu](https://github.com/bhouston/jest-gpu), documented at [jest-gpu.ben3d.ca](https://jest-gpu.ben3d.ca/).
|
|
20
57
|
|
|
21
|
-
##
|
|
58
|
+
## Compared to alternatives
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
WebGPU has no mock equivalent of a canvas stub that runs shaders, so the realistic options are a browser or
|
|
61
|
+
Dawn in Node.
|
|
62
|
+
|
|
63
|
+
| Approach | Real rendering | Notes |
|
|
64
|
+
| --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| Hand-written `navigator.gpu` mock | No | You can assert calls, not shader output or readbacks. |
|
|
66
|
+
| 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. |
|
|
67
|
+
| vitest-environment-webgpu-node | Yes | Dawn in the Node process (Metal, Vulkan, D3D12 and others via `dawnOptions`). Not a browser. |
|
|
68
|
+
|
|
69
|
+
## Supported versions and platforms
|
|
70
|
+
|
|
71
|
+
- Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
|
|
72
|
+
- Node 22 or later. CI runs Node 22 and 26 on `macos-latest`, `ubuntu-latest` and `windows-latest`.
|
|
73
|
+
- Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
|
|
74
|
+
- Linux without a GPU needs Mesa's software Vulkan driver, see [Linux and CI](#linux-and-ci).
|
|
75
|
+
|
|
76
|
+
## Useful for
|
|
26
77
|
|
|
27
|
-
|
|
78
|
+
three.js `WebGPURenderer`, Babylon Lite, Vercel's vgpu and raw WebGPU compute or render code. The demos exercise
|
|
79
|
+
those three; other WebGPU libraries such as TypeGPU or luma.gl use the same API but are not covered by this
|
|
80
|
+
repo's tests. These are compatible use cases, not endorsements.
|
|
28
81
|
|
|
29
82
|
## Usage
|
|
30
83
|
|
|
@@ -77,7 +130,7 @@ sudo apt-get update && sudo apt-get install -y libegl1 libgles2 libgl1-mesa-dri
|
|
|
77
130
|
export LIBGL_ALWAYS_SOFTWARE=1
|
|
78
131
|
```
|
|
79
132
|
|
|
80
|
-
This repo's own [CI workflow](
|
|
133
|
+
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
134
|
for real (not mocked) on `ubuntu-latest` on every PR. macOS runners use Dawn's Metal backend and need no
|
|
82
135
|
extra setup.
|
|
83
136
|
|
|
@@ -100,24 +153,36 @@ await expect(canvas).toMatchScreenshot('scene.png');
|
|
|
100
153
|
```
|
|
101
154
|
|
|
102
155
|
`readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
|
|
103
|
-
[`vitest-screenshot`](
|
|
156
|
+
[`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
|
|
104
157
|
`bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
|
|
105
158
|
`readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
|
|
106
159
|
defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
|
|
107
160
|
explicitly with `canvas.asElement<HTMLCanvasElement>()`.
|
|
108
161
|
|
|
109
|
-
##
|
|
162
|
+
## Image snapshots
|
|
163
|
+
|
|
164
|
+
`await expect(canvas).toMatchScreenshot('scene.png')` comes from [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot).
|
|
165
|
+
Call `extendMatchers()` from it once, then pass the canvas as in the example above.
|
|
166
|
+
|
|
167
|
+
## See also
|
|
168
|
+
|
|
169
|
+
- [`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node) for headless WebGL.
|
|
170
|
+
- [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) for `toMatchScreenshot()`.
|
|
171
|
+
- [`demo/`](https://github.com/bhouston/vitest-gpu/tree/main/demo/test/webgpu) for device, upload, readback, triangle, three.js, Babylon Lite and vgpu tests,
|
|
172
|
+
and the [repository README](https://github.com/bhouston/vitest-gpu#readme).
|
|
173
|
+
|
|
174
|
+
## Resource cleanup
|
|
110
175
|
|
|
111
|
-
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
## Contribution
|
|
179
|
+
|
|
180
|
+
See [Contributing Guide](https://github.com/bhouston/vitest-gpu/blob/main/CONTRIBUTING.md).
|
|
112
181
|
|
|
113
182
|
## License
|
|
114
183
|
|
|
115
184
|
MIT
|
|
116
185
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
[
|
|
120
|
-
[tests-badge]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml/badge.svg
|
|
121
|
-
[tests-url]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml
|
|
122
|
-
[discord-badge]: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
|
|
123
|
-
[discord-url]: https://discord.gg/fwupDN493R
|
|
186
|
+
## Author
|
|
187
|
+
|
|
188
|
+
[Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
|
package/dist/canvas.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
|
|
2
2
|
export type RgbaImage = {
|
|
3
|
+
/** Width in pixels. */
|
|
3
4
|
width: number;
|
|
5
|
+
/** Height in pixels. */
|
|
4
6
|
height: number;
|
|
7
|
+
/** RGBA8 bytes, four per pixel, row by row from the top. */
|
|
5
8
|
data: Uint8Array;
|
|
6
9
|
};
|
|
7
10
|
type DefaultCanvasElement = typeof globalThis extends {
|
|
@@ -12,11 +15,30 @@ type DefaultCanvasElement = typeof globalThis extends {
|
|
|
12
15
|
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
13
16
|
export declare class HeadlessCanvasContext {
|
|
14
17
|
#private;
|
|
18
|
+
/** The canvas this context belongs to. */
|
|
15
19
|
readonly canvas: HeadlessCanvas;
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Creates the context for a canvas. Canvases create their own; you rarely call this.
|
|
22
|
+
*
|
|
23
|
+
* @param canvas - The canvas whose size this context's texture follows.
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
/** The canvas this context belongs to. */
|
|
27
|
+
canvas: HeadlessCanvas);
|
|
28
|
+
/**
|
|
29
|
+
* Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
|
|
30
|
+
*
|
|
31
|
+
* @param config - Device, format and usage for the canvas texture.
|
|
32
|
+
*/
|
|
17
33
|
configure(config: GPUCanvasConfiguration): void;
|
|
34
|
+
/** Clears the configuration and discards the current texture. */
|
|
18
35
|
unconfigure(): void;
|
|
36
|
+
/** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
19
37
|
getConfiguration(): GPUCanvasConfiguration | null;
|
|
38
|
+
/**
|
|
39
|
+
* The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
|
|
40
|
+
* is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
|
|
41
|
+
*/
|
|
20
42
|
getCurrentTexture(): GPUTexture;
|
|
21
43
|
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
22
44
|
readPixels(): Promise<RgbaImage>;
|
|
@@ -24,31 +46,92 @@ export declare class HeadlessCanvasContext {
|
|
|
24
46
|
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
25
47
|
export declare class HeadlessCanvas {
|
|
26
48
|
#private;
|
|
49
|
+
/** Canvas width in pixels. */
|
|
27
50
|
width: number;
|
|
51
|
+
/** Canvas height in pixels. */
|
|
28
52
|
height: number;
|
|
53
|
+
/** A plain object libraries can write CSS properties to; it has no effect on rendering. */
|
|
29
54
|
readonly style: Record<string, string>;
|
|
30
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
|
|
57
|
+
*
|
|
58
|
+
* @param width - Canvas width in pixels.
|
|
59
|
+
* @param height - Canvas height in pixels.
|
|
60
|
+
*/
|
|
61
|
+
constructor(
|
|
62
|
+
/** Canvas width in pixels. */
|
|
63
|
+
width?: number,
|
|
64
|
+
/** Canvas height in pixels. */
|
|
65
|
+
height?: number);
|
|
66
|
+
/** Same as `width`; there is no layout in Node. */
|
|
31
67
|
get clientWidth(): number;
|
|
68
|
+
/** Same as `height`; there is no layout in Node. */
|
|
32
69
|
get clientHeight(): number;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
|
|
72
|
+
*
|
|
73
|
+
* @param id - The context type. Only `'webgpu'` is supported.
|
|
74
|
+
* @returns The context for `'webgpu'`, or `null` for any other type.
|
|
75
|
+
*/
|
|
33
76
|
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
|
|
79
|
+
*
|
|
80
|
+
* @param id - The context type. Only `'webgpu'` is supported.
|
|
81
|
+
* @returns The context for `'webgpu'`, or `null` for any other type.
|
|
82
|
+
*/
|
|
34
83
|
getContext(id: string): HeadlessCanvasContext | null;
|
|
84
|
+
/** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
|
|
35
85
|
readPixels(): Promise<RgbaImage>;
|
|
36
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* This same object, cast to the element type a rendering library expects.
|
|
88
|
+
*
|
|
89
|
+
* @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
|
|
90
|
+
*/
|
|
37
91
|
asElement<TElement = DefaultCanvasElement>(): TElement;
|
|
92
|
+
/** A rectangle at the origin with the canvas's width and height. */
|
|
38
93
|
getBoundingClientRect(): {
|
|
94
|
+
/** Always 0. */
|
|
39
95
|
x: number;
|
|
96
|
+
/** Always 0. */
|
|
40
97
|
y: number;
|
|
98
|
+
/** The canvas width. */
|
|
41
99
|
width: number;
|
|
100
|
+
/** The canvas height. */
|
|
42
101
|
height: number;
|
|
102
|
+
/** Always 0. */
|
|
43
103
|
top: number;
|
|
104
|
+
/** Always 0. */
|
|
44
105
|
left: number;
|
|
45
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Stores an attribute on the canvas.
|
|
109
|
+
*
|
|
110
|
+
* @param name - Attribute name.
|
|
111
|
+
* @param value - Attribute value.
|
|
112
|
+
*/
|
|
46
113
|
setAttribute(name: string, value: string): void;
|
|
114
|
+
/**
|
|
115
|
+
* Reads an attribute stored with `setAttribute()`.
|
|
116
|
+
*
|
|
117
|
+
* @param name - Attribute name.
|
|
118
|
+
* @returns The stored value, or `null` if none was set.
|
|
119
|
+
*/
|
|
47
120
|
getAttribute(name: string): string | null;
|
|
121
|
+
/** Does nothing; present so libraries that register listeners keep working. */
|
|
48
122
|
addEventListener(): void;
|
|
123
|
+
/** Does nothing; present so libraries that remove listeners keep working. */
|
|
49
124
|
removeEventListener(): void;
|
|
125
|
+
/** Does nothing and returns `true`. */
|
|
50
126
|
dispatchEvent(): boolean;
|
|
51
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a headless canvas for WebGPU tests.
|
|
130
|
+
*
|
|
131
|
+
* @param width - Canvas width in pixels.
|
|
132
|
+
* @param height - Canvas height in pixels.
|
|
133
|
+
* @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
|
|
134
|
+
*/
|
|
52
135
|
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
53
136
|
export {};
|
|
54
137
|
//# sourceMappingURL=canvas.d.ts.map
|
package/dist/canvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.d.ts","
|
|
1
|
+
{"version":3,"file":"canvas.d.ts","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AAEF,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;;IAU9B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,cAAc;IAPjC;;;;OAIG;IACH;IACE,0CAA0C;IACjC,MAAM,EAAE,cAAc,EAC7B;IAEJ;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,iEAAiE;IACjE,WAAW,IAAI,IAAI,CAGlB;IAED,yGAAyG;IACzG,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED;;;OAGG;IACH,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAmCrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAavB,8BAA8B;IACvB,KAAK;IACZ,+BAA+B;IACxB,MAAM;IAdf,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C;;;;;OAKG;IACH;IACE,8BAA8B;IACvB,KAAK,SAAM;IAClB,+BAA+B;IACxB,MAAM,SAAM,EACjB;IAEJ,mDAAmD;IACnD,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,oDAAoD;IACpD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,oGAAoG;IACpG,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAQ,GAAG,oBAAoB,KAAK,QAAQ,CAErD;IAED,oEAAoE;IACpE,qBAAqB,IAAI;QACvB,gBAAgB;QAChB,CAAC,EAAE,MAAM,CAAC;QACV,gBAAgB;QAChB,CAAC,EAAE,MAAM,CAAC;QACV,wBAAwB;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,yBAAyB;QACzB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,gBAAgB;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAEA;IAED;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,+EAA+E;IAC/E,gBAAgB,IAAI,IAAI,CAAG;IAC3B,6EAA6E;IAC7E,mBAAmB,IAAI,IAAI,CAAG;IAC9B,uCAAuC;IACvC,aAAa,IAAI,OAAO,CAEvB;CACF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
|
package/dist/canvas.js
CHANGED
|
@@ -13,20 +13,38 @@ export class HeadlessCanvasContext {
|
|
|
13
13
|
canvas;
|
|
14
14
|
#config;
|
|
15
15
|
#texture;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Creates the context for a canvas. Canvases create their own; you rarely call this.
|
|
18
|
+
*
|
|
19
|
+
* @param canvas - The canvas whose size this context's texture follows.
|
|
20
|
+
*/
|
|
21
|
+
constructor(
|
|
22
|
+
/** The canvas this context belongs to. */
|
|
23
|
+
canvas) {
|
|
17
24
|
this.canvas = canvas;
|
|
18
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Device, format and usage for the canvas texture.
|
|
30
|
+
*/
|
|
19
31
|
configure(config) {
|
|
20
32
|
this.#config = config;
|
|
21
33
|
this.#drop();
|
|
22
34
|
}
|
|
35
|
+
/** Clears the configuration and discards the current texture. */
|
|
23
36
|
unconfigure() {
|
|
24
37
|
this.#config = undefined;
|
|
25
38
|
this.#drop();
|
|
26
39
|
}
|
|
40
|
+
/** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
27
41
|
getConfiguration() {
|
|
28
42
|
return this.#config ?? null;
|
|
29
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
|
|
46
|
+
* is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
|
|
47
|
+
*/
|
|
30
48
|
getCurrentTexture() {
|
|
31
49
|
const config = this.#config;
|
|
32
50
|
if (!config)
|
|
@@ -92,42 +110,83 @@ export class HeadlessCanvas {
|
|
|
92
110
|
width;
|
|
93
111
|
height;
|
|
94
112
|
#context = new HeadlessCanvasContext(this);
|
|
113
|
+
/** A plain object libraries can write CSS properties to; it has no effect on rendering. */
|
|
95
114
|
style = {};
|
|
96
115
|
#attributes = new Map();
|
|
97
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
|
|
118
|
+
*
|
|
119
|
+
* @param width - Canvas width in pixels.
|
|
120
|
+
* @param height - Canvas height in pixels.
|
|
121
|
+
*/
|
|
122
|
+
constructor(
|
|
123
|
+
/** Canvas width in pixels. */
|
|
124
|
+
width = 300,
|
|
125
|
+
/** Canvas height in pixels. */
|
|
126
|
+
height = 150) {
|
|
98
127
|
this.width = width;
|
|
99
128
|
this.height = height;
|
|
100
129
|
}
|
|
130
|
+
/** Same as `width`; there is no layout in Node. */
|
|
101
131
|
get clientWidth() {
|
|
102
132
|
return this.width;
|
|
103
133
|
}
|
|
134
|
+
/** Same as `height`; there is no layout in Node. */
|
|
104
135
|
get clientHeight() {
|
|
105
136
|
return this.height;
|
|
106
137
|
}
|
|
107
138
|
getContext(id) {
|
|
108
139
|
return id === 'webgpu' ? this.#context : null;
|
|
109
140
|
}
|
|
141
|
+
/** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
|
|
110
142
|
readPixels() {
|
|
111
143
|
return this.#context.readPixels();
|
|
112
144
|
}
|
|
113
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* This same object, cast to the element type a rendering library expects.
|
|
147
|
+
*
|
|
148
|
+
* @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
|
|
149
|
+
*/
|
|
114
150
|
asElement() {
|
|
115
151
|
return this;
|
|
116
152
|
}
|
|
153
|
+
/** A rectangle at the origin with the canvas's width and height. */
|
|
117
154
|
getBoundingClientRect() {
|
|
118
155
|
return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
|
|
119
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Stores an attribute on the canvas.
|
|
159
|
+
*
|
|
160
|
+
* @param name - Attribute name.
|
|
161
|
+
* @param value - Attribute value.
|
|
162
|
+
*/
|
|
120
163
|
setAttribute(name, value) {
|
|
121
164
|
this.#attributes.set(name, value);
|
|
122
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Reads an attribute stored with `setAttribute()`.
|
|
168
|
+
*
|
|
169
|
+
* @param name - Attribute name.
|
|
170
|
+
* @returns The stored value, or `null` if none was set.
|
|
171
|
+
*/
|
|
123
172
|
getAttribute(name) {
|
|
124
173
|
return this.#attributes.get(name) ?? null;
|
|
125
174
|
}
|
|
175
|
+
/** Does nothing; present so libraries that register listeners keep working. */
|
|
126
176
|
addEventListener() { }
|
|
177
|
+
/** Does nothing; present so libraries that remove listeners keep working. */
|
|
127
178
|
removeEventListener() { }
|
|
179
|
+
/** Does nothing and returns `true`. */
|
|
128
180
|
dispatchEvent() {
|
|
129
181
|
return true;
|
|
130
182
|
}
|
|
131
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Creates a headless canvas for WebGPU tests.
|
|
186
|
+
*
|
|
187
|
+
* @param width - Canvas width in pixels.
|
|
188
|
+
* @param height - Canvas height in pixels.
|
|
189
|
+
* @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
|
|
190
|
+
*/
|
|
132
191
|
export const createCanvas = (width, height) => new HeadlessCanvas(width, height);
|
|
133
192
|
//# sourceMappingURL=canvas.js.map
|
package/dist/canvas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","
|
|
1
|
+
{"version":3,"file":"canvas.js","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,13 +4,33 @@ export type WebgpuNodeOptions = {
|
|
|
4
4
|
/** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
|
|
5
5
|
dawnOptions?: string[];
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
|
|
9
|
+
* classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
|
|
10
|
+
* on the global object (only where missing), and it removes them afterwards.
|
|
11
|
+
*/
|
|
7
12
|
declare const _default: {
|
|
13
|
+
/** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
|
|
8
14
|
name: string;
|
|
15
|
+
/** Runs test files through Vite's `ssr` environment, as Node code. */
|
|
9
16
|
viteEnvironment: "ssr";
|
|
17
|
+
/**
|
|
18
|
+
* Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
|
|
19
|
+
*
|
|
20
|
+
* @param global - The global object of the test file's context.
|
|
21
|
+
* @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
|
|
22
|
+
* @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
|
|
23
|
+
*/
|
|
10
24
|
setup(global: Record<string, unknown>, { webgpuNode }: {
|
|
11
25
|
webgpuNode?: WebgpuNodeOptions;
|
|
12
26
|
}): {
|
|
13
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
|
|
29
|
+
* removes the globals that `setup` added.
|
|
30
|
+
*
|
|
31
|
+
* @param _global - Unused; teardown restores the global object it captured in `setup`.
|
|
32
|
+
*/
|
|
33
|
+
teardown: (_global?: Record<string, unknown>) => void;
|
|
14
34
|
};
|
|
15
35
|
};
|
|
16
36
|
export default _default;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","
|
|
1
|
+
{"version":3,"file":"index.d.ts","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;AAwDF;;;;GAIG;;IAED,0FAA0F;;IAE1F,sEAAsE;;IAEtE;;;;;;OAMG;kBACW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;QA6D1F;;;;;WAKG;6BACkB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;AA/ElD,wBA0FwB"}
|
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()) {
|
|
@@ -33,11 +55,36 @@ const restore = (properties) => {
|
|
|
33
55
|
Reflect.deleteProperty(target, key);
|
|
34
56
|
}
|
|
35
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
|
|
60
|
+
* classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
|
|
61
|
+
* on the global object (only where missing), and it removes them afterwards.
|
|
62
|
+
*/
|
|
36
63
|
export default {
|
|
64
|
+
/** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
|
|
37
65
|
name: 'webgpu-node',
|
|
66
|
+
/** Runs test files through Vite's `ssr` environment, as Node code. */
|
|
38
67
|
viteEnvironment: 'ssr',
|
|
68
|
+
/**
|
|
69
|
+
* Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
|
|
70
|
+
*
|
|
71
|
+
* @param global - The global object of the test file's context.
|
|
72
|
+
* @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
|
|
73
|
+
* @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
|
|
74
|
+
*/
|
|
39
75
|
setup(global, { webgpuNode = {} }) {
|
|
40
|
-
const
|
|
76
|
+
const dom = domShims();
|
|
77
|
+
// oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals
|
|
78
|
+
const shims = { ...globals, ...dom.shims };
|
|
79
|
+
const devices = new Set();
|
|
80
|
+
let disposed = false;
|
|
81
|
+
const dispose = () => {
|
|
82
|
+
disposed = true;
|
|
83
|
+
dom.dispose();
|
|
84
|
+
for (const device of devices)
|
|
85
|
+
device.destroy();
|
|
86
|
+
devices.clear();
|
|
87
|
+
};
|
|
41
88
|
const modified = [];
|
|
42
89
|
try {
|
|
43
90
|
for (const key of Object.keys(shims)) {
|
|
@@ -56,20 +103,57 @@ export default {
|
|
|
56
103
|
}
|
|
57
104
|
const navigator = global.navigator;
|
|
58
105
|
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
106
|
+
const gpu = create(webgpuNode.dawnOptions ?? []);
|
|
107
|
+
const requestAdapter = gpu.requestAdapter.bind(gpu);
|
|
108
|
+
gpu.requestAdapter = async (options) => {
|
|
109
|
+
if (disposed)
|
|
110
|
+
throw new Error('WebGPU environment has been torn down');
|
|
111
|
+
const adapter = await requestAdapter(options);
|
|
112
|
+
if (adapter) {
|
|
113
|
+
const requestDevice = adapter.requestDevice.bind(adapter);
|
|
114
|
+
adapter.requestDevice = async (descriptor) => {
|
|
115
|
+
if (disposed)
|
|
116
|
+
throw new Error('WebGPU environment has been torn down');
|
|
117
|
+
const device = await requestDevice(descriptor);
|
|
118
|
+
if (disposed) {
|
|
119
|
+
device.destroy();
|
|
120
|
+
throw new Error('WebGPU environment has been torn down');
|
|
121
|
+
}
|
|
122
|
+
devices.add(device);
|
|
123
|
+
return device;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return adapter;
|
|
127
|
+
};
|
|
59
128
|
Object.defineProperty(navigator, 'gpu', {
|
|
60
|
-
value:
|
|
129
|
+
value: gpu,
|
|
61
130
|
writable: false,
|
|
62
131
|
enumerable: false,
|
|
63
132
|
configurable: true,
|
|
64
133
|
});
|
|
65
134
|
}
|
|
66
135
|
catch (error) {
|
|
136
|
+
dispose();
|
|
67
137
|
restore(modified);
|
|
68
138
|
throw error;
|
|
69
139
|
}
|
|
70
140
|
return {
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
|
|
143
|
+
* removes the globals that `setup` added.
|
|
144
|
+
*
|
|
145
|
+
* @param _global - Unused; teardown restores the global object it captured in `setup`.
|
|
146
|
+
*/
|
|
147
|
+
teardown: (_global) => {
|
|
148
|
+
if (disposed)
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
dispose();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
restore(modified);
|
|
155
|
+
modified.length = 0;
|
|
156
|
+
}
|
|
73
157
|
},
|
|
74
158
|
};
|
|
75
159
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","
|
|
1
|
+
{"version":3,"file":"index.js","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": "3.0.
|
|
4
|
-
"description": "Vitest environment
|
|
3
|
+
"version": "3.0.2",
|
|
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": {
|
|
@@ -42,7 +50,10 @@
|
|
|
42
50
|
"access": "public"
|
|
43
51
|
},
|
|
44
52
|
"dependencies": {
|
|
45
|
-
"webgpu": "^0.6.
|
|
53
|
+
"webgpu": "^0.6.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "7.1.0-beta"
|
|
46
57
|
},
|
|
47
58
|
"peerDependencies": {
|
|
48
59
|
"typescript": ">=6",
|
|
@@ -57,7 +68,7 @@
|
|
|
57
68
|
"node": ">=22"
|
|
58
69
|
},
|
|
59
70
|
"scripts": {
|
|
60
|
-
"build": "tsc",
|
|
61
|
-
"tsc": "tsc"
|
|
71
|
+
"build": "tsc -p tsconfig.build.json",
|
|
72
|
+
"tsc": "tsc --noEmit"
|
|
62
73
|
}
|
|
63
74
|
}
|
package/src/canvas.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
|
|
2
|
-
export type RgbaImage = {
|
|
2
|
+
export type RgbaImage = {
|
|
3
|
+
/** Width in pixels. */
|
|
4
|
+
width: number;
|
|
5
|
+
/** Height in pixels. */
|
|
6
|
+
height: number;
|
|
7
|
+
/** RGBA8 bytes, four per pixel, row by row from the top. */
|
|
8
|
+
data: Uint8Array;
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
type DefaultCanvasElement = typeof globalThis extends {
|
|
5
12
|
HTMLCanvasElement: { prototype: infer TElement };
|
|
@@ -23,22 +30,41 @@ export class HeadlessCanvasContext {
|
|
|
23
30
|
#config: GPUCanvasConfiguration | undefined;
|
|
24
31
|
#texture: GPUTexture | undefined;
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Creates the context for a canvas. Canvases create their own; you rarely call this.
|
|
35
|
+
*
|
|
36
|
+
* @param canvas - The canvas whose size this context's texture follows.
|
|
37
|
+
*/
|
|
38
|
+
constructor(
|
|
39
|
+
/** The canvas this context belongs to. */
|
|
40
|
+
readonly canvas: HeadlessCanvas,
|
|
41
|
+
) {}
|
|
27
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
|
|
45
|
+
*
|
|
46
|
+
* @param config - Device, format and usage for the canvas texture.
|
|
47
|
+
*/
|
|
28
48
|
configure(config: GPUCanvasConfiguration): void {
|
|
29
49
|
this.#config = config;
|
|
30
50
|
this.#drop();
|
|
31
51
|
}
|
|
32
52
|
|
|
53
|
+
/** Clears the configuration and discards the current texture. */
|
|
33
54
|
unconfigure(): void {
|
|
34
55
|
this.#config = undefined;
|
|
35
56
|
this.#drop();
|
|
36
57
|
}
|
|
37
58
|
|
|
59
|
+
/** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
38
60
|
getConfiguration(): GPUCanvasConfiguration | null {
|
|
39
61
|
return this.#config ?? null;
|
|
40
62
|
}
|
|
41
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
|
|
66
|
+
* is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
|
|
67
|
+
*/
|
|
42
68
|
getCurrentTexture(): GPUTexture {
|
|
43
69
|
const config = this.#config;
|
|
44
70
|
if (!config) throw new Error('getCurrentTexture() called before configure()');
|
|
@@ -100,54 +126,118 @@ export class HeadlessCanvasContext {
|
|
|
100
126
|
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
101
127
|
export class HeadlessCanvas {
|
|
102
128
|
readonly #context = new HeadlessCanvasContext(this);
|
|
129
|
+
/** A plain object libraries can write CSS properties to; it has no effect on rendering. */
|
|
103
130
|
readonly style: Record<string, string> = {};
|
|
104
131
|
readonly #attributes = new Map<string, string>();
|
|
105
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
|
|
135
|
+
*
|
|
136
|
+
* @param width - Canvas width in pixels.
|
|
137
|
+
* @param height - Canvas height in pixels.
|
|
138
|
+
*/
|
|
106
139
|
constructor(
|
|
140
|
+
/** Canvas width in pixels. */
|
|
107
141
|
public width = 300,
|
|
142
|
+
/** Canvas height in pixels. */
|
|
108
143
|
public height = 150,
|
|
109
144
|
) {}
|
|
110
145
|
|
|
146
|
+
/** Same as `width`; there is no layout in Node. */
|
|
111
147
|
get clientWidth(): number {
|
|
112
148
|
return this.width;
|
|
113
149
|
}
|
|
114
150
|
|
|
151
|
+
/** Same as `height`; there is no layout in Node. */
|
|
115
152
|
get clientHeight(): number {
|
|
116
153
|
return this.height;
|
|
117
154
|
}
|
|
118
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
|
|
158
|
+
*
|
|
159
|
+
* @param id - The context type. Only `'webgpu'` is supported.
|
|
160
|
+
* @returns The context for `'webgpu'`, or `null` for any other type.
|
|
161
|
+
*/
|
|
119
162
|
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
163
|
+
/**
|
|
164
|
+
* Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
|
|
165
|
+
*
|
|
166
|
+
* @param id - The context type. Only `'webgpu'` is supported.
|
|
167
|
+
* @returns The context for `'webgpu'`, or `null` for any other type.
|
|
168
|
+
*/
|
|
120
169
|
getContext(id: string): HeadlessCanvasContext | null;
|
|
121
170
|
getContext(id: string): HeadlessCanvasContext | null {
|
|
122
171
|
return id === 'webgpu' ? this.#context : null;
|
|
123
172
|
}
|
|
124
173
|
|
|
174
|
+
/** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
|
|
125
175
|
readPixels(): Promise<RgbaImage> {
|
|
126
176
|
return this.#context.readPixels();
|
|
127
177
|
}
|
|
128
178
|
|
|
129
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* This same object, cast to the element type a rendering library expects.
|
|
181
|
+
*
|
|
182
|
+
* @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
|
|
183
|
+
*/
|
|
130
184
|
asElement<TElement = DefaultCanvasElement>(): TElement {
|
|
131
185
|
return this as unknown as TElement;
|
|
132
186
|
}
|
|
133
187
|
|
|
134
|
-
|
|
188
|
+
/** A rectangle at the origin with the canvas's width and height. */
|
|
189
|
+
getBoundingClientRect(): {
|
|
190
|
+
/** Always 0. */
|
|
191
|
+
x: number;
|
|
192
|
+
/** Always 0. */
|
|
193
|
+
y: number;
|
|
194
|
+
/** The canvas width. */
|
|
195
|
+
width: number;
|
|
196
|
+
/** The canvas height. */
|
|
197
|
+
height: number;
|
|
198
|
+
/** Always 0. */
|
|
199
|
+
top: number;
|
|
200
|
+
/** Always 0. */
|
|
201
|
+
left: number;
|
|
202
|
+
} {
|
|
135
203
|
return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
|
|
136
204
|
}
|
|
137
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Stores an attribute on the canvas.
|
|
208
|
+
*
|
|
209
|
+
* @param name - Attribute name.
|
|
210
|
+
* @param value - Attribute value.
|
|
211
|
+
*/
|
|
138
212
|
setAttribute(name: string, value: string): void {
|
|
139
213
|
this.#attributes.set(name, value);
|
|
140
214
|
}
|
|
141
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Reads an attribute stored with `setAttribute()`.
|
|
218
|
+
*
|
|
219
|
+
* @param name - Attribute name.
|
|
220
|
+
* @returns The stored value, or `null` if none was set.
|
|
221
|
+
*/
|
|
142
222
|
getAttribute(name: string): string | null {
|
|
143
223
|
return this.#attributes.get(name) ?? null;
|
|
144
224
|
}
|
|
145
225
|
|
|
226
|
+
/** Does nothing; present so libraries that register listeners keep working. */
|
|
146
227
|
addEventListener(): void {}
|
|
228
|
+
/** Does nothing; present so libraries that remove listeners keep working. */
|
|
147
229
|
removeEventListener(): void {}
|
|
230
|
+
/** Does nothing and returns `true`. */
|
|
148
231
|
dispatchEvent(): boolean {
|
|
149
232
|
return true;
|
|
150
233
|
}
|
|
151
234
|
}
|
|
152
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Creates a headless canvas for WebGPU tests.
|
|
238
|
+
*
|
|
239
|
+
* @param width - Canvas width in pixels.
|
|
240
|
+
* @param height - Canvas height in pixels.
|
|
241
|
+
* @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
|
|
242
|
+
*/
|
|
153
243
|
export const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);
|
package/src/index.ts
CHANGED
|
@@ -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 };
|
|
@@ -46,11 +64,35 @@ const restore = (properties: SavedProperty[]): void => {
|
|
|
46
64
|
}
|
|
47
65
|
};
|
|
48
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
|
|
69
|
+
* classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
|
|
70
|
+
* on the global object (only where missing), and it removes them afterwards.
|
|
71
|
+
*/
|
|
49
72
|
export default {
|
|
73
|
+
/** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
|
|
50
74
|
name: 'webgpu-node',
|
|
75
|
+
/** Runs test files through Vite's `ssr` environment, as Node code. */
|
|
51
76
|
viteEnvironment: 'ssr',
|
|
77
|
+
/**
|
|
78
|
+
* Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
|
|
79
|
+
*
|
|
80
|
+
* @param global - The global object of the test file's context.
|
|
81
|
+
* @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
|
|
82
|
+
* @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
|
|
83
|
+
*/
|
|
52
84
|
setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
|
|
53
|
-
const
|
|
85
|
+
const dom = domShims();
|
|
86
|
+
// oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals
|
|
87
|
+
const shims = { ...globals, ...dom.shims } as Record<string, unknown>;
|
|
88
|
+
const devices = new Set<GPUDevice>();
|
|
89
|
+
let disposed = false;
|
|
90
|
+
const dispose = () => {
|
|
91
|
+
disposed = true;
|
|
92
|
+
dom.dispose();
|
|
93
|
+
for (const device of devices) device.destroy();
|
|
94
|
+
devices.clear();
|
|
95
|
+
};
|
|
54
96
|
const modified: SavedProperty[] = [];
|
|
55
97
|
try {
|
|
56
98
|
for (const key of Object.keys(shims)) {
|
|
@@ -68,19 +110,52 @@ export default {
|
|
|
68
110
|
}
|
|
69
111
|
const navigator = global.navigator as object;
|
|
70
112
|
modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
|
|
113
|
+
const gpu = create(webgpuNode.dawnOptions ?? []);
|
|
114
|
+
const requestAdapter = gpu.requestAdapter.bind(gpu);
|
|
115
|
+
gpu.requestAdapter = async (options) => {
|
|
116
|
+
if (disposed) throw new Error('WebGPU environment has been torn down');
|
|
117
|
+
const adapter = await requestAdapter(options);
|
|
118
|
+
if (adapter) {
|
|
119
|
+
const requestDevice = adapter.requestDevice.bind(adapter);
|
|
120
|
+
adapter.requestDevice = async (descriptor) => {
|
|
121
|
+
if (disposed) throw new Error('WebGPU environment has been torn down');
|
|
122
|
+
const device = await requestDevice(descriptor);
|
|
123
|
+
if (disposed) {
|
|
124
|
+
device.destroy();
|
|
125
|
+
throw new Error('WebGPU environment has been torn down');
|
|
126
|
+
}
|
|
127
|
+
devices.add(device);
|
|
128
|
+
return device;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return adapter;
|
|
132
|
+
};
|
|
71
133
|
Object.defineProperty(navigator, 'gpu', {
|
|
72
|
-
value:
|
|
134
|
+
value: gpu,
|
|
73
135
|
writable: false,
|
|
74
136
|
enumerable: false,
|
|
75
137
|
configurable: true,
|
|
76
138
|
});
|
|
77
139
|
} catch (error) {
|
|
140
|
+
dispose();
|
|
78
141
|
restore(modified);
|
|
79
142
|
throw error;
|
|
80
143
|
}
|
|
81
144
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
|
|
147
|
+
* removes the globals that `setup` added.
|
|
148
|
+
*
|
|
149
|
+
* @param _global - Unused; teardown restores the global object it captured in `setup`.
|
|
150
|
+
*/
|
|
151
|
+
teardown: (_global?: Record<string, unknown>) => {
|
|
152
|
+
if (disposed) return;
|
|
153
|
+
try {
|
|
154
|
+
dispose();
|
|
155
|
+
} finally {
|
|
156
|
+
restore(modified);
|
|
157
|
+
modified.length = 0;
|
|
158
|
+
}
|
|
84
159
|
},
|
|
85
160
|
};
|
|
86
161
|
},
|