wgpu-kit 1.0.3 → 1.1.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.cn.md +3 -3
- package/README.md +56 -16
- package/dist/core/buffer.js +17 -5
- package/dist/core/codes.d.ts +35 -0
- package/dist/core/codes.js +34 -0
- package/dist/core/context.js +1 -1
- package/dist/core/errors.d.ts +18 -6
- package/dist/core/errors.js +28 -13
- package/dist/core/kernel.js +14 -11
- package/dist/core/layout.js +7 -6
- package/dist/core/pingpong.js +1 -1
- package/dist/core/raw.js +3 -3
- package/dist/media.js +3 -3
- package/dist/observe.js +2 -2
- package/dist/packs/grid/index.d.ts +39 -0
- package/dist/packs/grid/index.js +187 -0
- package/dist/packs/image/index.js +1 -1
- package/dist/packs/life/boids.js +7 -103
- package/dist/packs/particles/config.d.ts +2 -1
- package/dist/packs/particles/config.js +5 -5
- package/dist/packs/particles/grid.js +35 -46
- package/dist/packs/particles/index.js +23 -22
- package/dist/packs/particles/presets.js +1 -1
- package/package.json +30 -11
package/README.cn.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wgpu-kit
|
|
2
2
|
|
|
3
|
-
> 浏览器创意编程 GPU 工具包:20 万粒子物理 120fps,只需 5 行代码。
|
|
3
|
+
> 浏览器创意编程 GPU 工具包:20 万粒子物理 120fps,只需 5 行代码。(所有 fps 均为**可见帧**)
|
|
4
4
|
> WebGPU 计算的全套样板——设备、缓冲、管线、dispatch、双缓冲、读回、错误行号映射——打包成两层简单 API。
|
|
5
5
|
|
|
6
6
|
[](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [](https://www.npmjs.com/package/wgpu-kit) [](LICENSE)
|
|
@@ -70,8 +70,8 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
|
|
|
70
70
|
|
|
71
71
|
| 指标 | 数值 | 环境 |
|
|
72
72
|
| --- | --- | --- |
|
|
73
|
-
| 粒子端到端 | 200,000 @ 122fps
|
|
74
|
-
| 粒子计算(grid) | 16k→262k 平坦,3.0→
|
|
73
|
+
| 粒子端到端 | 200,000 @ 122fps · 66,000 @ 144fps(**可见帧**)| RTX 4060 Laptop,playground 实测 |
|
|
74
|
+
| 粒子计算(grid) | 16k→262k 平坦,3.0→4.4ms/帧 | headless 基准,GPU 42°C |
|
|
75
75
|
| 邻域算法 | grid 近似 O(N),66k 时比暴力快 8.5× | 同会话 A/B |
|
|
76
76
|
| 库体积 | core gzip ~10kB(共享上下文构建) | gzip |
|
|
77
77
|
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# wgpu-kit
|
|
2
2
|
|
|
3
|
-
**Browser GPGPU middle layer. 200,000 particles at 142fps — in 5 lines of code.**
|
|
4
|
-
All the WebGPU boilerplate — device, buffers, pipelines, dispatch, readbacks — wrapped into two simple API layers.
|
|
5
|
-
|
|
6
3
|
[](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [](https://www.npmjs.com/package/wgpu-kit) [](LICENSE)
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
**Creative-coding GPU toolkit for the browser. 200,000-particle physics at 120fps — in 5 lines of code.**
|
|
6
|
+
All the WebGPU boilerplate — device, buffers, pipelines, dispatch, double
|
|
7
|
+
buffering, readbacks, error line-mapping — wrapped into two simple API layers.
|
|
8
|
+
|
|
9
|
+
[**API Reference**](docs/API.md) · [中文文档](README.cn.md) · **[LIVE DEMO](https://nanfengw0w.github.io/wgpu-kit/)**
|
|
9
10
|
|
|
10
11
|

|
|
11
12
|
|
|
@@ -18,7 +19,7 @@ npm i wgpu-kit
|
|
|
18
19
|
**100,000 particles in 5 lines:**
|
|
19
20
|
|
|
20
21
|
```ts
|
|
21
|
-
import { particles } from 'wgpu-kit';
|
|
22
|
+
import { particles } from 'wgpu-kit';
|
|
22
23
|
|
|
23
24
|
const sim = await particles({ count: 100_000, forces: 'cells' });
|
|
24
25
|
await sim.attach(canvas);
|
|
@@ -50,6 +51,42 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
|
|
|
50
51
|
Device management, buffer sizing, pipeline creation, double buffering,
|
|
51
52
|
dispatch, readbacks, error line-mapping — all handled by the library.
|
|
52
53
|
|
|
54
|
+
### The same thing, with raw WebGPU
|
|
55
|
+
|
|
56
|
+
For honesty: here is the **heavily condensed** native equivalent (full
|
|
57
|
+
version is ~150 lines; this excerpt omits error handling, resize, double
|
|
58
|
+
buffering, staging readbacks and the render pipeline):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
62
|
+
const device = await adapter.requestDevice();
|
|
63
|
+
|
|
64
|
+
// buffers — sizes and usages hand-computed
|
|
65
|
+
const pos = device.createBuffer({ size: 100_000 * 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
66
|
+
const vel = device.createBuffer({ size: 100_000 * 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
67
|
+
|
|
68
|
+
// hand-written WGSL, uniform struct aligned to 16 bytes by hand
|
|
69
|
+
const module = device.createShaderModule({ code: `...struct Params {...}...` });
|
|
70
|
+
|
|
71
|
+
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
72
|
+
const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [/* every binding, exact order */] });
|
|
73
|
+
|
|
74
|
+
function tick(dt) {
|
|
75
|
+
const enc = device.createCommandEncoder();
|
|
76
|
+
const pass = enc.beginComputePass();
|
|
77
|
+
pass.setPipeline(pipeline);
|
|
78
|
+
pass.setBindGroup(0, bindGroup);
|
|
79
|
+
pass.dispatchWorkgroups(Math.ceil(100_000 / 64));
|
|
80
|
+
pass.end();
|
|
81
|
+
device.queue.submit([enc.finish()]);
|
|
82
|
+
}
|
|
83
|
+
// …plus: staging readbacks, device-lost handling, WGSL compile diagnostics,
|
|
84
|
+
// canvas resize — and a render pipeline before anything is visible.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With `wgpu-kit`, the kernel is the only code you write — and when WGSL fails
|
|
88
|
+
to compile, the error points at **your line**.
|
|
89
|
+
|
|
53
90
|
## Entry points
|
|
54
91
|
|
|
55
92
|
| import | purpose |
|
|
@@ -62,6 +99,7 @@ dispatch, readbacks, error line-mapping — all handled by the library.
|
|
|
62
99
|
| `wgpu-kit/react` | `<ParticleCanvas />` |
|
|
63
100
|
| `wgpu-kit/three` | three.js snapshot interop |
|
|
64
101
|
| `wgpu-kit/media` | canvas recording (webm/mp4) |
|
|
102
|
+
| `wgpu-kit/observe` | GPU timing / device diagnostics / canvas helpers |
|
|
65
103
|
| `wgpu-kit/vite` | WGSL kernel hot reload |
|
|
66
104
|
|
|
67
105
|

|
|
@@ -70,19 +108,14 @@ dispatch, readbacks, error line-mapping — all handled by the library.
|
|
|
70
108
|
|
|
71
109
|
## Numbers (reproducible)
|
|
72
110
|
|
|
111
|
+
All fps numbers are **visible frames** — every `tick()` renders fresh state.
|
|
112
|
+
|
|
73
113
|
| metric | value | environment |
|
|
74
114
|
| --- | --- | --- |
|
|
75
|
-
| particles end-to-end | 200,000 @
|
|
76
|
-
| particle compute (grid) |
|
|
77
|
-
| neighborhood algorithms | grid ~O(N)
|
|
78
|
-
| bundle size | core gzip
|
|
79
|
-
|
|
80
|
-
## Verification
|
|
81
|
-
|
|
82
|
-
41+ automated probes run on a real GPU via a headless Chromium harness
|
|
83
|
-
(included under `tests/` + `scripts/verify.mjs`) — including a physics
|
|
84
|
-
equivalence regression that fails if the neighborhood algorithms ever
|
|
85
|
-
produce divergent structures.
|
|
115
|
+
| particles end-to-end | 200,000 @ 122fps · 66,000 @ 144fps | RTX 4060 Laptop, playground |
|
|
116
|
+
| particle compute (grid) | 16k→262k flat, 3.0→4.4ms/frame | reproducible via `npm run bench` → docs/BENCHMARK.md |
|
|
117
|
+
| neighborhood algorithms | grid ~O(N), 8.5× faster than brute force at 66k | same-session A/B |
|
|
118
|
+
| bundle size | core gzip ~10kB (all entries share one context) | measured by `npm run build` |
|
|
86
119
|
|
|
87
120
|
## Three design rules
|
|
88
121
|
|
|
@@ -92,6 +125,13 @@ produce divergent structures.
|
|
|
92
125
|
3. **Benchmarks are documentation** — every published number is reproducible;
|
|
93
126
|
gzip budgets are enforced by `npm run build`.
|
|
94
127
|
|
|
128
|
+
## Verification
|
|
129
|
+
|
|
130
|
+
41+ automated probes run on a real GPU via a headless Chromium harness
|
|
131
|
+
(included: `tests/` + `scripts/verify.mjs`) — including a **physics
|
|
132
|
+
equivalence regression** that fails the build if the neighborhood algorithms
|
|
133
|
+
(n2 / tiled / grid) ever produce divergent structures.
|
|
134
|
+
|
|
95
135
|
## Support matrix
|
|
96
136
|
|
|
97
137
|
| browser | status |
|
package/dist/core/buffer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TYPES } from "./layout.js";
|
|
2
2
|
import { GpuContext } from "./context.js";
|
|
3
|
-
import { UsageError } from "./errors.js";
|
|
3
|
+
import { ERR, UsageError } from "./errors.js";
|
|
4
4
|
const TYPED_CTORS = {
|
|
5
5
|
Float32Array, Int32Array, Uint32Array,
|
|
6
6
|
};
|
|
@@ -16,6 +16,7 @@ export class Buffer {
|
|
|
16
16
|
#ctx;
|
|
17
17
|
#byteLength;
|
|
18
18
|
#stride;
|
|
19
|
+
#reading = null;
|
|
19
20
|
#staging = null;
|
|
20
21
|
constructor(ctx, kind, length, gpuBuffer) {
|
|
21
22
|
this.#ctx = ctx;
|
|
@@ -27,11 +28,11 @@ export class Buffer {
|
|
|
27
28
|
}
|
|
28
29
|
static async create(kind, length) {
|
|
29
30
|
if (!Number.isInteger(length) || length <= 0) {
|
|
30
|
-
throw new UsageError(`Buffer
|
|
31
|
+
throw new UsageError(ERR.BUFFER_CREATE, `Buffer length must be a positive integer, got: ${String(length)}`);
|
|
31
32
|
}
|
|
32
33
|
const def = TYPES[kind];
|
|
33
34
|
if (!def)
|
|
34
|
-
throw new UsageError(
|
|
35
|
+
throw new UsageError(ERR.BUFFER_CREATE, `Unknown Buffer kind "${String(kind)}". Available: ${Object.keys(TYPES).join(", ")}`);
|
|
35
36
|
const ctx = await GpuContext.get();
|
|
36
37
|
const gpuBuffer = ctx.device.createBuffer({
|
|
37
38
|
size: length * def.stride,
|
|
@@ -45,11 +46,11 @@ export class Buffer {
|
|
|
45
46
|
const def = TYPES[this.kind];
|
|
46
47
|
const ctor = TYPED_CTORS[def.typed];
|
|
47
48
|
if (!(data instanceof ctor)) {
|
|
48
|
-
throw new UsageError(`Buffer<${this.kind}>.write
|
|
49
|
+
throw new UsageError(ERR.BUFFER_WRITE, `Buffer<${this.kind}>.write expects ${def.typed}, got ${data.constructor?.name ?? typeof data}`);
|
|
49
50
|
}
|
|
50
51
|
const expected = this.length * def.comps;
|
|
51
52
|
if (data.length !== expected) {
|
|
52
|
-
throw new UsageError(`Buffer<${this.kind}>[${this.length}].write
|
|
53
|
+
throw new UsageError(ERR.BUFFER_WRITE, `Buffer<${this.kind}>[${this.length}].write expects ${expected} components, got ${data.length}`);
|
|
53
54
|
}
|
|
54
55
|
if (def.stride === def.size || def.comps === 1) {
|
|
55
56
|
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, data);
|
|
@@ -67,6 +68,17 @@ export class Buffer {
|
|
|
67
68
|
}
|
|
68
69
|
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
69
70
|
async read() {
|
|
71
|
+
if (this.#reading)
|
|
72
|
+
return this.#reading;
|
|
73
|
+
this.#reading = this.#doRead();
|
|
74
|
+
try {
|
|
75
|
+
return await this.#reading;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.#reading = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async #doRead() {
|
|
70
82
|
const def = TYPES[this.kind];
|
|
71
83
|
if (!this.#staging) {
|
|
72
84
|
this.#staging = this.#ctx.device.createBuffer({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for programmatic identification of wgpu-kit errors.
|
|
3
|
+
* These are stable string constants — safe to switch on in user code.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ERR: {
|
|
6
|
+
/** WebGPU is unavailable or the adapter could not be acquired */
|
|
7
|
+
readonly WGPU_UNAVAILABLE: "ERR_WGPU_UNAVAILABLE";
|
|
8
|
+
/** WGSL compilation failed */
|
|
9
|
+
readonly COMPILE: "ERR_COMPILE";
|
|
10
|
+
/** Invalid argument passed by the caller */
|
|
11
|
+
readonly USAGE: "ERR_USAGE";
|
|
12
|
+
/** A required uniform field is missing or has an invalid value */
|
|
13
|
+
readonly UNIFORM_FIELD: "ERR_UNIFORM_FIELD";
|
|
14
|
+
/** Uniform scalar types are not yet supported */
|
|
15
|
+
readonly UNIFORM_UNSUPPORTED: "ERR_UNIFORM_UNSUPPORTED";
|
|
16
|
+
/** workgroupSize is outside the valid range */
|
|
17
|
+
readonly WORKGROUP_SIZE: "ERR_WORKGROUP_SIZE";
|
|
18
|
+
/** A resource (Buffer) is missing from the resources map */
|
|
19
|
+
readonly RESOURCE_MISSING: "ERR_RESOURCE_MISSING";
|
|
20
|
+
/** A resource type does not match the kernel declaration */
|
|
21
|
+
readonly RESOURCE_TYPE: "ERR_RESOURCE_TYPE";
|
|
22
|
+
/** Resource lengths are inconsistent across state/inputs */
|
|
23
|
+
readonly RESOURCE_LENGTH: "ERR_RESOURCE_LENGTH";
|
|
24
|
+
/** Buffer.create received an invalid kind or length */
|
|
25
|
+
readonly BUFFER_CREATE: "ERR_BUFFER_CREATE";
|
|
26
|
+
/** Buffer.write type or component count mismatch */
|
|
27
|
+
readonly BUFFER_WRITE: "ERR_BUFFER_WRITE";
|
|
28
|
+
/** MediaRecorder is unavailable or the recording failed */
|
|
29
|
+
readonly MEDIA: "ERR_MEDIA";
|
|
30
|
+
/** timestamp-query is not supported on this device */
|
|
31
|
+
readonly TIMESTAMP_UNSUPPORTED: "ERR_TIMESTAMP_UNSUPPORTED";
|
|
32
|
+
/** A generic library error that doesn't fit any specific code */
|
|
33
|
+
readonly GENERIC: "ERR_GENERIC";
|
|
34
|
+
};
|
|
35
|
+
export type ErrorCode = (typeof ERR)[keyof typeof ERR];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for programmatic identification of wgpu-kit errors.
|
|
3
|
+
* These are stable string constants — safe to switch on in user code.
|
|
4
|
+
*/
|
|
5
|
+
export const ERR = {
|
|
6
|
+
/** WebGPU is unavailable or the adapter could not be acquired */
|
|
7
|
+
WGPU_UNAVAILABLE: 'ERR_WGPU_UNAVAILABLE',
|
|
8
|
+
/** WGSL compilation failed */
|
|
9
|
+
COMPILE: 'ERR_COMPILE',
|
|
10
|
+
/** Invalid argument passed by the caller */
|
|
11
|
+
USAGE: 'ERR_USAGE',
|
|
12
|
+
/** A required uniform field is missing or has an invalid value */
|
|
13
|
+
UNIFORM_FIELD: 'ERR_UNIFORM_FIELD',
|
|
14
|
+
/** Uniform scalar types are not yet supported */
|
|
15
|
+
UNIFORM_UNSUPPORTED: 'ERR_UNIFORM_UNSUPPORTED',
|
|
16
|
+
/** workgroupSize is outside the valid range */
|
|
17
|
+
WORKGROUP_SIZE: 'ERR_WORKGROUP_SIZE',
|
|
18
|
+
/** A resource (Buffer) is missing from the resources map */
|
|
19
|
+
RESOURCE_MISSING: 'ERR_RESOURCE_MISSING',
|
|
20
|
+
/** A resource type does not match the kernel declaration */
|
|
21
|
+
RESOURCE_TYPE: 'ERR_RESOURCE_TYPE',
|
|
22
|
+
/** Resource lengths are inconsistent across state/inputs */
|
|
23
|
+
RESOURCE_LENGTH: 'ERR_RESOURCE_LENGTH',
|
|
24
|
+
/** Buffer.create received an invalid kind or length */
|
|
25
|
+
BUFFER_CREATE: 'ERR_BUFFER_CREATE',
|
|
26
|
+
/** Buffer.write type or component count mismatch */
|
|
27
|
+
BUFFER_WRITE: 'ERR_BUFFER_WRITE',
|
|
28
|
+
/** MediaRecorder is unavailable or the recording failed */
|
|
29
|
+
MEDIA: 'ERR_MEDIA',
|
|
30
|
+
/** timestamp-query is not supported on this device */
|
|
31
|
+
TIMESTAMP_UNSUPPORTED: 'ERR_TIMESTAMP_UNSUPPORTED',
|
|
32
|
+
/** A generic library error that doesn't fit any specific code */
|
|
33
|
+
GENERIC: 'ERR_GENERIC',
|
|
34
|
+
};
|
package/dist/core/context.js
CHANGED
|
@@ -26,7 +26,7 @@ export class GpuContext {
|
|
|
26
26
|
}
|
|
27
27
|
static async #create() {
|
|
28
28
|
if (typeof navigator === 'undefined' || !('gpu' in navigator) || !navigator.gpu) {
|
|
29
|
-
throw new WebGPUUnavailableError('navigator.gpu
|
|
29
|
+
throw new WebGPUUnavailableError('navigator.gpu is not available');
|
|
30
30
|
}
|
|
31
31
|
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
32
32
|
if (!adapter)
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ErrorCode } from './codes.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Error hierarchy for wgpu-kit. All errors extend WgpuKitError and carry a
|
|
4
|
+
* stable machine-readable `code` for programmatic handling.
|
|
5
|
+
*/
|
|
2
6
|
export declare class WgpuKitError extends Error {
|
|
3
|
-
|
|
7
|
+
/** Stable error code, e.g. 'ERR_WGPU_UNAVAILABLE' — safe to switch on. */
|
|
8
|
+
readonly code: ErrorCode;
|
|
9
|
+
constructor(code: ErrorCode, message: string);
|
|
4
10
|
}
|
|
5
|
-
/**
|
|
11
|
+
/** WebGPU is unavailable or the adapter could not be acquired. */
|
|
6
12
|
export declare class WebGPUUnavailableError extends WgpuKitError {
|
|
7
13
|
constructor(reason: string);
|
|
8
14
|
}
|
|
9
|
-
/** WGSL
|
|
15
|
+
/** WGSL compilation failed. Line numbers are mapped back to user code. */
|
|
10
16
|
export declare class CompileError extends WgpuKitError {
|
|
11
17
|
constructor(kernelName: string, messages: readonly {
|
|
12
18
|
line: number;
|
|
13
19
|
msg: string;
|
|
14
20
|
}[], userCodeOffset: number);
|
|
15
21
|
}
|
|
16
|
-
/**
|
|
22
|
+
/** Caller passed an invalid argument (missing resource, type mismatch, bad length, etc.). */
|
|
17
23
|
export declare class UsageError extends WgpuKitError {
|
|
24
|
+
constructor(code: ErrorCode, message: string);
|
|
18
25
|
}
|
|
19
|
-
/**
|
|
26
|
+
/** Compute pipeline creation failed validation (e.g. too many storage buffers). */
|
|
27
|
+
export declare class PipelineError extends WgpuKitError {
|
|
28
|
+
constructor(label: string, detail: string);
|
|
29
|
+
}
|
|
30
|
+
export { ERR, type ErrorCode } from './codes.ts';
|
|
31
|
+
/** Create a compute pipeline with pushErrorScope to surface async validation errors. */
|
|
20
32
|
export declare function createComputePipelineChecked(device: GPUDevice, module: GPUShaderModule, label: string, entryPoint?: string): Promise<GPUComputePipeline>;
|
package/dist/core/errors.js
CHANGED
|
@@ -1,42 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { ERR } from "./codes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error hierarchy for wgpu-kit. All errors extend WgpuKitError and carry a
|
|
4
|
+
* stable machine-readable `code` for programmatic handling.
|
|
5
|
+
*/
|
|
2
6
|
export class WgpuKitError extends Error {
|
|
3
|
-
|
|
7
|
+
/** Stable error code, e.g. 'ERR_WGPU_UNAVAILABLE' — safe to switch on. */
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
4
10
|
super(message);
|
|
5
11
|
this.name = new.target.name;
|
|
12
|
+
this.code = code;
|
|
6
13
|
}
|
|
7
14
|
}
|
|
8
|
-
/**
|
|
15
|
+
/** WebGPU is unavailable or the adapter could not be acquired. */
|
|
9
16
|
export class WebGPUUnavailableError extends WgpuKitError {
|
|
10
17
|
constructor(reason) {
|
|
11
|
-
super(
|
|
12
|
-
'
|
|
13
|
-
' 可用 navigator.gpu 是否存在快速判断。');
|
|
18
|
+
super(ERR.WGPU_UNAVAILABLE, `WebGPU is unavailable: ${reason}\n` +
|
|
19
|
+
' Check: 1) Use Chrome/Edge 113+ or Safari 18+. 2) Enable WebGPU in headless mode. 3) Verify GPU drivers and hardware acceleration.');
|
|
14
20
|
}
|
|
15
21
|
}
|
|
16
|
-
/** WGSL
|
|
22
|
+
/** WGSL compilation failed. Line numbers are mapped back to user code. */
|
|
17
23
|
export class CompileError extends WgpuKitError {
|
|
18
24
|
constructor(kernelName, messages, userCodeOffset) {
|
|
19
25
|
const mapped = messages
|
|
20
26
|
.map((m) => {
|
|
21
27
|
const userLine = m.line - userCodeOffset;
|
|
22
|
-
const where = userLine > 0 ?
|
|
28
|
+
const where = userLine > 0 ? `your code, line ${userLine}` : `generated code, line ${m.line} (library issue — please file an issue)`;
|
|
23
29
|
return ` ${where}: ${m.msg}`;
|
|
24
30
|
})
|
|
25
31
|
.join('\n');
|
|
26
|
-
super(`kernel "${kernelName}" WGSL
|
|
32
|
+
super(ERR.COMPILE, `kernel "${kernelName}" WGSL compilation failed:\n${mapped}`);
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
|
-
/**
|
|
35
|
+
/** Caller passed an invalid argument (missing resource, type mismatch, bad length, etc.). */
|
|
30
36
|
export class UsageError extends WgpuKitError {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(code, message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Compute pipeline creation failed validation (e.g. too many storage buffers). */
|
|
42
|
+
export class PipelineError extends WgpuKitError {
|
|
43
|
+
constructor(label, detail) {
|
|
44
|
+
super(ERR.GENERIC, `compute pipeline "${label}" creation failed: ${detail}`);
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
|
-
|
|
47
|
+
export { ERR } from "./codes.js";
|
|
48
|
+
/** Create a compute pipeline with pushErrorScope to surface async validation errors. */
|
|
33
49
|
export async function createComputePipelineChecked(device, module, label, entryPoint = 'main') {
|
|
34
50
|
device.pushErrorScope('validation');
|
|
35
51
|
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint } });
|
|
36
52
|
const err = await device.popErrorScope();
|
|
37
53
|
if (err) {
|
|
38
|
-
throw new
|
|
39
|
-
常见原因:storage buffer 数超过每阶段上限(可向本库提 issue 申请 limits 支持)`);
|
|
54
|
+
throw new PipelineError(label, err.message);
|
|
40
55
|
}
|
|
41
56
|
return pipeline;
|
|
42
57
|
}
|
package/dist/core/kernel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { planUniform, packUniformInto, TYPES } from "./layout.js";
|
|
2
2
|
import { GpuContext } from "./context.js";
|
|
3
3
|
import { Buffer } from "./buffer.js";
|
|
4
|
-
import { CompileError, UsageError } from "./errors.js";
|
|
4
|
+
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
5
5
|
const RESERVED = new Set(['count']);
|
|
6
6
|
/** GPUBuffer 的稳定数字身份(WeakMap 分配,用于 bind group 缓存键) */
|
|
7
7
|
let nextBufferId = 0;
|
|
@@ -19,24 +19,24 @@ export function generateElementKernel(spec) {
|
|
|
19
19
|
const name = spec.name ?? 'kernel';
|
|
20
20
|
const workgroupSize = spec.workgroupSize ?? 64;
|
|
21
21
|
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
22
|
-
throw new UsageError(`workgroupSize
|
|
22
|
+
throw new UsageError(ERR.WORKGROUP_SIZE, `workgroupSize must be in 1..512, got: ${String(workgroupSize)}`);
|
|
23
23
|
}
|
|
24
24
|
const state = Object.entries(spec.state ?? {});
|
|
25
25
|
const inputs = Object.entries(spec.inputs ?? {});
|
|
26
26
|
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
27
27
|
if (state.length + inputs.length === 0) {
|
|
28
|
-
throw new UsageError(`elementKernel "${name}"
|
|
28
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `elementKernel "${name}" requires at least one state or inputs field`);
|
|
29
29
|
}
|
|
30
30
|
for (const [uName] of uniforms) {
|
|
31
31
|
if (RESERVED.has(uName))
|
|
32
|
-
throw new UsageError(`uniform
|
|
32
|
+
throw new UsageError(ERR.USAGE, `uniform name "${uName}" is reserved (count is auto-injected by the library)`);
|
|
33
33
|
}
|
|
34
34
|
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
35
35
|
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
36
|
-
throw new UsageError(`elementKernel "${name}"
|
|
36
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" has duplicate field names across state/inputs/uniforms`);
|
|
37
37
|
}
|
|
38
38
|
if (typeof spec.code !== 'string' || spec.code.trim().length === 0) {
|
|
39
|
-
throw new UsageError(`elementKernel "${name}"
|
|
39
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" is missing code (user WGSL function)`);
|
|
40
40
|
}
|
|
41
41
|
const uniformEntries = [...uniforms, ['count', 'u32']];
|
|
42
42
|
const uniformLayout = planUniform(uniformEntries);
|
|
@@ -143,17 +143,17 @@ export function elementKernel(spec) {
|
|
|
143
143
|
const f = orderedFields[i];
|
|
144
144
|
const buf = resources[f.key];
|
|
145
145
|
if (!buf)
|
|
146
|
-
throw new UsageError(`kernel "${normalized.name}".run
|
|
146
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `kernel "${normalized.name}".run is missing resource "${f.key}"`);
|
|
147
147
|
const want = expectedKinds.get(f.key);
|
|
148
148
|
if (buf.kind !== want) {
|
|
149
|
-
throw new UsageError(
|
|
149
|
+
throw new UsageError(ERR.RESOURCE_TYPE, `Resource "${f.key}" type mismatch: expected ${want}, got ${buf.kind}`);
|
|
150
150
|
}
|
|
151
151
|
if (count === -1) {
|
|
152
152
|
count = buf.length;
|
|
153
153
|
firstKey = f.key;
|
|
154
154
|
}
|
|
155
155
|
else if (buf.length !== count) {
|
|
156
|
-
throw new UsageError(
|
|
156
|
+
throw new UsageError(ERR.RESOURCE_LENGTH, `Resource "${f.key}" length ${buf.length} does not match "${firstKey}" length ${count}`);
|
|
157
157
|
}
|
|
158
158
|
ordered.push(buf);
|
|
159
159
|
}
|
|
@@ -171,12 +171,15 @@ export function elementKernel(spec) {
|
|
|
171
171
|
let cacheKey = 0;
|
|
172
172
|
for (let i = 0; i < ordered.length; i++)
|
|
173
173
|
cacheKey = (cacheKey * 31 + bufId(ordered[i].gpuBuffer)) | 0;
|
|
174
|
-
|
|
174
|
+
const ids = ordered.map((b) => bufId(b.gpuBuffer));
|
|
175
|
+
const cached = bindGroupCache.get(cacheKey);
|
|
176
|
+
const identityMatch = cached && cached.ids.length === ids.length && cached.ids.every((id, idx) => id === ids[idx]);
|
|
177
|
+
let bg = identityMatch ? cached.bg : undefined;
|
|
175
178
|
if (!bg) {
|
|
176
179
|
const entries = [{ binding: 0, resource: { buffer: uniformBuffer } }];
|
|
177
180
|
ordered.forEach((buffer, i) => entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } }));
|
|
178
181
|
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
179
|
-
bindGroupCache.set(cacheKey, bg);
|
|
182
|
+
bindGroupCache.set(cacheKey, { bg, ids });
|
|
180
183
|
}
|
|
181
184
|
// —— 编码提交 ——
|
|
182
185
|
const enc = device.createCommandEncoder();
|
package/dist/core/layout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ERR, UsageError } from "./errors.js";
|
|
1
2
|
export const TYPES = {
|
|
2
3
|
f32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Float32Array', wgsl: 'f32' },
|
|
3
4
|
i32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Int32Array', wgsl: 'i32' },
|
|
@@ -38,13 +39,13 @@ export function packUniform(layout, values) {
|
|
|
38
39
|
for (const f of layout.fields) {
|
|
39
40
|
const pack = PACKERS[f.kind];
|
|
40
41
|
if (!pack) {
|
|
41
|
-
throw new
|
|
42
|
+
throw new UsageError(ERR.UNIFORM_UNSUPPORTED, `Uniform field "${f.name}" has unsupported type "${f.kind}". Only scalars are currently supported.`);
|
|
42
43
|
}
|
|
43
44
|
const v = values[f.name];
|
|
44
45
|
if (v === undefined)
|
|
45
|
-
throw new
|
|
46
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Missing uniform value for "${f.name}"`);
|
|
46
47
|
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
47
|
-
throw new
|
|
48
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Uniform value for "${f.name}" must be a finite number, got: ${String(v)}`);
|
|
48
49
|
}
|
|
49
50
|
pack(view, f.offset, v);
|
|
50
51
|
}
|
|
@@ -56,13 +57,13 @@ export function packUniformInto(target, layout, values) {
|
|
|
56
57
|
for (const f of layout.fields) {
|
|
57
58
|
const pack = PACKERS[f.kind];
|
|
58
59
|
if (!pack) {
|
|
59
|
-
throw new
|
|
60
|
+
throw new UsageError(ERR.UNIFORM_UNSUPPORTED, `Uniform field "${f.name}" has unsupported type "${f.kind}". Only scalars are currently supported.`);
|
|
60
61
|
}
|
|
61
62
|
const v = values[f.name];
|
|
62
63
|
if (v === undefined)
|
|
63
|
-
throw new
|
|
64
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Missing uniform value for "${f.name}"`);
|
|
64
65
|
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
65
|
-
throw new
|
|
66
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Uniform value for "${f.name}" must be a finite number, got: ${String(v)}`);
|
|
66
67
|
}
|
|
67
68
|
pack(view, f.offset, v);
|
|
68
69
|
}
|
package/dist/core/pingpong.js
CHANGED
|
@@ -20,7 +20,7 @@ export class PingPong {
|
|
|
20
20
|
static async create(kinds, length) {
|
|
21
21
|
const names = Object.keys(kinds);
|
|
22
22
|
if (names.length === 0)
|
|
23
|
-
throw new Error('PingPong
|
|
23
|
+
throw new Error('PingPong requires at least one field');
|
|
24
24
|
const make = async () => {
|
|
25
25
|
const side = {};
|
|
26
26
|
for (const name of names)
|
package/dist/core/raw.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { GpuContext } from "./context.js";
|
|
2
|
-
import { CompileError, UsageError } from "./errors.js";
|
|
2
|
+
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
3
3
|
export function rawKernel(code, entryPoint = 'main', label = 'rawKernel') {
|
|
4
4
|
if (typeof code !== 'string' || code.trim().length === 0)
|
|
5
|
-
throw new UsageError('rawKernel
|
|
5
|
+
throw new UsageError(ERR.USAGE, 'rawKernel requires WGSL code');
|
|
6
6
|
let pipelinePromise = null;
|
|
7
7
|
return {
|
|
8
8
|
async run(entries, workgroups) {
|
|
9
9
|
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
10
|
-
throw new UsageError(`rawKernel.run
|
|
10
|
+
throw new UsageError(ERR.USAGE, `rawKernel.run workgroups must be a positive integer, got ${String(workgroups)}`);
|
|
11
11
|
}
|
|
12
12
|
const ctx = await GpuContext.get();
|
|
13
13
|
if (!pipelinePromise) {
|
package/dist/media.js
CHANGED
|
@@ -24,14 +24,14 @@ export class CanvasRecorder {
|
|
|
24
24
|
constructor() {
|
|
25
25
|
const mime = pickMime();
|
|
26
26
|
if (!mime)
|
|
27
|
-
throw new Error('
|
|
27
|
+
throw new Error('MediaRecorder is not supported in this environment');
|
|
28
28
|
this.#mime = mime;
|
|
29
29
|
}
|
|
30
30
|
get mimeType() { return this.#mime; }
|
|
31
31
|
get recording() { return this.#recorder?.state === 'recording'; }
|
|
32
32
|
start(canvas, videoBitsPerSecond = 12_000_000) {
|
|
33
33
|
if (this.#recorder)
|
|
34
|
-
throw new Error('
|
|
34
|
+
throw new Error('Recording already in progress');
|
|
35
35
|
const stream = canvas.captureStream(60);
|
|
36
36
|
this.#chunks = [];
|
|
37
37
|
this.#recorder = new MediaRecorder(stream, { mimeType: this.#mime, videoBitsPerSecond });
|
|
@@ -51,7 +51,7 @@ export class CanvasRecorder {
|
|
|
51
51
|
const blob = new Blob(this.#chunks, { type: this.#mime });
|
|
52
52
|
this.#recorder = null;
|
|
53
53
|
if (blob.size === 0) {
|
|
54
|
-
err(new Error(
|
|
54
|
+
err(new Error(`Recording produced 0 bytes (${this.#mime}); encoder may be unavailable`));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
ok({ blob, mimeType: this.#mime, seconds: (performance.now() - this.#startedAt) / 1000, bytes: blob.size });
|
package/dist/observe.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* —— 评审指出的"最大产品级缺口":GPGPU 库却看不到"这帧 GPU 花了多少"。
|
|
4
4
|
*/
|
|
5
5
|
import { GpuContext } from "./core/context.js";
|
|
6
|
-
import { UsageError } from "./core/errors.js";
|
|
6
|
+
import { ERR, UsageError } from "./core/errors.js";
|
|
7
7
|
/** 时间戳查询封装:测量一段 GPU 工作的真实耗时(毫秒)。
|
|
8
8
|
* Chrome/Edge 支持 timestamp-query;不支持的浏览器 reject。 */
|
|
9
9
|
export async function timeGpu(fn) {
|
|
@@ -11,7 +11,7 @@ export async function timeGpu(fn) {
|
|
|
11
11
|
const device = ctx.device;
|
|
12
12
|
const featureOk = device.features.has('timestamp-query');
|
|
13
13
|
if (!featureOk)
|
|
14
|
-
throw new UsageError('timestamp-query
|
|
14
|
+
throw new UsageError(ERR.TIMESTAMP_UNSUPPORTED, 'timestamp-query is not supported on this device (requires Chrome/Edge + a GPU with timestamp support)');
|
|
15
15
|
const QUERY_POOL = 2;
|
|
16
16
|
const querySet = device.createQuerySet({ type: 'timestamp', count: QUERY_POOL });
|
|
17
17
|
const resolveBuf = device.createBuffer({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Buffer } from '../../core/buffer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* NeighborGrid —— 通用空间邻域加速(计数排序 spatial hash)。
|
|
4
|
+
*
|
|
5
|
+
* 从粒子包的 grid 实现中提取的通用能力:任意"每帧需要查邻居"的模拟
|
|
6
|
+
* (流体 SPH / boids / 碰撞 / 聚类)都能用,实测 8.5× 于暴力解、近似 O(N)。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* const grid = await NeighborGrid.create({ count, worldHalf, cellSize });
|
|
10
|
+
* // 每帧:先 update(按位置建格),再让你的力 kernel 读 cellStart/cellFill/order
|
|
11
|
+
* grid.update(posBuffer);
|
|
12
|
+
* // 你的 kernel 通过 order[k] 解引用邻居(或直接用 grid.sortedPos 若启用了 payload)
|
|
13
|
+
*
|
|
14
|
+
* 设计说明:三个 build pass 在同一个 encoder 内提交(实测 pass 边界保证可见性);
|
|
15
|
+
* 粒子包保留其含 payload 排序的专用高性能变体,本包是无 payload 的通用版。
|
|
16
|
+
*/
|
|
17
|
+
export interface NeighborGridConfig {
|
|
18
|
+
/** 粒子/实体数量 */
|
|
19
|
+
count: number;
|
|
20
|
+
/** 世界半宽(世界 = [-worldHalf, worldHalf]) */
|
|
21
|
+
worldHalf: number;
|
|
22
|
+
/** 格子边长(通常 = 交互半径,使邻域恰为 3×3 格) */
|
|
23
|
+
cellSize: number;
|
|
24
|
+
workgroupSize?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface NeighborGrid {
|
|
27
|
+
readonly gridSize: number;
|
|
28
|
+
readonly cells: number;
|
|
29
|
+
/** 格内首个有序槽位 */
|
|
30
|
+
cellStart: Buffer;
|
|
31
|
+
/** 格内结束槽位(原子填充) */
|
|
32
|
+
cellFill: Buffer;
|
|
33
|
+
/** 按格子序排列的实体下标(order[slot] = 实体 i) */
|
|
34
|
+
order: Buffer;
|
|
35
|
+
/** 建格:counts → scan → scatter(三 pass,一 encoder,内部提交) */
|
|
36
|
+
update(pos: Buffer): void;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createNeighborGrid(config: NeighborGridConfig): Promise<NeighborGrid>;
|