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 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
  [![CI](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/wgpu-kit)](https://www.npmjs.com/package/wgpu-kit) [![license MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
@@ -70,8 +70,8 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
70
70
 
71
71
  | 指标 | 数值 | 环境 |
72
72
  | --- | --- | --- |
73
- | 粒子端到端 | 200,000 @ 122fps / 66,000 @ 144fps | RTX 4060 Laptop,playground 实测 |
74
- | 粒子计算(grid) | 16k→262k 平坦,3.0→3.3ms/帧 | headless 基准 |
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
  [![CI](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/wgpu-kit)](https://www.npmjs.com/package/wgpu-kit) [![license MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
4
 
8
- [**API Reference**](docs/API.md) · [中文文档](README.cn.md) · **[LIVE DEMO](https://nanfengw0w.github.io/wgpu-kit/)** · Changelog: [releases](https://github.com/nanfengw0w/wgpu-kit/releases)
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
  ![wgpu-kit particle life](hero.gif)
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'; // 或 'wgpu-kit/particles'
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
  ![life quartet](life-quartet.png)
@@ -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 @ 142fps | RTX 4060 Laptop, playground |
76
- | particle compute (grid) | 131k @ 0.89ms/frame | headless bench |
77
- | neighborhood algorithms | grid ~O(N); 8.5× faster than brute force at 66k | same-session A/B |
78
- | bundle size | core gzip 5.5kB; +particles 10.3kB | 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 |
@@ -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 长度必须是正整数,收到: ${String(length)}`);
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(`未知类型 "${String(kind)}",可用: ${Object.keys(TYPES).join(', ')}`);
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 需要 ${def.typed},收到 ${data.constructor?.name ?? typeof data}`);
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 需要 ${expected} 个分量,收到 ${data.length}`);
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
+ };
@@ -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)
@@ -1,20 +1,32 @@
1
- /** 错误体系:所有错误说人话,给出定位与修复建议(章程原则 3)。 */
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
- constructor(message: string);
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
- /** 环境无 WebGPU / 拿不到 adapter */
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
- /** 创建 compute 管线并用 pushErrorScope 捕获异步校验错误(超限等),把"黑屏刷屏"变成显式报错 */
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>;
@@ -1,42 +1,57 @@
1
- /** 错误体系:所有错误说人话,给出定位与修复建议(章程原则 3)。 */
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
- constructor(message) {
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
- /** 环境无 WebGPU / 拿不到 adapter */
15
+ /** WebGPU is unavailable or the adapter could not be acquired. */
9
16
  export class WebGPUUnavailableError extends WgpuKitError {
10
17
  constructor(reason) {
11
- super(`当前环境不可用 WebGPU: ${reason}\n` +
12
- ' 排查:① 浏览器需 Chrome/Edge 113+ Safari 18+;② 无头环境需开启 WebGPU;③ 检查 GPU 驱动与硬件加速设置。\n' +
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 ? `用户代码第 ${userLine} 行` : `生成代码第 ${m.line} (库的问题,欢迎报 issue)`;
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 编译失败:\n${mapped}`);
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
- /** 创建 compute 管线并用 pushErrorScope 捕获异步校验错误(超限等),把"黑屏刷屏"变成显式报错 */
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 WgpuKitError(`compute 管线 "${label}" 创建失败: ${err.message}
39
- 常见原因:storage buffer 数超过每阶段上限(可向本库提 issue 申请 limits 支持)`);
54
+ throw new PipelineError(label, err.message);
40
55
  }
41
56
  return pipeline;
42
57
  }
@@ -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 必须在 1..512,收到: ${String(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}" 至少需要一个 state inputs 字段`);
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 "${uName}" 是保留名(count 由库自动注入)`);
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}" state/inputs/uniforms 存在重名字段`);
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}" 缺少 code(用户 WGSL 函数)`);
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 缺少资源 "${f.key}"`);
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(`资源 "${f.key}" 类型不匹配: 需要 ${want},收到 ${buf.kind}`);
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(`资源 "${f.key}" 长度 ${buf.length} "${firstKey}" ${count} 不一致`);
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
- let bg = bindGroupCache.get(cacheKey);
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();
@@ -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 Error(`uniform 字段 ${f.name} 的类型 ${f.kind} 暂不支持(当前仅支持标量)`);
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 Error(`缺少 uniform 值: ${f.name}`);
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 Error(`uniform ${f.name} 必须是有限数字,收到: ${String(v)}`);
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 Error(`uniform 字段 ${f.name} 的类型 ${f.kind} 暂不支持(当前仅支持标量)`);
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 Error(`缺少 uniform 值: ${f.name}`);
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 Error(`uniform ${f.name} 必须是有限数字,收到: ${String(v)}`);
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
  }
@@ -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 需要 WGSL 代码');
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 workgroups 必须是正整数,收到 ${String(workgroups)}`);
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('当前环境不支持 MediaRecorder 录制(无可用编码)');
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(`录制产物为空(${this.#mime});编码器可能不可用,换浏览器或网络前重试`));
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 在当前设备不可用( Chrome/Edge + 支持时间戳的 GPU)');
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>;