wgpu-kit 1.1.0 → 1.1.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.cn.md CHANGED
@@ -51,7 +51,7 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
51
51
 
52
52
  | 导入 | 用途 |
53
53
  | --- | --- |
54
- | `wgpu-kit` | elementKernel 核心 + Buffer / PingPong / rawKernel |
54
+ | `wgpu-kit` | elementKernel 核心 + Buffer / PingPong / rawKernel + **类型化 schema** + **pack 平台** |
55
55
  | `wgpu-kit/particles` | 粒子生命:力矩阵预设、自适应世界、热更新 |
56
56
  | `wgpu-kit/life` | 图灵斑图 / 粘菌 / Boids / 软体触手 |
57
57
  | `wgpu-kit/fields` | 向量场平迹 |
@@ -66,14 +66,75 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
66
66
 
67
67
  *life 包:图灵斑图 / 粘菌 / Boids / 软体触手 — [打开演示](https://nanfengw0w.github.io/wgpu-kit/life.html)。*
68
68
 
69
+ ## 类型化 schema
70
+
71
+ WGSL 仍是 WGSL,但**字段表不再是会写错的字符串**。声明一次,TS 行类型、
72
+ WGSL struct 代码和 GPU 缓冲全部同源;拼错字段是编辑器里的红线,不是运行时错误:
73
+
74
+ ```ts
75
+ import { defineSchema, elementKernel } from 'wgpu-kit';
76
+
77
+ const Boid = defineSchema({ pos: 'vec2f', vel: 'vec2f', species: 'u32' });
78
+ type Boid = SchemaInfer<typeof Boid.fields>; // { pos: {x,y}, vel: {x,y}, species: number }
79
+
80
+ const bufs = await Boid.buffers(count);
81
+ bufs.pos.write([{ x: 1, y: 2 }, /* … */]); // ❌ 写成 `{ z: 0 }` 编译期就报
82
+ const k = elementKernel({ state: Boid.fields, code: 'fn userFn(idx: u32) { … }' });
83
+ await k.run(bufs.raws());
84
+ const rows = await bufs.pos.read(); // 返回带类型的行对象
85
+ ```
86
+
87
+ 诚实边界:WGSL 函数体**内部**的拼写错误仍由 WGSL 编译器报错(带你的行号映射)。
88
+ 完整的 WGSL 类型检查是编译器工程;本层消灭的是 JS/WGSL **schema 漂移**和
89
+ 无类型的 buffer 读写。
90
+
91
+ ## 平台,不是功能列表
92
+
93
+ 内置包没有任何特权。`definePack` 就是内置包自己用的契约——统一生命周期、
94
+ 统计、自验证 `probe()` 和注册表:
95
+
96
+ ```ts
97
+ import { definePack, registerPack, listPacks } from 'wgpu-kit';
98
+
99
+ const orbit = definePack({
100
+ name: 'orbit',
101
+ description: 'my N-body toy',
102
+ create: async (config) => {
103
+ // … 用 elementKernel / rawKernel 搭你的模拟 …
104
+ return {
105
+ tick() { /* … */ },
106
+ async probe() { return { energyDrift: 0.003 }; }, // verify harness 会收集
107
+ destroy() { /* … */ },
108
+ };
109
+ },
110
+ });
111
+ registerPack(orbit);
112
+ listPacks(); // [{ name: 'particles', … }, { name: 'fields', … }, { name: 'orbit', … }]
113
+ ```
114
+
115
+ `probe()` 是平台的关键约定:第三方包在验证 harness 里享受与内置包完全相同
116
+ 的待遇——正确性是契约的一部分,不是恩赐。
117
+
69
118
  ## 数字(全部可复现)
70
119
 
71
- | 指标 | 数值 | 环境 |
72
- | --- | --- | --- |
73
- | 粒子端到端 | 200,000 @ 122fps · 66,000 @ 144fps(**可见帧**)| RTX 4060 Laptop,playground 实测 |
74
- | 粒子计算(grid) | 16k→262k 平坦,3.0→4.4ms/帧 | headless 基准,GPU 42°C |
75
- | 邻域算法 | grid 近似 O(N),66k 时比暴力快 8.5× | 同会话 A/B |
76
- | 库体积 | core gzip ~10kB(共享上下文构建) | gzip |
120
+ 三种都是真实数字,量的是不同的东西,**别混着读**(双口径全表见
121
+ [docs/BENCHMARK.md](docs/BENCHMARK.md),由 `npm run bench` 生成):
122
+
123
+ - **显示帧率**:playground 里实际看到的 fps,由浏览器节流(探针:URL
124
+ `?verify=10` 自报);
125
+ - **管线饱和吞吐**:3 帧在途泵送——每帧提交不等完成、在途满 3 帧排空一次,
126
+ 这是 GPU 的持续吞吐上限(`npm run bench` 的"管线 fps"列);
127
+ - **同步延迟**:每帧 `tick()` 后等 GPU 完成——单帧往返上界,用于算法 A/B。
128
+
129
+ | 指标 | 数值 | 口径 | 环境 |
130
+ | --- | --- | --- | --- |
131
+ | 粒子端到端 | 200,000 @ ~120fps · 66,000 @ ~144fps | 显示 | RTX 4060 Laptop,playground 探针 |
132
+ | 粒子计算(grid)同步 | 16k → 200k:3.6 → 36ms/帧 | 同步 | `npm run bench` → docs/BENCHMARK.md |
133
+ | 邻域算法 | grid 近似 O(N),66k 时比暴力快 8.5× | 同步 A/B | 同会话 |
134
+ | 库体积 | core gzip ~15kB,含类型化 schema 与 pack 平台(共享上下文构建) | — | gzip 预算由 build 强制 |
135
+
136
+ 所以:如果你用每帧 `device.queue.onSubmittedWorkDone()` 去测 grid@200k,
137
+ 看到的会是 ~30ms——那是同步延迟列,和 120fps 不矛盾。
77
138
 
78
139
  ## 三条设计铁律
79
140
 
@@ -81,6 +142,17 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
81
142
  2. **错误说人话**——WGSL 编译失败映射回你的代码行号;
82
143
  3. **基准即文档**——所有数字可复现;gzip 体积预算由 `npm run build` 强制核对。
83
144
 
145
+ ## 验证
146
+
147
+ 41+ 自动化探针在真实 GPU 上经 headless Chromium harness 运行(随库附带:
148
+ `tests/` + `scripts/verify.mjs`)——包括**物理等价性回归**(n2 / tiled / grid
149
+ 三种邻域算法结构发散即构建失败),以及扫描不变量与冻结带检测(确定性拦截
150
+ 半格失效类 bug)。
151
+
152
+ **CI 中**:正确性子集(smoke / packages / grid 不变量)在每次 push 时于
153
+ Chrome 的 SwiftShader WebGPU 上运行——无需 GPU——物理正确性不会在机器之间
154
+ 悄悄回退。fps 类探针在 CPU 适配器上没有意义,按设计只在真机上跑。
155
+
84
156
  ## 支持矩阵
85
157
 
86
158
  | 浏览器 | 状态 |
package/README.md CHANGED
@@ -91,7 +91,7 @@ to compile, the error points at **your line**.
91
91
 
92
92
  | import | purpose |
93
93
  | --- | --- |
94
- | `wgpu-kit` | elementKernel core + Buffer / PingPong / rawKernel |
94
+ | `wgpu-kit` | elementKernel core + Buffer / PingPong / rawKernel + **typed schemas** + **pack platform** |
95
95
  | `wgpu-kit/particles` | particle life: presets, adaptive world, live updates |
96
96
  | `wgpu-kit/life` | Turing patterns / Physarum / Boids / Tentacles |
97
97
  | `wgpu-kit/fields` | vector-field advection trails |
@@ -106,16 +106,81 @@ to compile, the error points at **your line**.
106
106
 
107
107
  *The life pack: Turing patterns / Physarum / Boids / Tentacles — [open the demo](https://nanfengw0w.github.io/wgpu-kit/life.html).*
108
108
 
109
+ ## Type-safe schemas
110
+
111
+ WGSL stays WGSL, but the *field table* stops being a string you can get wrong.
112
+ Declare once — TypeScript row types, WGSL struct code and GPU buffers all come
113
+ from the same declaration; a typo'd field is a red squiggle, not a runtime error:
114
+
115
+ ```ts
116
+ import { defineSchema, elementKernel } from 'wgpu-kit';
117
+
118
+ const Boid = defineSchema({ pos: 'vec2f', vel: 'vec2f', species: 'u32' });
119
+ type Boid = SchemaInfer<typeof Boid.fields>; // { pos: {x,y}, vel: {x,y}, species: number }
120
+
121
+ const bufs = await Boid.buffers(count);
122
+ bufs.pos.write([{ x: 1, y: 2 }, /* … */]); // ❌ `{ z: 0 }` fails at compile time
123
+ const k = elementKernel({ state: Boid.fields, code: 'fn userFn(idx: u32) { … }' });
124
+ await k.run(bufs.raws());
125
+ const rows = await bufs.pos.read(); // typed rows back
126
+ ```
127
+
128
+ Honest boundary: errors inside your WGSL function body are still caught by the
129
+ WGSL compiler (with your-line mapping). Full WGSL type-checking is a compiler
130
+ project; what this layer eliminates is JS/WGSL **schema drift** and untyped
131
+ buffer I/O.
132
+
133
+ ## A platform, not a feature list
134
+
135
+ The built-in packs are not privileged. `definePack` is the same contract they
136
+ use — lifecycle, stats, a self-verification `probe()` and a registry:
137
+
138
+ ```ts
139
+ import { definePack, registerPack, listPacks } from 'wgpu-kit';
140
+
141
+ const orbit = definePack({
142
+ name: 'orbit',
143
+ description: 'my N-body toy',
144
+ create: async (config) => {
145
+ // … build your sim from elementKernel / rawKernel …
146
+ return {
147
+ tick() { /* … */ },
148
+ async probe() { return { energyDrift: 0.003 }; }, // verify harness collects this
149
+ destroy() { /* … */ },
150
+ };
151
+ },
152
+ });
153
+ registerPack(orbit);
154
+ listPacks(); // [{ name: 'particles', … }, { name: 'fields', … }, { name: 'orbit', … }]
155
+ ```
156
+
157
+ `probe()` is the platform deal: a third-party pack gets the same treatment in
158
+ the verification harness as the built-ins — correctness is part of the
159
+ contract, not a courtesy.
160
+
109
161
  ## Numbers (reproducible)
110
162
 
111
- All fps numbers are **visible frames** every `tick()` renders fresh state.
163
+ Two honest measurement conventionsboth real, measuring different things. **Don't
164
+ mix them up** (see [docs/BENCHMARK.md](docs/BENCHMARK.md) for both, generated by
165
+ `npm run bench`):
166
+
167
+ - **Display fps** — what you actually see in the playground, paced by the
168
+ browser (probe: append `?verify=10` to any playground URL — it self-reports
169
+ fps and GPU errors).
170
+ - **Pipeline saturation** — bounded 3-frame-in-flight pump: `tick()` without
171
+ waiting, drain every 3 frames. The GPU's sustained throughput ceiling.
172
+ - **Sync latency** — `tick()` then wait for GPU completion every frame. Upper
173
+ bound on per-frame round-trip; used for same-session algorithm A/B.
112
174
 
113
- | metric | value | environment |
114
- | --- | --- | --- |
115
- | particles end-to-end | 200,000 @ 122fps · 66,000 @ 144fps | RTX 4060 Laptop, playground |
116
- | particle compute (grid) | 16k→262k flat, 3.04.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` |
175
+ | metric | value | convention | environment |
176
+ | --- | --- | --- | --- |
177
+ | particles end-to-end | 200,000 @ ~120fps · 66,000 @ ~144fps | display | RTX 4060 Laptop, playground probe |
178
+ | particle compute (grid), sync | 16k 200k: 3.6 36 ms/frame | sync | `npm run bench` → docs/BENCHMARK.md |
179
+ | neighborhood algorithms | grid ~O(N), 8.5× faster than brute force at 66k | sync A/B | same-session |
180
+ | bundle size | core gzip ~15kB incl. typed schemas + pack platform (all entries share one context) | | enforced by `npm run build` |
181
+
182
+ So yes: if you benchmark grid @200k with a per-frame `device.queue.onSubmittedWorkDone()`
183
+ you will see ~30ms — that is the sync-latency column, not a contradiction.
119
184
 
120
185
  ## Three design rules
121
186
 
@@ -130,7 +195,13 @@ All fps numbers are **visible frames** — every `tick()` renders fresh state.
130
195
  41+ automated probes run on a real GPU via a headless Chromium harness
131
196
  (included: `tests/` + `scripts/verify.mjs`) — including a **physics
132
197
  equivalence regression** that fails the build if the neighborhood algorithms
133
- (n2 / tiled / grid) ever produce divergent structures.
198
+ (n2 / tiled / grid) ever produce divergent structures, plus scan-invariant and
199
+ frozen-band checks that catch partial-grid failures deterministically.
200
+
201
+ **In CI:** the correctness subset (smoke / packages / grid invariants) runs on
202
+ every push against Chrome's SwiftShader WebGPU — no GPU required — so the
203
+ physics cannot silently regress between machines. fps-class probes are
204
+ meaningless on a CPU adapter and stay on real hardware by design.
134
205
 
135
206
  ## Support matrix
136
207
 
@@ -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
+ };
@@ -6,9 +6,17 @@ import { WebGPUUnavailableError } from "./errors.js";
6
6
  export class GpuContext {
7
7
  device;
8
8
  adapterInfo;
9
- constructor(device, adapterInfo) {
9
+ /**
10
+ * 必须持有 adapter 强引用:adapter 是 JS 侧到 Dawn Instance 的锚。若被 GC
11
+ * 回收,设备的异步操作(mapAsync / getCompilationInfo)会随机 abort
12
+ * "A valid external Instance reference no longer exists" —— 无头 SwiftShader
13
+ * 上必现的重负载页面死亡根因(CI gpu-probes 第 2~7 跑的事故链)。
14
+ */
15
+ #adapter;
16
+ constructor(device, adapterInfo, adapter) {
10
17
  this.device = device;
11
18
  this.adapterInfo = adapterInfo;
19
+ this.#adapter = adapter;
12
20
  }
13
21
  static #singleton = null;
14
22
  /** 仅供设备丢失自动重建(observe.watchDevice)使用:重置单例 */
@@ -26,9 +34,13 @@ export class GpuContext {
26
34
  }
27
35
  static async #create() {
28
36
  if (typeof navigator === 'undefined' || !('gpu' in navigator) || !navigator.gpu) {
29
- throw new WebGPUUnavailableError('navigator.gpu 不存在');
37
+ throw new WebGPUUnavailableError('navigator.gpu is not available');
30
38
  }
31
- const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
39
+ // 机器有真 GPU high-performance 命中独显;无 GPU 的 CI/虚拟机/远程桌面
40
+ // 场景再退到 forceFallbackAdapter(SwiftShader 软件适配器)——正确性一致,
41
+ // 只是慢,让探针能在任何机器上跑起来而不是直接报"无 WebGPU"。
42
+ const adapter = (await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' })) ??
43
+ (await navigator.gpu.requestAdapter({ forceFallbackAdapter: true }));
32
44
  if (!adapter)
33
45
  throw new WebGPUUnavailableError('requestAdapter() 返回 null');
34
46
  const info = adapter.info;
@@ -49,7 +61,7 @@ export class GpuContext {
49
61
  requiredLimits[key] = supported;
50
62
  }
51
63
  const device = await adapter.requestDevice({ label: 'wgpu-kit', requiredLimits });
52
- return new GpuContext(device, label);
64
+ return new GpuContext(device, label, adapter);
53
65
  }
54
66
  /** device lost 时 reject;调用方可 await 做清理/提示 */
55
67
  get lost() {
@@ -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
  }
@@ -40,6 +40,8 @@ interface NormalizedSpec {
40
40
  inputs: Array<readonly [string, ScalarKind]>;
41
41
  uniforms: Array<readonly [string, ScalarKind]>;
42
42
  code: string;
43
+ /** 入口点静态使用的 binding 号('auto' 布局语义,见生成处的注释) */
44
+ usedBindings: Set<number>;
43
45
  }
44
46
  /** 纯函数:规范校验 + WGSL 代码生成。单测直接覆盖,不碰 GPU。 */
45
47
  export declare function generateElementKernel(spec: ElementKernelSpec): {