wgpu-kit 0.9.10 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/core/buffer.d.ts +1 -1
- package/dist/core/buffer.js +109 -0
- package/dist/core/context.d.ts +2 -0
- package/dist/core/context.js +62 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/kernel.js +197 -0
- package/dist/core/layout.d.ts +5 -1
- package/dist/core/layout.js +69 -0
- package/dist/core/pingpong.js +61 -0
- package/dist/core/raw.js +38 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +11 -484
- package/dist/interop/three.js +29 -0
- package/dist/media.js +70 -0
- package/dist/observe.d.ts +19 -0
- package/dist/observe.js +76 -0
- package/dist/packs/fields/index.js +230 -0
- package/dist/packs/image/index.js +250 -0
- package/dist/packs/life/boids.js +367 -0
- package/dist/packs/life/index.js +8 -0
- package/dist/packs/life/map.js +110 -0
- package/dist/packs/life/physarum.js +228 -0
- package/dist/packs/life/tentacles.js +238 -0
- package/dist/packs/life/turing.js +203 -0
- package/dist/packs/particles/config.d.ts +1 -0
- package/dist/packs/particles/config.js +36 -0
- package/dist/packs/particles/grid.js +248 -0
- package/dist/packs/particles/index.js +361 -0
- package/dist/packs/particles/presets.js +75 -0
- package/dist/packs/particles/render.js +116 -0
- package/dist/packs/particles/wgsl.js +175 -0
- package/dist/react/index.js +36 -0
- package/dist/vite.js +27 -28
- package/package.json +1 -1
- package/dist/fields.js +0 -585
- package/dist/image.js +0 -318
- package/dist/life.js +0 -1407
- package/dist/particles.js +0 -1245
- package/dist/react.js +0 -1289
- package/dist/three.js +0 -33
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm i wgpu-kit
|
|
|
16
16
|
**100,000 particles in 5 lines:**
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
import { particles } from 'wgpu-kit';
|
|
19
|
+
import { particles } from 'wgpu-kit'; // 或 'wgpu-kit/particles'
|
|
20
20
|
|
|
21
21
|
const sim = await particles({ count: 100_000, forces: 'cells' });
|
|
22
22
|
await sim.attach(canvas);
|
|
@@ -75,6 +75,13 @@ dispatch, readbacks, error line-mapping — all handled by the library.
|
|
|
75
75
|
| neighborhood algorithms | grid ~O(N); 8.5× faster than brute force at 66k | same-session A/B |
|
|
76
76
|
| bundle size | core gzip 5.5kB; +particles 10.3kB | gzip |
|
|
77
77
|
|
|
78
|
+
## Verification
|
|
79
|
+
|
|
80
|
+
41+ automated probes run on a real GPU via a headless Chromium harness
|
|
81
|
+
(included under `tests/` + `scripts/verify.mjs`) — including a physics
|
|
82
|
+
equivalence regression that fails if the neighborhood algorithms ever
|
|
83
|
+
produce divergent structures.
|
|
84
|
+
|
|
78
85
|
## Three design rules
|
|
79
86
|
|
|
80
87
|
1. **Level-2 works in 5 minutes, level-1 has no ceiling** — `rawKernel` and
|
package/dist/core/buffer.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare class Buffer<K extends ScalarKind = ScalarKind> {
|
|
|
13
13
|
readonly gpuBuffer: GPUBuffer;
|
|
14
14
|
private constructor();
|
|
15
15
|
static create<K extends ScalarKind>(kind: K, length: number): Promise<Buffer<K>>;
|
|
16
|
-
/** 校验并写入(CPU → GPU) */
|
|
16
|
+
/** 校验并写入(CPU → GPU);vec3f 等 stride≠size 的类型自动补 padding */
|
|
17
17
|
write(data: NumArray): void;
|
|
18
18
|
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
19
19
|
read(): Promise<NumArray>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { TYPES } from "./layout.js";
|
|
2
|
+
import { GpuContext } from "./context.js";
|
|
3
|
+
import { UsageError } from "./errors.js";
|
|
4
|
+
const TYPED_CTORS = {
|
|
5
|
+
Float32Array, Int32Array, Uint32Array,
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 显存数组的类型化封装。替用户做三件事:
|
|
9
|
+
* ① 算字节数与 usage;② write() 校验长度;③ read() 内置 staging buffer(mapAsync 陷阱全包掉)。
|
|
10
|
+
* `gpuBuffer` 原生句柄永远可取(章程原则 1:逃生舱常开)。
|
|
11
|
+
*/
|
|
12
|
+
export class Buffer {
|
|
13
|
+
kind;
|
|
14
|
+
length;
|
|
15
|
+
gpuBuffer;
|
|
16
|
+
#ctx;
|
|
17
|
+
#byteLength;
|
|
18
|
+
#stride;
|
|
19
|
+
#staging = null;
|
|
20
|
+
constructor(ctx, kind, length, gpuBuffer) {
|
|
21
|
+
this.#ctx = ctx;
|
|
22
|
+
this.kind = kind;
|
|
23
|
+
this.length = length;
|
|
24
|
+
this.gpuBuffer = gpuBuffer;
|
|
25
|
+
this.#byteLength = length * TYPES[kind].stride;
|
|
26
|
+
this.#stride = TYPES[kind].stride;
|
|
27
|
+
}
|
|
28
|
+
static async create(kind, length) {
|
|
29
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
30
|
+
throw new UsageError(`Buffer 长度必须是正整数,收到: ${String(length)}`);
|
|
31
|
+
}
|
|
32
|
+
const def = TYPES[kind];
|
|
33
|
+
if (!def)
|
|
34
|
+
throw new UsageError(`未知类型 "${String(kind)}",可用: ${Object.keys(TYPES).join(', ')}`);
|
|
35
|
+
const ctx = await GpuContext.get();
|
|
36
|
+
const gpuBuffer = ctx.device.createBuffer({
|
|
37
|
+
size: length * def.stride,
|
|
38
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
39
|
+
label: `wgpu-kit Buffer<${kind}>[${length}]`,
|
|
40
|
+
});
|
|
41
|
+
return new Buffer(ctx, kind, length, gpuBuffer);
|
|
42
|
+
}
|
|
43
|
+
/** 校验并写入(CPU → GPU);vec3f 等 stride≠size 的类型自动补 padding */
|
|
44
|
+
write(data) {
|
|
45
|
+
const def = TYPES[this.kind];
|
|
46
|
+
const ctor = TYPED_CTORS[def.typed];
|
|
47
|
+
if (!(data instanceof ctor)) {
|
|
48
|
+
throw new UsageError(`Buffer<${this.kind}>.write 需要 ${def.typed},收到 ${data.constructor?.name ?? typeof data}`);
|
|
49
|
+
}
|
|
50
|
+
const expected = this.length * def.comps;
|
|
51
|
+
if (data.length !== expected) {
|
|
52
|
+
throw new UsageError(`Buffer<${this.kind}>[${this.length}].write 需要 ${expected} 个分量,收到 ${data.length}`);
|
|
53
|
+
}
|
|
54
|
+
if (def.stride === def.size || def.comps === 1) {
|
|
55
|
+
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, data);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// stride ≠ size(vec3f):逐元素补 padding 到 GPU 布局
|
|
59
|
+
const comps = def.comps;
|
|
60
|
+
const per = def.stride / 4;
|
|
61
|
+
const gpu = new Float32Array(this.length * per);
|
|
62
|
+
for (let i = 0; i < this.length; i++) {
|
|
63
|
+
for (let c = 0; c < comps; c++)
|
|
64
|
+
gpu[i * per + c] = data[i * comps + c];
|
|
65
|
+
}
|
|
66
|
+
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, gpu);
|
|
67
|
+
}
|
|
68
|
+
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
69
|
+
async read() {
|
|
70
|
+
const def = TYPES[this.kind];
|
|
71
|
+
if (!this.#staging) {
|
|
72
|
+
this.#staging = this.#ctx.device.createBuffer({
|
|
73
|
+
size: this.#byteLength,
|
|
74
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
75
|
+
label: `wgpu-kit staging[${this.length}]`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const enc = this.#ctx.device.createCommandEncoder();
|
|
79
|
+
enc.copyBufferToBuffer(this.gpuBuffer, 0, this.#staging, 0, this.#byteLength);
|
|
80
|
+
this.#ctx.device.queue.submit([enc.finish()]);
|
|
81
|
+
await this.#staging.mapAsync(GPUMapMode.READ);
|
|
82
|
+
const ab = this.#staging.getMappedRange().slice(0);
|
|
83
|
+
this.#staging.unmap();
|
|
84
|
+
if (def.stride === def.size || def.comps === 1) {
|
|
85
|
+
if (def.typed === 'Float32Array')
|
|
86
|
+
return new Float32Array(ab);
|
|
87
|
+
if (def.typed === 'Int32Array')
|
|
88
|
+
return new Int32Array(ab);
|
|
89
|
+
return new Uint32Array(ab);
|
|
90
|
+
}
|
|
91
|
+
// stride ≠ size(vec3f):剥掉 GPU 布局的 padding
|
|
92
|
+
const comps = def.comps;
|
|
93
|
+
const per = def.stride / 4;
|
|
94
|
+
const src = new Float32Array(ab);
|
|
95
|
+
const out = new Float32Array(this.length * comps);
|
|
96
|
+
for (let i = 0; i < this.length; i++) {
|
|
97
|
+
for (let c = 0; c < comps; c++)
|
|
98
|
+
out[i * comps + c] = src[i * per + c];
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
destroy() {
|
|
103
|
+
if (this.#staging) {
|
|
104
|
+
this.#staging.destroy();
|
|
105
|
+
this.#staging = null;
|
|
106
|
+
}
|
|
107
|
+
this.gpuBuffer.destroy();
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/core/context.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class GpuContext {
|
|
|
7
7
|
readonly device: GPUDevice;
|
|
8
8
|
readonly adapterInfo: string;
|
|
9
9
|
private constructor();
|
|
10
|
+
/** 仅供设备丢失自动重建(observe.watchDevice)使用:重置单例 */
|
|
11
|
+
static resetForTests(): void;
|
|
10
12
|
static get(): Promise<GpuContext>;
|
|
11
13
|
/** device lost 时 reject;调用方可 await 做清理/提示 */
|
|
12
14
|
get lost(): Promise<GPUDeviceLostInfo>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { WebGPUUnavailableError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* 设备上下文:全库单例,惰性申请 adapter/device。
|
|
4
|
+
* 峰值性能偏好;失败给人话报错;device lost 以 promise 形式暴露。
|
|
5
|
+
*/
|
|
6
|
+
export class GpuContext {
|
|
7
|
+
device;
|
|
8
|
+
adapterInfo;
|
|
9
|
+
constructor(device, adapterInfo) {
|
|
10
|
+
this.device = device;
|
|
11
|
+
this.adapterInfo = adapterInfo;
|
|
12
|
+
}
|
|
13
|
+
static #singleton = null;
|
|
14
|
+
/** 仅供设备丢失自动重建(observe.watchDevice)使用:重置单例 */
|
|
15
|
+
static resetForTests() {
|
|
16
|
+
GpuContext.#singleton = null;
|
|
17
|
+
}
|
|
18
|
+
static get() {
|
|
19
|
+
if (!GpuContext.#singleton) {
|
|
20
|
+
GpuContext.#singleton = GpuContext.#create().catch((e) => {
|
|
21
|
+
GpuContext.#singleton = null; // 允许环境修复后重试
|
|
22
|
+
throw e;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return GpuContext.#singleton;
|
|
26
|
+
}
|
|
27
|
+
static async #create() {
|
|
28
|
+
if (typeof navigator === 'undefined' || !('gpu' in navigator) || !navigator.gpu) {
|
|
29
|
+
throw new WebGPUUnavailableError('navigator.gpu 不存在');
|
|
30
|
+
}
|
|
31
|
+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
32
|
+
if (!adapter)
|
|
33
|
+
throw new WebGPUUnavailableError('requestAdapter() 返回 null');
|
|
34
|
+
const info = adapter.info;
|
|
35
|
+
const label = info
|
|
36
|
+
? [info.vendor, info.architecture, info.description].filter(Boolean).join(' / ') || 'unknown'
|
|
37
|
+
: 'unknown';
|
|
38
|
+
// 按适配器能力申请存储缓冲上限:grid 力核等大 binding 数内核需要 >8,
|
|
39
|
+
// 而默认上限是 8(有头真机会直接判管线无效——v0.9.5 黑屏事故的根因)
|
|
40
|
+
const requiredLimits = {};
|
|
41
|
+
const want = [
|
|
42
|
+
'maxStorageBuffersPerShaderStage',
|
|
43
|
+
'maxStorageBuffersInVertexStage',
|
|
44
|
+
'maxStorageBufferBindingSize',
|
|
45
|
+
];
|
|
46
|
+
for (const key of want) {
|
|
47
|
+
const supported = adapter.limits[key];
|
|
48
|
+
if (typeof supported === 'number')
|
|
49
|
+
requiredLimits[key] = supported;
|
|
50
|
+
}
|
|
51
|
+
const device = await adapter.requestDevice({ label: 'wgpu-kit', requiredLimits });
|
|
52
|
+
return new GpuContext(device, label);
|
|
53
|
+
}
|
|
54
|
+
/** device lost 时 reject;调用方可 await 做清理/提示 */
|
|
55
|
+
get lost() {
|
|
56
|
+
return this.device.lost;
|
|
57
|
+
}
|
|
58
|
+
/** 等待队列中已提交的全部 GPU 工作完成(测试/读回前同步用) */
|
|
59
|
+
async sync() {
|
|
60
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** 错误体系:所有错误说人话,给出定位与修复建议(章程原则 3)。 */
|
|
2
|
+
export class WgpuKitError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** 环境无 WebGPU / 拿不到 adapter。 */
|
|
9
|
+
export class WebGPUUnavailableError extends WgpuKitError {
|
|
10
|
+
constructor(reason) {
|
|
11
|
+
super(`当前环境不可用 WebGPU: ${reason}\n` +
|
|
12
|
+
' 排查:① 浏览器需 Chrome/Edge 113+ 或 Safari 18+;② 无头环境需开启 WebGPU;③ 检查 GPU 驱动与硬件加速设置。\n' +
|
|
13
|
+
' 可用 navigator.gpu 是否存在快速判断。');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** WGSL 编译错误,行号已映射回用户代码。 */
|
|
17
|
+
export class CompileError extends WgpuKitError {
|
|
18
|
+
constructor(kernelName, messages, userCodeOffset) {
|
|
19
|
+
const mapped = messages
|
|
20
|
+
.map((m) => {
|
|
21
|
+
const userLine = m.line - userCodeOffset;
|
|
22
|
+
const where = userLine > 0 ? `用户代码第 ${userLine} 行` : `生成代码第 ${m.line} 行(库的问题,欢迎报 issue)`;
|
|
23
|
+
return ` ${where}: ${m.msg}`;
|
|
24
|
+
})
|
|
25
|
+
.join('\n');
|
|
26
|
+
super(`kernel "${kernelName}" WGSL 编译失败:\n${mapped}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** 调用方使用不当(缺资源/类型不匹配/长度不一致等)。 */
|
|
30
|
+
export class UsageError extends WgpuKitError {
|
|
31
|
+
}
|
|
32
|
+
/** 创建 compute 管线并用 pushErrorScope 捕获异步校验错误(超限等),把"黑屏刷屏"变成显式报错 */
|
|
33
|
+
export async function createComputePipelineChecked(device, module, label, entryPoint = 'main') {
|
|
34
|
+
device.pushErrorScope('validation');
|
|
35
|
+
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint } });
|
|
36
|
+
const err = await device.popErrorScope();
|
|
37
|
+
if (err) {
|
|
38
|
+
throw new WgpuKitError(`compute 管线 "${label}" 创建失败: ${err.message}
|
|
39
|
+
常见原因:storage buffer 数超过每阶段上限(可向本库提 issue 申请 limits 支持)`);
|
|
40
|
+
}
|
|
41
|
+
return pipeline;
|
|
42
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { planUniform, packUniformInto, TYPES } from "./layout.js";
|
|
2
|
+
import { GpuContext } from "./context.js";
|
|
3
|
+
import { Buffer } from "./buffer.js";
|
|
4
|
+
import { CompileError, UsageError } from "./errors.js";
|
|
5
|
+
const RESERVED = new Set(['count']);
|
|
6
|
+
/** GPUBuffer 的稳定数字身份(WeakMap 分配,用于 bind group 缓存键) */
|
|
7
|
+
let nextBufferId = 0;
|
|
8
|
+
const bufferIds = new WeakMap();
|
|
9
|
+
function bufId(b) {
|
|
10
|
+
let id = bufferIds.get(b);
|
|
11
|
+
if (id === undefined) {
|
|
12
|
+
id = ++nextBufferId;
|
|
13
|
+
bufferIds.set(b, id);
|
|
14
|
+
}
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
/** 纯函数:规范校验 + WGSL 代码生成。单测直接覆盖,不碰 GPU。 */
|
|
18
|
+
export function generateElementKernel(spec) {
|
|
19
|
+
const name = spec.name ?? 'kernel';
|
|
20
|
+
const workgroupSize = spec.workgroupSize ?? 64;
|
|
21
|
+
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
22
|
+
throw new UsageError(`workgroupSize 必须在 1..512,收到: ${String(workgroupSize)}`);
|
|
23
|
+
}
|
|
24
|
+
const state = Object.entries(spec.state ?? {});
|
|
25
|
+
const inputs = Object.entries(spec.inputs ?? {});
|
|
26
|
+
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
27
|
+
if (state.length + inputs.length === 0) {
|
|
28
|
+
throw new UsageError(`elementKernel "${name}" 至少需要一个 state 或 inputs 字段`);
|
|
29
|
+
}
|
|
30
|
+
for (const [uName] of uniforms) {
|
|
31
|
+
if (RESERVED.has(uName))
|
|
32
|
+
throw new UsageError(`uniform 名 "${uName}" 是保留名(count 由库自动注入)`);
|
|
33
|
+
}
|
|
34
|
+
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
35
|
+
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
36
|
+
throw new UsageError(`elementKernel "${name}" 的 state/inputs/uniforms 存在重名字段`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof spec.code !== 'string' || spec.code.trim().length === 0) {
|
|
39
|
+
throw new UsageError(`elementKernel "${name}" 缺少 code(用户 WGSL 函数)`);
|
|
40
|
+
}
|
|
41
|
+
const uniformEntries = [...uniforms, ['count', 'u32']];
|
|
42
|
+
const uniformLayout = planUniform(uniformEntries);
|
|
43
|
+
// —— 生成 WGSL:头部(声明) + main + 用户代码 ——
|
|
44
|
+
const header = [];
|
|
45
|
+
header.push('// 由 wgpu-kit elementKernel 生成');
|
|
46
|
+
header.push('struct Params {');
|
|
47
|
+
for (const [n, k] of uniformEntries)
|
|
48
|
+
header.push(` ${n}: ${TYPES[k].wgsl},`);
|
|
49
|
+
header.push('};');
|
|
50
|
+
header.push('@group(0) @binding(0) var<uniform> params: Params;');
|
|
51
|
+
let binding = 1;
|
|
52
|
+
for (const [n, k] of state)
|
|
53
|
+
header.push(`@group(0) @binding(${binding++}) var<storage, read_write> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
54
|
+
for (const [n, k] of inputs)
|
|
55
|
+
header.push(`@group(0) @binding(${binding++}) var<storage, read> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
56
|
+
header.push('');
|
|
57
|
+
header.push(`@compute @workgroup_size(${workgroupSize})`);
|
|
58
|
+
header.push('fn main(@builtin(global_invocation_id) gid: vec3u) {');
|
|
59
|
+
header.push(' let idx = gid.x;');
|
|
60
|
+
header.push(' if (idx >= params.count) { return; }');
|
|
61
|
+
const uniformArgs = uniforms.map(([n]) => `params.${n}`).join(', ');
|
|
62
|
+
// 用户函数固定命名 userFn —— 唯一约定,杜绝调用名对不上的脆弱性
|
|
63
|
+
header.push(` userFn(idx${uniformArgs ? ', ' + uniformArgs : ''});`);
|
|
64
|
+
header.push('}');
|
|
65
|
+
const userCodeLineOffset = header.length; // 1-based 行号:用户代码从 offset+1 行开始
|
|
66
|
+
const source = [...header, spec.code].join('\n');
|
|
67
|
+
return {
|
|
68
|
+
normalized: { name, workgroupSize, state, inputs, uniforms, code: spec.code },
|
|
69
|
+
source,
|
|
70
|
+
uniformLayout,
|
|
71
|
+
userCodeLineOffset,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function elementKernel(spec) {
|
|
75
|
+
const first = generateElementKernel(spec);
|
|
76
|
+
const uniformLayout = first.uniformLayout;
|
|
77
|
+
let normalized = first.normalized;
|
|
78
|
+
let source = first.source;
|
|
79
|
+
let userCodeLineOffset = first.userCodeLineOffset;
|
|
80
|
+
const uniformBufferName = `${normalized.name}:uniform`;
|
|
81
|
+
// 构造期预展开:稳态 run() 每帧零分配(评审:两次展开 + O(字段²) find + 字符串 key)
|
|
82
|
+
const orderedFields = [
|
|
83
|
+
...normalized.state.map(([k, t]) => ({ key: k, kind: t })),
|
|
84
|
+
...normalized.inputs.map(([k, t]) => ({ key: k, kind: t })),
|
|
85
|
+
];
|
|
86
|
+
const expectedKinds = new Map(orderedFields.map((f) => [f.key, f.kind]));
|
|
87
|
+
const sharedPack = new ArrayBuffer(uniformLayout.size); // 复用打包缓冲(writeBuffer 会拷贝)
|
|
88
|
+
let pipelinePromise = null;
|
|
89
|
+
let cachedCtx = null; // 首帧后缓存为普通引用
|
|
90
|
+
const bindGroupCache = new Map();
|
|
91
|
+
let uniformBuffer = null;
|
|
92
|
+
const compilePipeline = async () => {
|
|
93
|
+
const ctx = await GpuContext.get();
|
|
94
|
+
const device = ctx.device;
|
|
95
|
+
const module = device.createShaderModule({ code: source, label: normalized.name });
|
|
96
|
+
// 捕获编译错误并映射行号
|
|
97
|
+
const info = await module.getCompilationInfo();
|
|
98
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
99
|
+
if (errors.length > 0) {
|
|
100
|
+
throw new CompileError(normalized.name, errors.map((m) => ({ line: m.lineNum, msg: m.message })), userCodeLineOffset);
|
|
101
|
+
}
|
|
102
|
+
return device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
103
|
+
};
|
|
104
|
+
async function getPipeline() {
|
|
105
|
+
if (!pipelinePromise) {
|
|
106
|
+
pipelinePromise = compilePipeline().catch((e) => { pipelinePromise = null; throw e; });
|
|
107
|
+
}
|
|
108
|
+
return pipelinePromise;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
get name() { return normalized.name; },
|
|
112
|
+
get source() { return source; },
|
|
113
|
+
get uniformLayout() { return first.uniformLayout; },
|
|
114
|
+
get workgroupSize() { return normalized.workgroupSize; },
|
|
115
|
+
async replace(code) {
|
|
116
|
+
const regen = generateElementKernel({ ...spec, code });
|
|
117
|
+
// 先编译后切换:新代码编译失败则保持旧版不动
|
|
118
|
+
const savedSource = source;
|
|
119
|
+
const savedOffset = userCodeLineOffset;
|
|
120
|
+
source = regen.source;
|
|
121
|
+
userCodeLineOffset = regen.userCodeLineOffset;
|
|
122
|
+
try {
|
|
123
|
+
const p = await compilePipeline();
|
|
124
|
+
pipelinePromise = Promise.resolve(p);
|
|
125
|
+
bindGroupCache.clear();
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
source = savedSource;
|
|
129
|
+
userCodeLineOffset = savedOffset;
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
async run(resources, uniforms = {}) {
|
|
134
|
+
if (!cachedCtx)
|
|
135
|
+
cachedCtx = await GpuContext.get();
|
|
136
|
+
const device = cachedCtx.device;
|
|
137
|
+
const pipeline = await getPipeline();
|
|
138
|
+
// —— 资源校验 + 有序收集(预展开清单,稳态零分配) ——
|
|
139
|
+
const ordered = [];
|
|
140
|
+
let count = -1;
|
|
141
|
+
let firstKey = '';
|
|
142
|
+
for (let i = 0; i < orderedFields.length; i++) {
|
|
143
|
+
const f = orderedFields[i];
|
|
144
|
+
const buf = resources[f.key];
|
|
145
|
+
if (!buf)
|
|
146
|
+
throw new UsageError(`kernel "${normalized.name}".run 缺少资源 "${f.key}"`);
|
|
147
|
+
const want = expectedKinds.get(f.key);
|
|
148
|
+
if (buf.kind !== want) {
|
|
149
|
+
throw new UsageError(`资源 "${f.key}" 类型不匹配: 需要 ${want},收到 ${buf.kind}`);
|
|
150
|
+
}
|
|
151
|
+
if (count === -1) {
|
|
152
|
+
count = buf.length;
|
|
153
|
+
firstKey = f.key;
|
|
154
|
+
}
|
|
155
|
+
else if (buf.length !== count) {
|
|
156
|
+
throw new UsageError(`资源 "${f.key}" 长度 ${buf.length} 与 "${firstKey}" 的 ${count} 不一致`);
|
|
157
|
+
}
|
|
158
|
+
ordered.push(buf);
|
|
159
|
+
}
|
|
160
|
+
// —— uniform 打包上传 ——
|
|
161
|
+
packUniformInto(sharedPack, uniformLayout, { ...uniforms, count });
|
|
162
|
+
if (!uniformBuffer) {
|
|
163
|
+
uniformBuffer = device.createBuffer({
|
|
164
|
+
size: uniformLayout.size,
|
|
165
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
166
|
+
label: uniformBufferName,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
device.queue.writeBuffer(uniformBuffer, 0, sharedPack);
|
|
170
|
+
// —— bind group(按 buffer 身份缓存) ——
|
|
171
|
+
let cacheKey = 0;
|
|
172
|
+
for (let i = 0; i < ordered.length; i++)
|
|
173
|
+
cacheKey = (cacheKey * 31 + bufId(ordered[i].gpuBuffer)) | 0;
|
|
174
|
+
let bg = bindGroupCache.get(cacheKey);
|
|
175
|
+
if (!bg) {
|
|
176
|
+
const entries = [{ binding: 0, resource: { buffer: uniformBuffer } }];
|
|
177
|
+
ordered.forEach((buffer, i) => entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } }));
|
|
178
|
+
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
179
|
+
bindGroupCache.set(cacheKey, bg);
|
|
180
|
+
}
|
|
181
|
+
// —— 编码提交 ——
|
|
182
|
+
const enc = device.createCommandEncoder();
|
|
183
|
+
const pass = enc.beginComputePass();
|
|
184
|
+
pass.setPipeline(pipeline);
|
|
185
|
+
pass.setBindGroup(0, bg);
|
|
186
|
+
pass.dispatchWorkgroups(Math.ceil(count / normalized.workgroupSize));
|
|
187
|
+
pass.end();
|
|
188
|
+
device.queue.submit([enc.finish()]);
|
|
189
|
+
},
|
|
190
|
+
destroy() {
|
|
191
|
+
pipelinePromise = null;
|
|
192
|
+
bindGroupCache.clear();
|
|
193
|
+
uniformBuffer?.destroy();
|
|
194
|
+
uniformBuffer = null;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** WGSL 类型布局表:尺寸、对齐、组件数、对应的 TypedArray。库替用户算字节数的根据。 */
|
|
2
2
|
export type ScalarKind = 'f32' | 'i32' | 'u32' | 'vec2f' | 'vec2i' | 'vec2u' | 'vec3f' | 'vec4f';
|
|
3
3
|
export interface TypeDef {
|
|
4
|
-
/** 字节数 */
|
|
4
|
+
/** 字节数(uniform/标量语义) */
|
|
5
5
|
readonly size: number;
|
|
6
|
+
/** **storage 数组元素步长**(WGSL 规则:array<vec3f> 步长 16,与 size 12 不同) */
|
|
7
|
+
readonly stride: number;
|
|
6
8
|
/** 字节对齐(WGSL 规则:vec3 对齐到 16) */
|
|
7
9
|
readonly align: number;
|
|
8
10
|
/** 分量数 */
|
|
@@ -30,3 +32,5 @@ export interface UniformLayout {
|
|
|
30
32
|
export declare function planUniform(entries: ReadonlyArray<readonly [string, ScalarKind]>): UniformLayout;
|
|
31
33
|
/** 把标量值按布局写进 ArrayBuffer;缺失字段报错,多余字段忽略前给出字段清单便于排错 */
|
|
32
34
|
export declare function packUniform(layout: UniformLayout, values: Readonly<Record<string, number>>): ArrayBuffer;
|
|
35
|
+
/** packUniform 的零分配变体:写入调用方提供的缓冲(每帧路径用,避免 new ArrayBuffer) */
|
|
36
|
+
export declare function packUniformInto(target: ArrayBuffer, layout: UniformLayout, values: Readonly<Record<string, number>>): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const TYPES = {
|
|
2
|
+
f32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Float32Array', wgsl: 'f32' },
|
|
3
|
+
i32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Int32Array', wgsl: 'i32' },
|
|
4
|
+
u32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Uint32Array', wgsl: 'u32' },
|
|
5
|
+
vec2f: { size: 8, stride: 8, align: 8, comps: 2, typed: 'Float32Array', wgsl: 'vec2f' },
|
|
6
|
+
vec2i: { size: 8, stride: 8, align: 8, comps: 2, typed: 'Int32Array', wgsl: 'vec2i' },
|
|
7
|
+
vec2u: { size: 8, stride: 8, align: 8, comps: 2, typed: 'Uint32Array', wgsl: 'vec2u' },
|
|
8
|
+
vec3f: { size: 12, stride: 16, align: 16, comps: 3, typed: 'Float32Array', wgsl: 'vec3f' },
|
|
9
|
+
vec4f: { size: 16, stride: 16, align: 16, comps: 4, typed: 'Float32Array', wgsl: 'vec4f' },
|
|
10
|
+
};
|
|
11
|
+
export function alignTo(offset, align) {
|
|
12
|
+
return Math.ceil(offset / align) * align;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 按声明顺序规划 uniform 布局。声明顺序 = WGSL struct 成员顺序 = 用户函数参数顺序,
|
|
16
|
+
* 三者由库保证一致,不给用户对齐自由(对齐是库的事)。
|
|
17
|
+
*/
|
|
18
|
+
export function planUniform(entries) {
|
|
19
|
+
const fields = [];
|
|
20
|
+
let cursor = 0;
|
|
21
|
+
for (const [name, kind] of entries) {
|
|
22
|
+
const def = TYPES[kind];
|
|
23
|
+
cursor = alignTo(cursor, def.align);
|
|
24
|
+
fields.push({ name, kind, offset: cursor });
|
|
25
|
+
cursor += def.size;
|
|
26
|
+
}
|
|
27
|
+
return { fields, size: alignTo(Math.max(cursor, 1), 16) };
|
|
28
|
+
}
|
|
29
|
+
const PACKERS = {
|
|
30
|
+
f32: (v, o, x) => v.setFloat32(o, x, true),
|
|
31
|
+
i32: (v, o, x) => v.setInt32(o, x, true),
|
|
32
|
+
u32: (v, o, x) => v.setUint32(o, x, true),
|
|
33
|
+
};
|
|
34
|
+
/** 把标量值按布局写进 ArrayBuffer;缺失字段报错,多余字段忽略前给出字段清单便于排错 */
|
|
35
|
+
export function packUniform(layout, values) {
|
|
36
|
+
const buf = new ArrayBuffer(layout.size);
|
|
37
|
+
const view = new DataView(buf);
|
|
38
|
+
for (const f of layout.fields) {
|
|
39
|
+
const pack = PACKERS[f.kind];
|
|
40
|
+
if (!pack) {
|
|
41
|
+
throw new Error(`uniform 字段 ${f.name} 的类型 ${f.kind} 暂不支持(当前仅支持标量)`);
|
|
42
|
+
}
|
|
43
|
+
const v = values[f.name];
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
throw new Error(`缺少 uniform 值: ${f.name}`);
|
|
46
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
47
|
+
throw new Error(`uniform 值 ${f.name} 必须是有限数字,收到: ${String(v)}`);
|
|
48
|
+
}
|
|
49
|
+
pack(view, f.offset, v);
|
|
50
|
+
}
|
|
51
|
+
return buf;
|
|
52
|
+
}
|
|
53
|
+
/** packUniform 的零分配变体:写入调用方提供的缓冲(每帧路径用,避免 new ArrayBuffer) */
|
|
54
|
+
export function packUniformInto(target, layout, values) {
|
|
55
|
+
const view = new DataView(target);
|
|
56
|
+
for (const f of layout.fields) {
|
|
57
|
+
const pack = PACKERS[f.kind];
|
|
58
|
+
if (!pack) {
|
|
59
|
+
throw new Error(`uniform 字段 ${f.name} 的类型 ${f.kind} 暂不支持(当前仅支持标量)`);
|
|
60
|
+
}
|
|
61
|
+
const v = values[f.name];
|
|
62
|
+
if (v === undefined)
|
|
63
|
+
throw new Error(`缺少 uniform 值: ${f.name}`);
|
|
64
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
65
|
+
throw new Error(`uniform 值 ${f.name} 必须是有限数字,收到: ${String(v)}`);
|
|
66
|
+
}
|
|
67
|
+
pack(view, f.offset, v);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Buffer } from "./buffer.js";
|
|
2
|
+
import {} from "./layout.js";
|
|
3
|
+
/**
|
|
4
|
+
* 双缓冲:模拟类 kernel 的绝对刚需(spike 中是 45 行仪式)。
|
|
5
|
+
* A/B 两组同构缓冲,读"当前侧"、写"另一侧",swap 一次完成帧间翻转。
|
|
6
|
+
* 泛型 K 保留字段名的字面量类型,Record 访问不会被 noUncheckedIndexedAccess 弄成 undefined。
|
|
7
|
+
*/
|
|
8
|
+
export class PingPong {
|
|
9
|
+
#sides;
|
|
10
|
+
#names;
|
|
11
|
+
#length;
|
|
12
|
+
#kinds;
|
|
13
|
+
#index = 0;
|
|
14
|
+
constructor(names, kinds, length, a, b) {
|
|
15
|
+
this.#names = names;
|
|
16
|
+
this.#kinds = kinds;
|
|
17
|
+
this.#length = length;
|
|
18
|
+
this.#sides = [a, b];
|
|
19
|
+
}
|
|
20
|
+
static async create(kinds, length) {
|
|
21
|
+
const names = Object.keys(kinds);
|
|
22
|
+
if (names.length === 0)
|
|
23
|
+
throw new Error('PingPong 至少需要一个字段');
|
|
24
|
+
const make = async () => {
|
|
25
|
+
const side = {};
|
|
26
|
+
for (const name of names)
|
|
27
|
+
side[name] = await Buffer.create(kinds[name], length);
|
|
28
|
+
return side;
|
|
29
|
+
};
|
|
30
|
+
return new PingPong(names, kinds, length, await make(), await make());
|
|
31
|
+
}
|
|
32
|
+
/** 当前帧的数据侧(渲染/读回用) */
|
|
33
|
+
get current() {
|
|
34
|
+
return this.#sides[this.#index];
|
|
35
|
+
}
|
|
36
|
+
/** 另一侧(kernel 写入目标) */
|
|
37
|
+
get other() {
|
|
38
|
+
return this.#sides[1 - this.#index];
|
|
39
|
+
}
|
|
40
|
+
/** 帧末翻转 */
|
|
41
|
+
swap() {
|
|
42
|
+
this.#index = 1 - this.#index;
|
|
43
|
+
}
|
|
44
|
+
/** 以 (写侧, 读侧) 调用 fn 后自动 swap 的语法糖 */
|
|
45
|
+
async runWith(fn) {
|
|
46
|
+
await fn(this.other, this.current);
|
|
47
|
+
this.swap();
|
|
48
|
+
}
|
|
49
|
+
destroy() {
|
|
50
|
+
for (const side of this.#sides)
|
|
51
|
+
for (const b of Object.values(side))
|
|
52
|
+
b.destroy();
|
|
53
|
+
}
|
|
54
|
+
/** 克隆一份同构 PingPong(同字段同长度) */
|
|
55
|
+
async clone() {
|
|
56
|
+
return PingPong.create(this.#kinds, this.#length);
|
|
57
|
+
}
|
|
58
|
+
get names() {
|
|
59
|
+
return this.#names;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/core/raw.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { GpuContext } from "./context.js";
|
|
2
|
+
import { CompileError, UsageError } from "./errors.js";
|
|
3
|
+
export function rawKernel(code, entryPoint = 'main', label = 'rawKernel') {
|
|
4
|
+
if (typeof code !== 'string' || code.trim().length === 0)
|
|
5
|
+
throw new UsageError('rawKernel 需要 WGSL 代码');
|
|
6
|
+
let pipelinePromise = null;
|
|
7
|
+
return {
|
|
8
|
+
async run(entries, workgroups) {
|
|
9
|
+
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
10
|
+
throw new UsageError(`rawKernel.run 的 workgroups 必须是正整数,收到 ${String(workgroups)}`);
|
|
11
|
+
}
|
|
12
|
+
const ctx = await GpuContext.get();
|
|
13
|
+
if (!pipelinePromise) {
|
|
14
|
+
pipelinePromise = (async () => {
|
|
15
|
+
const module = ctx.device.createShaderModule({ code, label });
|
|
16
|
+
const info = await module.getCompilationInfo();
|
|
17
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
pipelinePromise = null;
|
|
20
|
+
// 实测(Dawn/Edge 151):lineNum 已是 1-based
|
|
21
|
+
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
22
|
+
}
|
|
23
|
+
return ctx.device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint } });
|
|
24
|
+
})();
|
|
25
|
+
}
|
|
26
|
+
const pipeline = await pipelinePromise;
|
|
27
|
+
const bg = ctx.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
28
|
+
const enc = ctx.device.createCommandEncoder();
|
|
29
|
+
const pass = enc.beginComputePass();
|
|
30
|
+
pass.setPipeline(pipeline);
|
|
31
|
+
pass.setBindGroup(0, bg);
|
|
32
|
+
pass.dispatchWorkgroups(workgroups);
|
|
33
|
+
pass.end();
|
|
34
|
+
ctx.device.queue.submit([enc.finish()]);
|
|
35
|
+
},
|
|
36
|
+
destroy() { pipelinePromise = null; },
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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 { particles, type ParticlesSim } from './packs/particles/index.ts';
|
|
7
8
|
export type { ElementKernel, ElementKernelSpec };
|
|
8
|
-
export { TYPES, planUniform, packUniform, type ScalarKind } from './core/layout.ts';
|
|
9
|
+
export { TYPES, planUniform, packUniform, packUniformInto, type ScalarKind } from './core/layout.ts';
|
|
9
10
|
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from './core/errors.ts';
|