wgpu-kit 1.1.1 → 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 +79 -7
- package/README.md +80 -9
- package/dist/core/context.js +15 -3
- package/dist/core/kernel.d.ts +2 -0
- package/dist/core/kernel.js +34 -6
- package/dist/core/pack.d.ts +37 -0
- package/dist/core/pack.js +35 -0
- package/dist/core/raw.js +3 -3
- package/dist/core/schema.d.ts +71 -0
- package/dist/core/schema.js +118 -0
- package/dist/core/shader.d.ts +4 -0
- package/dist/core/shader.js +38 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -0
- package/dist/packs/fields/index.d.ts +11 -1
- package/dist/packs/fields/index.js +27 -3
- package/dist/packs/grid/index.js +3 -3
- package/dist/packs/image/index.js +3 -3
- package/dist/packs/life/boids.js +3 -3
- package/dist/packs/life/physarum.js +3 -3
- package/dist/packs/life/turing.js +3 -3
- package/dist/packs/particles/index.js +3 -3
- package/package.json +1 -1
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
163
|
+
Two honest measurement conventions — both 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 @
|
|
116
|
-
| particle compute (grid) | 16k→
|
|
117
|
-
| neighborhood algorithms | grid ~O(N), 8.5× faster than brute force at 66k |
|
|
118
|
-
| bundle size | core gzip ~
|
|
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
|
|
package/dist/core/context.js
CHANGED
|
@@ -6,9 +6,17 @@ import { WebGPUUnavailableError } from "./errors.js";
|
|
|
6
6
|
export class GpuContext {
|
|
7
7
|
device;
|
|
8
8
|
adapterInfo;
|
|
9
|
-
|
|
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)使用:重置单例 */
|
|
@@ -28,7 +36,11 @@ export class GpuContext {
|
|
|
28
36
|
if (typeof navigator === 'undefined' || !('gpu' in navigator) || !navigator.gpu) {
|
|
29
37
|
throw new WebGPUUnavailableError('navigator.gpu is not available');
|
|
30
38
|
}
|
|
31
|
-
|
|
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() {
|
package/dist/core/kernel.d.ts
CHANGED
|
@@ -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): {
|
package/dist/core/kernel.js
CHANGED
|
@@ -2,6 +2,7 @@ import { planUniform, packUniformInto, TYPES } from "./layout.js";
|
|
|
2
2
|
import { GpuContext } from "./context.js";
|
|
3
3
|
import { Buffer } from "./buffer.js";
|
|
4
4
|
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "./shader.js";
|
|
5
6
|
const RESERVED = new Set(['count']);
|
|
6
7
|
/** GPUBuffer 的稳定数字身份(WeakMap 分配,用于 bind group 缓存键) */
|
|
7
8
|
let nextBufferId = 0;
|
|
@@ -40,6 +41,25 @@ export function generateElementKernel(spec) {
|
|
|
40
41
|
}
|
|
41
42
|
const uniformEntries = [...uniforms, ['count', 'u32']];
|
|
42
43
|
const uniformLayout = planUniform(uniformEntries);
|
|
44
|
+
// —— 静态使用分析:layout:'auto' 的绑定组布局只含入口点**实际引用**的绑定。
|
|
45
|
+
// 声明了但 userFn 没用到的字段若塞进 bind group → 校验错误且被异步吞掉
|
|
46
|
+
// (表现为核不生效)。头文件由我们生成,字段是否使用等价于其名字是否作为
|
|
47
|
+
// 词法 token 出现在用户代码里。params(binding 0)因 params.count 恒被使用。
|
|
48
|
+
const wordInCode = (n) => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(spec.code);
|
|
49
|
+
const usedBindings = new Set([0]);
|
|
50
|
+
{
|
|
51
|
+
let b = 1;
|
|
52
|
+
for (const [n] of state) {
|
|
53
|
+
if (wordInCode(n))
|
|
54
|
+
usedBindings.add(b);
|
|
55
|
+
b++;
|
|
56
|
+
}
|
|
57
|
+
for (const [n] of inputs) {
|
|
58
|
+
if (wordInCode(n))
|
|
59
|
+
usedBindings.add(b);
|
|
60
|
+
b++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
// —— 生成 WGSL:头部(声明) + main + 用户代码 ——
|
|
44
64
|
const header = [];
|
|
45
65
|
header.push('// 由 wgpu-kit elementKernel 生成');
|
|
@@ -65,7 +85,7 @@ export function generateElementKernel(spec) {
|
|
|
65
85
|
const userCodeLineOffset = header.length; // 1-based 行号:用户代码从 offset+1 行开始
|
|
66
86
|
const source = [...header, spec.code].join('\n');
|
|
67
87
|
return {
|
|
68
|
-
normalized: { name, workgroupSize, state, inputs, uniforms, code: spec.code },
|
|
88
|
+
normalized: { name, workgroupSize, state, inputs, uniforms, code: spec.code, usedBindings },
|
|
69
89
|
source,
|
|
70
90
|
uniformLayout,
|
|
71
91
|
userCodeLineOffset,
|
|
@@ -92,10 +112,9 @@ export function elementKernel(spec) {
|
|
|
92
112
|
const compilePipeline = async () => {
|
|
93
113
|
const ctx = await GpuContext.get();
|
|
94
114
|
const device = ctx.device;
|
|
95
|
-
const module = device
|
|
115
|
+
const { module, messages } = await createShaderModuleChecked(device, source, normalized.name);
|
|
96
116
|
// 捕获编译错误并映射行号
|
|
97
|
-
const
|
|
98
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
117
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
99
118
|
if (errors.length > 0) {
|
|
100
119
|
throw new CompileError(normalized.name, errors.map((m) => ({ line: m.lineNum, msg: m.message })), userCodeLineOffset);
|
|
101
120
|
}
|
|
@@ -117,8 +136,10 @@ export function elementKernel(spec) {
|
|
|
117
136
|
// 先编译后切换:新代码编译失败则保持旧版不动
|
|
118
137
|
const savedSource = source;
|
|
119
138
|
const savedOffset = userCodeLineOffset;
|
|
139
|
+
const savedNormalized = normalized;
|
|
120
140
|
source = regen.source;
|
|
121
141
|
userCodeLineOffset = regen.userCodeLineOffset;
|
|
142
|
+
normalized = regen.normalized; // usedBindings 随新代码重算
|
|
122
143
|
try {
|
|
123
144
|
const p = await compilePipeline();
|
|
124
145
|
pipelinePromise = Promise.resolve(p);
|
|
@@ -127,6 +148,7 @@ export function elementKernel(spec) {
|
|
|
127
148
|
catch (e) {
|
|
128
149
|
source = savedSource;
|
|
129
150
|
userCodeLineOffset = savedOffset;
|
|
151
|
+
normalized = savedNormalized;
|
|
130
152
|
throw e;
|
|
131
153
|
}
|
|
132
154
|
},
|
|
@@ -176,8 +198,14 @@ export function elementKernel(spec) {
|
|
|
176
198
|
const identityMatch = cached && cached.ids.length === ids.length && cached.ids.every((id, idx) => id === ids[idx]);
|
|
177
199
|
let bg = identityMatch ? cached.bg : undefined;
|
|
178
200
|
if (!bg) {
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
// 只绑定入口点静态使用的绑定(layout:'auto' 语义,见生成处的注释)
|
|
202
|
+
const entries = [];
|
|
203
|
+
if (normalized.usedBindings.has(0))
|
|
204
|
+
entries.push({ binding: 0, resource: { buffer: uniformBuffer } });
|
|
205
|
+
ordered.forEach((buffer, i) => {
|
|
206
|
+
if (normalized.usedBindings.has(i + 1))
|
|
207
|
+
entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } });
|
|
208
|
+
});
|
|
181
209
|
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
182
210
|
bindGroupCache.set(cacheKey, { bg, ids });
|
|
183
211
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack 平台契约:第三方在 core 之上写自己的模拟包,与内置包(particles/life/
|
|
3
|
+
* fields)平级注册、平级验证。这是"功能集 → 平台"的那一步。
|
|
4
|
+
*
|
|
5
|
+
* 契约只有三条(章程原则 2:错误说人话;原则 3:可验证):
|
|
6
|
+
* ① create(config) → PackSim:统一的生命周期(attach/tick/stats/destroy);
|
|
7
|
+
* ② 可选 probe():自报物理不变量 —— verify harness 会收集并展示,
|
|
8
|
+
* 第三方包从第一天就拥有与内置包相同的验证故事;
|
|
9
|
+
* ③ registerPack():进入运行时注册表,工具链(playground/gallery)可枚举启动。
|
|
10
|
+
*/
|
|
11
|
+
/** 所有 pack 模拟实例的统一生命周期。tick/destroy 必须;其余可选。 */
|
|
12
|
+
export interface PackSim {
|
|
13
|
+
/** 需要 canvas 的包在此建渲染器(可选) */
|
|
14
|
+
attach?(canvas: HTMLCanvasElement): Promise<void>;
|
|
15
|
+
/** 推进一帧(计算 + 可选渲染) */
|
|
16
|
+
tick(): void;
|
|
17
|
+
/** 轻量运行统计(fps 等),工具链直接读 */
|
|
18
|
+
stats?(): Record<string, number | string>;
|
|
19
|
+
/** 自检:返回物理不变量(如 totalMass / meanSpeed / gpuErrors),verify 页收集展示 */
|
|
20
|
+
probe?(): Promise<Record<string, number | string | boolean>>;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
export interface WgpuKitPack<TConfig, TSim extends PackSim = PackSim> {
|
|
24
|
+
/** 注册表命名空间名(如 'fields');重复注册同名包报 UsageError */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly description?: string;
|
|
27
|
+
create(config?: TConfig): Promise<TSim>;
|
|
28
|
+
}
|
|
29
|
+
/** 声明一个 pack。运行时只做契约校验;泛型把 TConfig 的类型检查交给编辑器。 */
|
|
30
|
+
export declare function definePack<TConfig, TSim extends PackSim>(pack: WgpuKitPack<TConfig, TSim>): WgpuKitPack<TConfig, TSim>;
|
|
31
|
+
/** 注册进全局注册表(同名重复注册报错,防止第三方覆盖内置包) */
|
|
32
|
+
export declare function registerPack<TConfig, TSim extends PackSim>(pack: WgpuKitPack<TConfig, TSim>): void;
|
|
33
|
+
export declare function getPack(name: string): WgpuKitPack<unknown, PackSim> | undefined;
|
|
34
|
+
export declare function listPacks(): Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack 平台契约:第三方在 core 之上写自己的模拟包,与内置包(particles/life/
|
|
3
|
+
* fields)平级注册、平级验证。这是"功能集 → 平台"的那一步。
|
|
4
|
+
*
|
|
5
|
+
* 契约只有三条(章程原则 2:错误说人话;原则 3:可验证):
|
|
6
|
+
* ① create(config) → PackSim:统一的生命周期(attach/tick/stats/destroy);
|
|
7
|
+
* ② 可选 probe():自报物理不变量 —— verify harness 会收集并展示,
|
|
8
|
+
* 第三方包从第一天就拥有与内置包相同的验证故事;
|
|
9
|
+
* ③ registerPack():进入运行时注册表,工具链(playground/gallery)可枚举启动。
|
|
10
|
+
*/
|
|
11
|
+
import { UsageError, ERR } from "./errors.js";
|
|
12
|
+
/** 声明一个 pack。运行时只做契约校验;泛型把 TConfig 的类型检查交给编辑器。 */
|
|
13
|
+
export function definePack(pack) {
|
|
14
|
+
if (!pack || typeof pack.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(pack.name)) {
|
|
15
|
+
throw new UsageError(ERR.USAGE, `definePack: name must be a lowercase identifier, got: ${String(pack?.name)}`);
|
|
16
|
+
}
|
|
17
|
+
if (typeof pack.create !== 'function') {
|
|
18
|
+
throw new UsageError(ERR.USAGE, `definePack("${pack.name}"): create(config) is required`);
|
|
19
|
+
}
|
|
20
|
+
return pack;
|
|
21
|
+
}
|
|
22
|
+
const REGISTRY = new Map();
|
|
23
|
+
/** 注册进全局注册表(同名重复注册报错,防止第三方覆盖内置包) */
|
|
24
|
+
export function registerPack(pack) {
|
|
25
|
+
if (REGISTRY.has(pack.name)) {
|
|
26
|
+
throw new UsageError(ERR.USAGE, `registerPack("${pack.name}"): already registered`);
|
|
27
|
+
}
|
|
28
|
+
REGISTRY.set(pack.name, pack);
|
|
29
|
+
}
|
|
30
|
+
export function getPack(name) {
|
|
31
|
+
return REGISTRY.get(name);
|
|
32
|
+
}
|
|
33
|
+
export function listPacks() {
|
|
34
|
+
return [...REGISTRY.values()].map((p) => ({ name: p.name, description: p.description ?? '' }));
|
|
35
|
+
}
|
package/dist/core/raw.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GpuContext } from "./context.js";
|
|
2
2
|
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
3
|
+
import { createShaderModuleChecked } from "./shader.js";
|
|
3
4
|
export function rawKernel(code, entryPoint = 'main', label = 'rawKernel') {
|
|
4
5
|
if (typeof code !== 'string' || code.trim().length === 0)
|
|
5
6
|
throw new UsageError(ERR.USAGE, 'rawKernel requires WGSL code');
|
|
@@ -12,9 +13,8 @@ export function rawKernel(code, entryPoint = 'main', label = 'rawKernel') {
|
|
|
12
13
|
const ctx = await GpuContext.get();
|
|
13
14
|
if (!pipelinePromise) {
|
|
14
15
|
pipelinePromise = (async () => {
|
|
15
|
-
const module = ctx.device
|
|
16
|
-
const
|
|
17
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
16
|
+
const { module, messages } = await createShaderModuleChecked(ctx.device, code, label);
|
|
17
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
18
18
|
if (errors.length > 0) {
|
|
19
19
|
pipelinePromise = null;
|
|
20
20
|
// 实测(Dawn/Edge 151):lineNum 已是 1-based
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 类型化 schema 层:字段名与类型用 const 对象声明一次,三处受益——
|
|
3
|
+
* ① TS 类型:SchemaInfer 给出精确的行类型,字段拼错/类型不匹配在编辑器就报红;
|
|
4
|
+
* ② WGSL:wgslStruct() 由同一份声明生成 struct 代码,消灭"TS 字段表和
|
|
5
|
+
* WGSL struct 各写一份"的漂移(名称/顺序由库保证一致);
|
|
6
|
+
* ③ 缓冲:buffers() 生成与 elementKernel 绑定模型一致的逐字段 TypedBuffer。
|
|
7
|
+
* 诚实边界:用户 WGSL 函数体内部的拼写错误仍由 WGSL 编译期报错(带用户行号
|
|
8
|
+
* 映射)——完整 WGSL 类型检查是一个编译器工程,不在本层承诺范围内。
|
|
9
|
+
*/
|
|
10
|
+
import type { ScalarKind } from './layout.ts';
|
|
11
|
+
import { Buffer } from './buffer.ts';
|
|
12
|
+
export interface Vec2 {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}
|
|
16
|
+
export interface Vec3 {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
z: number;
|
|
20
|
+
}
|
|
21
|
+
export interface Vec4 {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
z: number;
|
|
25
|
+
w: number;
|
|
26
|
+
}
|
|
27
|
+
/** ScalarKind → TS 行类型(编译期映射,本层类型安全的根基) */
|
|
28
|
+
export type KindOf<S extends ScalarKind> = S extends 'f32' | 'i32' | 'u32' ? number : S extends 'vec2f' | 'vec2i' | 'vec2u' ? Vec2 : S extends 'vec3f' ? Vec3 : S extends 'vec4f' ? Vec4 : never;
|
|
29
|
+
/** schema 声明 → TS 行类型。字段名逐字保留,拼错即编译错误。 */
|
|
30
|
+
export type SchemaInfer<F extends Record<string, ScalarKind>> = {
|
|
31
|
+
readonly [K in keyof F]: KindOf<F[K]>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* 单字段类型化缓冲:write/read 收发**行对象**(如 {x, y});原始 flat TypedArray
|
|
35
|
+
* 走 raw.write/raw.read(章程原则 1:逃生舱常开)。
|
|
36
|
+
*/
|
|
37
|
+
export declare class TypedBuffer<K extends ScalarKind> {
|
|
38
|
+
readonly raw: Buffer<K>;
|
|
39
|
+
readonly kind: K;
|
|
40
|
+
readonly length: number;
|
|
41
|
+
private constructor();
|
|
42
|
+
static create<K extends ScalarKind>(kind: K, length: number): Promise<TypedBuffer<K>>;
|
|
43
|
+
/** 行对象数组 → GPU */
|
|
44
|
+
write(rows: ReadonlyArray<KindOf<K>>): void;
|
|
45
|
+
/** GPU → 行对象数组 */
|
|
46
|
+
read(): Promise<Array<KindOf<K>>>;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
}
|
|
49
|
+
/** 逐字段缓冲集合:key 逐字保留 schema 字段名;raws() 直接喂 elementKernel.run() */
|
|
50
|
+
export type SchemaBuffers<F extends Record<string, ScalarKind>> = {
|
|
51
|
+
readonly [K in keyof F]: TypedBuffer<F[K]>;
|
|
52
|
+
} & {
|
|
53
|
+
/** 传给 elementKernel 的 run()/初始绑定:字段名 → 底层缓冲 */
|
|
54
|
+
raws(): {
|
|
55
|
+
readonly [K in keyof F]: Buffer<F[K]>;
|
|
56
|
+
};
|
|
57
|
+
destroy(): void;
|
|
58
|
+
};
|
|
59
|
+
/** defineSchema 的返回值:同一份声明,喂类型、喂 WGSL、喂缓冲 */
|
|
60
|
+
export interface Schema<F extends Record<string, ScalarKind>> {
|
|
61
|
+
/** 声明顺序即 WGSL struct 成员顺序;直接作 elementKernel 的 state */
|
|
62
|
+
readonly fields: F;
|
|
63
|
+
/**
|
|
64
|
+
* 生成 `struct <name> { ... }`。storage 语义下 vec3f 成员补 @size(16)
|
|
65
|
+
* (WGSL array 元素步长规则),与 CPU 侧 TYPES 步长一一对应,永不漂移。
|
|
66
|
+
*/
|
|
67
|
+
wgslStruct(name: string, addressSpace?: 'storage' | 'uniform'): string;
|
|
68
|
+
/** 逐字段缓冲(与 elementKernel 的绑定模型一致) */
|
|
69
|
+
buffers(count: number): Promise<SchemaBuffers<F>>;
|
|
70
|
+
}
|
|
71
|
+
export declare function defineSchema<const F extends Record<string, ScalarKind>>(fields: F): Schema<F>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { TYPES } from "./layout.js";
|
|
2
|
+
import { Buffer } from "./buffer.js";
|
|
3
|
+
import { ERR, UsageError } from "./errors.js";
|
|
4
|
+
const COMPS = {
|
|
5
|
+
f32: 1, i32: 1, u32: 1, vec2f: 2, vec2i: 2, vec2u: 2, vec3f: 3, vec4f: 4,
|
|
6
|
+
};
|
|
7
|
+
const COMP_NAMES = ['x', 'y', 'z', 'w'];
|
|
8
|
+
function rowToFlat(kind, row, index, out, base) {
|
|
9
|
+
const comps = COMPS[kind];
|
|
10
|
+
if (comps === 1) {
|
|
11
|
+
if (typeof row !== 'number' || !Number.isFinite(row)) {
|
|
12
|
+
throw new UsageError(ERR.BUFFER_WRITE, `schema row ${index} expects a finite number, got: ${typeof row}`);
|
|
13
|
+
}
|
|
14
|
+
out[base] = row;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof row !== 'object' || row === null) {
|
|
18
|
+
throw new UsageError(ERR.BUFFER_WRITE, `schema row ${index} expects an object with ${comps} components, got: ${typeof row}`);
|
|
19
|
+
}
|
|
20
|
+
const rec = row;
|
|
21
|
+
for (let c = 0; c < comps; c++) {
|
|
22
|
+
const v = rec[COMP_NAMES[c]];
|
|
23
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
24
|
+
throw new UsageError(ERR.BUFFER_WRITE, `schema row ${index} component "${COMP_NAMES[c]}" must be a finite number, got: ${String(v)}`);
|
|
25
|
+
}
|
|
26
|
+
out[base + c] = v;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function flatToRow(kind, flat, i) {
|
|
30
|
+
const comps = COMPS[kind];
|
|
31
|
+
const base = i * comps;
|
|
32
|
+
if (comps === 1)
|
|
33
|
+
return flat[base];
|
|
34
|
+
if (comps === 2)
|
|
35
|
+
return { x: flat[base], y: flat[base + 1] };
|
|
36
|
+
if (comps === 3)
|
|
37
|
+
return { x: flat[base], y: flat[base + 1], z: flat[base + 2] };
|
|
38
|
+
return { x: flat[base], y: flat[base + 1], z: flat[base + 2], w: flat[base + 3] };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 单字段类型化缓冲:write/read 收发**行对象**(如 {x, y});原始 flat TypedArray
|
|
42
|
+
* 走 raw.write/raw.read(章程原则 1:逃生舱常开)。
|
|
43
|
+
*/
|
|
44
|
+
export class TypedBuffer {
|
|
45
|
+
raw;
|
|
46
|
+
kind;
|
|
47
|
+
length;
|
|
48
|
+
constructor(raw, kind) {
|
|
49
|
+
this.raw = raw;
|
|
50
|
+
this.kind = kind;
|
|
51
|
+
this.length = raw.length;
|
|
52
|
+
}
|
|
53
|
+
static async create(kind, length) {
|
|
54
|
+
return new TypedBuffer(await Buffer.create(kind, length), kind);
|
|
55
|
+
}
|
|
56
|
+
/** 行对象数组 → GPU */
|
|
57
|
+
write(rows) {
|
|
58
|
+
const comps = COMPS[this.kind];
|
|
59
|
+
const def = TYPES[this.kind];
|
|
60
|
+
const flat = (def.typed === 'Float32Array' ? new Float32Array(rows.length * comps)
|
|
61
|
+
: def.typed === 'Int32Array' ? new Int32Array(rows.length * comps)
|
|
62
|
+
: new Uint32Array(rows.length * comps));
|
|
63
|
+
for (let i = 0; i < rows.length; i++)
|
|
64
|
+
rowToFlat(this.kind, rows[i], i, flat, i * comps);
|
|
65
|
+
this.raw.write(flat);
|
|
66
|
+
}
|
|
67
|
+
/** GPU → 行对象数组 */
|
|
68
|
+
async read() {
|
|
69
|
+
const flat = await this.raw.read();
|
|
70
|
+
const rows = new Array(this.length);
|
|
71
|
+
for (let i = 0; i < this.length; i++)
|
|
72
|
+
rows[i] = flatToRow(this.kind, flat, i);
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
75
|
+
destroy() {
|
|
76
|
+
this.raw.destroy();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function defineSchema(fields) {
|
|
80
|
+
const keys = Object.keys(fields);
|
|
81
|
+
if (keys.length === 0) {
|
|
82
|
+
throw new UsageError(ERR.USAGE, 'defineSchema requires at least one field');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
fields,
|
|
86
|
+
wgslStruct(name, addressSpace = 'storage') {
|
|
87
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
|
|
88
|
+
throw new UsageError(ERR.USAGE, `wgslStruct name must be a valid WGSL identifier, got: "${name}"`);
|
|
89
|
+
}
|
|
90
|
+
const lines = keys.map((k) => {
|
|
91
|
+
const def = TYPES[fields[k]];
|
|
92
|
+
// vec3f 在数组元素语义下步长 16 —— 显式 @size 让 GPU/CPU 永不漂移
|
|
93
|
+
const pad = addressSpace === 'storage' && def.stride !== def.size ? ` @size(${def.stride})` : '';
|
|
94
|
+
return ` ${k}:${pad} ${def.wgsl},`;
|
|
95
|
+
});
|
|
96
|
+
return `struct ${name} {\n${lines.join('\n')}\n}`;
|
|
97
|
+
},
|
|
98
|
+
async buffers(count) {
|
|
99
|
+
const store = {};
|
|
100
|
+
for (const k of keys) {
|
|
101
|
+
store[k] = await TypedBuffer.create(fields[k], count);
|
|
102
|
+
}
|
|
103
|
+
const raws = () => {
|
|
104
|
+
const r = {};
|
|
105
|
+
for (const k of keys)
|
|
106
|
+
r[k] = store[k].raw;
|
|
107
|
+
return r;
|
|
108
|
+
};
|
|
109
|
+
return Object.assign(store, {
|
|
110
|
+
raws,
|
|
111
|
+
destroy() {
|
|
112
|
+
for (const k of keys)
|
|
113
|
+
store[k].destroy();
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 带去重与降级的 shader 编译检查。
|
|
3
|
+
*
|
|
4
|
+
* ① 去重:同一 device 上相同 code 的模块直接复用(GPUShaderModule 创建后
|
|
5
|
+
* 不可变,跨管线共享安全)。
|
|
6
|
+
*
|
|
7
|
+
* ② 降级:CI 的 SwiftShader 在页面 GPU 负载累积后,getCompilationInfo 会
|
|
8
|
+
* 对一切新模块抛 "Instance dropped"(OperationError,连唯一代码的首查
|
|
9
|
+
* 都失败)。此时返回 messages: [] 继续编译 —— 编译错误并不会被掩盖:
|
|
10
|
+
* 管线创建/首.dispatch 会触发 uncapturederror,各验证页都有监听。
|
|
11
|
+
* 健康环境(真 GPU / smoke 页)下查询照常工作,CompileError 行号映射
|
|
12
|
+
* 不受影响。
|
|
13
|
+
*/
|
|
14
|
+
const MODULE_CACHE = new WeakMap();
|
|
15
|
+
export async function createShaderModuleChecked(device, code, label) {
|
|
16
|
+
let cache = MODULE_CACHE.get(device);
|
|
17
|
+
if (!cache) {
|
|
18
|
+
cache = new Map();
|
|
19
|
+
MODULE_CACHE.set(device, cache);
|
|
20
|
+
}
|
|
21
|
+
const hit = cache.get(code);
|
|
22
|
+
if (hit)
|
|
23
|
+
return hit;
|
|
24
|
+
const module = device.createShaderModule({ code, label });
|
|
25
|
+
let messages;
|
|
26
|
+
try {
|
|
27
|
+
messages = (await module.getCompilationInfo()).messages;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.warn(`[wgpu-kit] getCompilationInfo unavailable (${String(e?.message ?? e).slice(0, 60)}); skipping compile-info check for "${label}"`);
|
|
31
|
+
const result = { module, messages: [] };
|
|
32
|
+
cache.set(code, result);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const result = { module, messages };
|
|
36
|
+
cache.set(code, result);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { elementKernel, type ElementKernel, type ElementKernelSpec } from './cor
|
|
|
4
4
|
import { PingPong } from './core/pingpong.ts';
|
|
5
5
|
import { rawKernel } from './core/raw.ts';
|
|
6
6
|
export { GpuContext, Buffer, elementKernel, PingPong, rawKernel };
|
|
7
|
+
export { defineSchema, TypedBuffer, type Schema, type SchemaBuffers, type SchemaInfer, type KindOf, type Vec2, type Vec3, type Vec4 } from './core/schema.ts';
|
|
8
|
+
export { definePack, registerPack, getPack, listPacks, type PackSim, type WgpuKitPack } from './core/pack.ts';
|
|
7
9
|
export { particles, type ParticlesSim } from './packs/particles/index.ts';
|
|
10
|
+
export { fieldsPack, type FlowSim, type FlowConfig } from './packs/fields/index.ts';
|
|
8
11
|
export type { ElementKernel, ElementKernelSpec };
|
|
9
12
|
export { TYPES, planUniform, packUniform, packUniformInto, type ScalarKind } from './core/layout.ts';
|
|
10
13
|
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from './core/errors.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,20 @@ import { PingPong } from "./core/pingpong.js";
|
|
|
5
5
|
import { rawKernel } from "./core/raw.js";
|
|
6
6
|
import { UsageError } from "./core/errors.js";
|
|
7
7
|
export { GpuContext, Buffer, elementKernel, PingPong, rawKernel };
|
|
8
|
+
export { defineSchema, TypedBuffer } from "./core/schema.js";
|
|
9
|
+
export { definePack, registerPack, getPack, listPacks } from "./core/pack.js";
|
|
8
10
|
// 主入口直达旗舰包:import { particles } from 'wgpu-kit' 开箱即用
|
|
9
11
|
export { particles } from "./packs/particles/index.js";
|
|
12
|
+
export { fieldsPack } from "./packs/fields/index.js";
|
|
10
13
|
export { TYPES, planUniform, packUniform, packUniformInto } from "./core/layout.js";
|
|
11
14
|
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from "./core/errors.js";
|
|
15
|
+
// 内置包进入注册表(第三方包用 registerPack 注册后同样可被 listPacks 枚举)
|
|
16
|
+
import { registerPack } from "./core/pack.js";
|
|
17
|
+
import { particles as _particles } from "./packs/particles/index.js";
|
|
18
|
+
import { fieldsPack as _fieldsPack } from "./packs/fields/index.js";
|
|
19
|
+
registerPack({
|
|
20
|
+
name: 'particles',
|
|
21
|
+
description: 'Particle-life physics (n2 / tiled / grid up to 200k+)',
|
|
22
|
+
create: (config) => _particles(config ?? {}),
|
|
23
|
+
});
|
|
24
|
+
registerPack(_fieldsPack);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackSim } from '../../core/pack.ts';
|
|
1
2
|
import { type Colormap } from '../life/map.ts';
|
|
2
3
|
/**
|
|
3
4
|
* fields 包:向量场平迹(flow)。粒子被解析向量场平流,沉积到带衰减的信息素图上,
|
|
@@ -15,13 +16,22 @@ export interface FlowConfig {
|
|
|
15
16
|
seed?: string | number;
|
|
16
17
|
colormap?: Colormap;
|
|
17
18
|
}
|
|
18
|
-
export interface FlowSim {
|
|
19
|
+
export interface FlowSim extends PackSim {
|
|
19
20
|
attach(canvas: HTMLCanvasElement): Promise<void>;
|
|
20
21
|
tick(): void;
|
|
21
22
|
stats(): {
|
|
22
23
|
fps: number;
|
|
23
24
|
};
|
|
25
|
+
/** 自检:信息素图应有结构(有限值、非零峰值)——verify harness 直接收集 */
|
|
26
|
+
probe(): Promise<{
|
|
27
|
+
finite: boolean;
|
|
28
|
+
trailMax: number;
|
|
29
|
+
trailMean: number;
|
|
30
|
+
frames: number;
|
|
31
|
+
}>;
|
|
24
32
|
sampleTrail(): Promise<Float32Array>;
|
|
25
33
|
destroy(): void;
|
|
26
34
|
}
|
|
27
35
|
export declare function flow(config?: FlowConfig): Promise<FlowSim>;
|
|
36
|
+
/** fields 包的平台注册形态:第三方包与它长得一模一样(见 docs "Writing a pack") */
|
|
37
|
+
export declare const fieldsPack: import("../../index.ts").WgpuKitPack<FlowConfig, FlowSim>;
|
|
@@ -2,6 +2,8 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
6
|
+
import { definePack } from "../../core/pack.js";
|
|
5
7
|
import { MapRenderer } from "../life/map.js";
|
|
6
8
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
9
|
const FIELD_FNS = {
|
|
@@ -79,9 +81,8 @@ export async function flow(config = {}) {
|
|
|
79
81
|
device.queue.writeBuffer(diffuseUniform, 0, new Uint32Array([mapSize, mapSize]));
|
|
80
82
|
device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
|
|
81
83
|
const compile = async (code, label) => {
|
|
82
|
-
const m = device
|
|
83
|
-
const
|
|
84
|
-
const errors = info.messages.filter((x) => x.type === 'error');
|
|
84
|
+
const { module: m, messages } = await createShaderModuleChecked(device, code, label);
|
|
85
|
+
const errors = messages.filter((x) => x.type === 'error');
|
|
85
86
|
if (errors.length > 0)
|
|
86
87
|
throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
|
|
87
88
|
return m;
|
|
@@ -151,6 +152,23 @@ export async function flow(config = {}) {
|
|
|
151
152
|
},
|
|
152
153
|
stats() { return { fps: lastFps }; },
|
|
153
154
|
async sampleTrail() { return (await trail.current.t.read()); },
|
|
155
|
+
async probe() {
|
|
156
|
+
const t = (await trail.current.t.read());
|
|
157
|
+
let finite = true;
|
|
158
|
+
let maxv = 0;
|
|
159
|
+
let sum = 0;
|
|
160
|
+
for (let i = 0; i < t.length; i++) {
|
|
161
|
+
const v = t[i];
|
|
162
|
+
if (!Number.isFinite(v)) {
|
|
163
|
+
finite = false;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (v > maxv)
|
|
167
|
+
maxv = v;
|
|
168
|
+
sum += v;
|
|
169
|
+
}
|
|
170
|
+
return { finite, trailMax: maxv, trailMean: sum / Math.max(t.length, 1), frames: frame };
|
|
171
|
+
},
|
|
154
172
|
destroy() {
|
|
155
173
|
posBuf.destroy();
|
|
156
174
|
trail.destroy();
|
|
@@ -159,6 +177,12 @@ export async function flow(config = {}) {
|
|
|
159
177
|
},
|
|
160
178
|
};
|
|
161
179
|
}
|
|
180
|
+
/** fields 包的平台注册形态:第三方包与它长得一模一样(见 docs "Writing a pack") */
|
|
181
|
+
export const fieldsPack = definePack({
|
|
182
|
+
name: 'fields',
|
|
183
|
+
description: 'Vector-field advection trails (vortex / curl / twin)',
|
|
184
|
+
create: (config) => flow(config ?? {}),
|
|
185
|
+
});
|
|
162
186
|
function advectWgsl(fieldFn, mapSize) {
|
|
163
187
|
return /* wgsl */ `
|
|
164
188
|
struct Params {
|
package/dist/packs/grid/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GpuContext } from "../../core/context.js";
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { CompileError } from "../../core/errors.js";
|
|
4
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
4
5
|
const WG = 64;
|
|
5
6
|
const SCAN = 256;
|
|
6
7
|
const USIZE = 32;
|
|
@@ -34,9 +35,8 @@ export async function createNeighborGrid(config) {
|
|
|
34
35
|
const cellFill = await Buffer.create('u32', cells);
|
|
35
36
|
const order = await Buffer.create('u32', count);
|
|
36
37
|
cellCount.write(new Uint32Array(cells));
|
|
37
|
-
const module = device
|
|
38
|
-
const
|
|
39
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
38
|
+
const { module, messages } = await createShaderModuleChecked(device, gridWgsl(), 'ngrid');
|
|
39
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
40
40
|
if (errors.length > 0)
|
|
41
41
|
throw new CompileError('ngrid', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
42
42
|
const pCounts = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_counts' } });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GpuContext } from "../../core/context.js";
|
|
2
2
|
import { CompileError } from "../../core/errors.js";
|
|
3
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
3
4
|
const OPS = ['grayscale', 'invert', 'edge', 'blur', 'sharpen', 'brightness', 'contrast'];
|
|
4
5
|
const OP_IDS = { grayscale: 0, invert: 1, edge: 2, blur: 3, sharpen: 4, brightness: 5, contrast: 6 };
|
|
5
6
|
export async function applyImage(source, target, ops) {
|
|
@@ -34,9 +35,8 @@ export async function applyImage(source, target, ops) {
|
|
|
34
35
|
});
|
|
35
36
|
return t;
|
|
36
37
|
};
|
|
37
|
-
const module = device
|
|
38
|
-
const
|
|
39
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
38
|
+
const { module, messages } = await createShaderModuleChecked(device, shader(), 'image-filters');
|
|
39
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
40
40
|
if (errors.length > 0)
|
|
41
41
|
throw new CompileError('image-filters', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
42
42
|
const pipeline = device.createRenderPipeline({
|
package/dist/packs/life/boids.js
CHANGED
|
@@ -2,6 +2,7 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { createNeighborGrid } from "../grid/index.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const WG = 64;
|
|
@@ -47,9 +48,8 @@ export async function boids(config = {}) {
|
|
|
47
48
|
device.queue.writeBuffer(uniform, 0, b);
|
|
48
49
|
};
|
|
49
50
|
writeUniform();
|
|
50
|
-
const module = device
|
|
51
|
-
const
|
|
52
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
51
|
+
const { module, messages } = await createShaderModuleChecked(device, boidsWgsl(size), 'boids');
|
|
52
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
53
53
|
if (errors.length > 0)
|
|
54
54
|
throw new CompileError('boids', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
55
55
|
const neighborGrid = await createNeighborGrid({ count: N, worldHalf: 1.0, cellSize: perception });
|
|
@@ -2,6 +2,7 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { MapRenderer } from "./map.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const AWG = 32; // count(u32) pad(u32) + sensorAngle, sensorDist, turnAngle, step, deposit, worldHalf (f32×6) = 32
|
|
@@ -51,9 +52,8 @@ export async function physarum(config = {}) {
|
|
|
51
52
|
// decay 写进 diffuse kernel 的第二个 uniform?并成 16B:w,h,decayFrac,pad
|
|
52
53
|
device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
|
|
53
54
|
const compile = async (code, label) => {
|
|
54
|
-
const m = device
|
|
55
|
-
const
|
|
56
|
-
const errors = info.messages.filter((x) => x.type === 'error');
|
|
55
|
+
const { module: m, messages } = await createShaderModuleChecked(device, code, label);
|
|
56
|
+
const errors = messages.filter((x) => x.type === 'error');
|
|
57
57
|
if (errors.length > 0)
|
|
58
58
|
throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
|
|
59
59
|
return m;
|
|
@@ -2,6 +2,7 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { MapRenderer } from "./map.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const PRESETS = {
|
|
@@ -58,9 +59,8 @@ export async function turing(config = {}) {
|
|
|
58
59
|
device.queue.writeBuffer(uniform, 0, buf);
|
|
59
60
|
};
|
|
60
61
|
writeUniform();
|
|
61
|
-
const module = device
|
|
62
|
-
const
|
|
63
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
62
|
+
const { module, messages } = await createShaderModuleChecked(device, updateWgsl(), 'turing-update');
|
|
63
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
64
64
|
if (errors.length > 0)
|
|
65
65
|
throw new CompileError('turing-update', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
66
66
|
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
@@ -2,6 +2,7 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError, createComputePipelineChecked } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { resolveConfig } from "./config.js";
|
|
6
7
|
import { mulberry32, resolveMatrix, hashSeed } from "./presets.js";
|
|
7
8
|
import { simWgsl, WORKGROUP } from "./wgsl.js";
|
|
@@ -61,9 +62,8 @@ export async function particles(config = {}) {
|
|
|
61
62
|
writeUniform(cfg.dt);
|
|
62
63
|
// —— 着色器模块(编译错误 → 行号映射) ——
|
|
63
64
|
const compile = async (code, label) => {
|
|
64
|
-
const module = device
|
|
65
|
-
const
|
|
66
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
65
|
+
const { module, messages } = await createShaderModuleChecked(device, code, label);
|
|
66
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
67
67
|
if (errors.length > 0)
|
|
68
68
|
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
69
69
|
return module;
|
package/package.json
CHANGED