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 +79 -7
- package/README.md +80 -9
- package/dist/core/buffer.js +17 -5
- package/dist/core/codes.d.ts +35 -0
- package/dist/core/codes.js +34 -0
- package/dist/core/context.js +16 -4
- package/dist/core/errors.d.ts +18 -6
- package/dist/core/errors.js +28 -13
- package/dist/core/kernel.d.ts +2 -0
- package/dist/core/kernel.js +48 -17
- package/dist/core/layout.js +7 -6
- package/dist/core/pack.d.ts +37 -0
- package/dist/core/pack.js +35 -0
- package/dist/core/pingpong.js +1 -1
- package/dist/core/raw.js +6 -6
- 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/media.js +3 -3
- package/dist/observe.js +2 -2
- package/dist/packs/fields/index.d.ts +11 -1
- package/dist/packs/fields/index.js +27 -3
- package/dist/packs/grid/index.js +5 -5
- package/dist/packs/image/index.js +4 -4
- 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/config.js +4 -4
- package/dist/packs/particles/grid.js +39 -58
- package/dist/packs/particles/index.js +42 -56
- package/dist/packs/particles/presets.js +1 -1
- package/package.json +7 -2
package/dist/core/kernel.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { planUniform, packUniformInto, TYPES } from "./layout.js";
|
|
2
2
|
import { GpuContext } from "./context.js";
|
|
3
3
|
import { Buffer } from "./buffer.js";
|
|
4
|
-
import { CompileError, UsageError } from "./errors.js";
|
|
4
|
+
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "./shader.js";
|
|
5
6
|
const RESERVED = new Set(['count']);
|
|
6
7
|
/** GPUBuffer 的稳定数字身份(WeakMap 分配,用于 bind group 缓存键) */
|
|
7
8
|
let nextBufferId = 0;
|
|
@@ -19,27 +20,46 @@ export function generateElementKernel(spec) {
|
|
|
19
20
|
const name = spec.name ?? 'kernel';
|
|
20
21
|
const workgroupSize = spec.workgroupSize ?? 64;
|
|
21
22
|
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
22
|
-
throw new UsageError(`workgroupSize
|
|
23
|
+
throw new UsageError(ERR.WORKGROUP_SIZE, `workgroupSize must be in 1..512, got: ${String(workgroupSize)}`);
|
|
23
24
|
}
|
|
24
25
|
const state = Object.entries(spec.state ?? {});
|
|
25
26
|
const inputs = Object.entries(spec.inputs ?? {});
|
|
26
27
|
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
27
28
|
if (state.length + inputs.length === 0) {
|
|
28
|
-
throw new UsageError(`elementKernel "${name}"
|
|
29
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `elementKernel "${name}" requires at least one state or inputs field`);
|
|
29
30
|
}
|
|
30
31
|
for (const [uName] of uniforms) {
|
|
31
32
|
if (RESERVED.has(uName))
|
|
32
|
-
throw new UsageError(`uniform
|
|
33
|
+
throw new UsageError(ERR.USAGE, `uniform name "${uName}" is reserved (count is auto-injected by the library)`);
|
|
33
34
|
}
|
|
34
35
|
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
35
36
|
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
36
|
-
throw new UsageError(`elementKernel "${name}"
|
|
37
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" has duplicate field names across state/inputs/uniforms`);
|
|
37
38
|
}
|
|
38
39
|
if (typeof spec.code !== 'string' || spec.code.trim().length === 0) {
|
|
39
|
-
throw new UsageError(`elementKernel "${name}"
|
|
40
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" is missing code (user WGSL function)`);
|
|
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
|
},
|
|
@@ -143,17 +165,17 @@ export function elementKernel(spec) {
|
|
|
143
165
|
const f = orderedFields[i];
|
|
144
166
|
const buf = resources[f.key];
|
|
145
167
|
if (!buf)
|
|
146
|
-
throw new UsageError(`kernel "${normalized.name}".run
|
|
168
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `kernel "${normalized.name}".run is missing resource "${f.key}"`);
|
|
147
169
|
const want = expectedKinds.get(f.key);
|
|
148
170
|
if (buf.kind !== want) {
|
|
149
|
-
throw new UsageError(
|
|
171
|
+
throw new UsageError(ERR.RESOURCE_TYPE, `Resource "${f.key}" type mismatch: expected ${want}, got ${buf.kind}`);
|
|
150
172
|
}
|
|
151
173
|
if (count === -1) {
|
|
152
174
|
count = buf.length;
|
|
153
175
|
firstKey = f.key;
|
|
154
176
|
}
|
|
155
177
|
else if (buf.length !== count) {
|
|
156
|
-
throw new UsageError(
|
|
178
|
+
throw new UsageError(ERR.RESOURCE_LENGTH, `Resource "${f.key}" length ${buf.length} does not match "${firstKey}" length ${count}`);
|
|
157
179
|
}
|
|
158
180
|
ordered.push(buf);
|
|
159
181
|
}
|
|
@@ -171,12 +193,21 @@ export function elementKernel(spec) {
|
|
|
171
193
|
let cacheKey = 0;
|
|
172
194
|
for (let i = 0; i < ordered.length; i++)
|
|
173
195
|
cacheKey = (cacheKey * 31 + bufId(ordered[i].gpuBuffer)) | 0;
|
|
174
|
-
|
|
196
|
+
const ids = ordered.map((b) => bufId(b.gpuBuffer));
|
|
197
|
+
const cached = bindGroupCache.get(cacheKey);
|
|
198
|
+
const identityMatch = cached && cached.ids.length === ids.length && cached.ids.every((id, idx) => id === ids[idx]);
|
|
199
|
+
let bg = identityMatch ? cached.bg : undefined;
|
|
175
200
|
if (!bg) {
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
});
|
|
178
209
|
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
179
|
-
bindGroupCache.set(cacheKey, bg);
|
|
210
|
+
bindGroupCache.set(cacheKey, { bg, ids });
|
|
180
211
|
}
|
|
181
212
|
// —— 编码提交 ——
|
|
182
213
|
const enc = device.createCommandEncoder();
|
package/dist/core/layout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ERR, UsageError } from "./errors.js";
|
|
1
2
|
export const TYPES = {
|
|
2
3
|
f32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Float32Array', wgsl: 'f32' },
|
|
3
4
|
i32: { size: 4, stride: 4, align: 4, comps: 1, typed: 'Int32Array', wgsl: 'i32' },
|
|
@@ -38,13 +39,13 @@ export function packUniform(layout, values) {
|
|
|
38
39
|
for (const f of layout.fields) {
|
|
39
40
|
const pack = PACKERS[f.kind];
|
|
40
41
|
if (!pack) {
|
|
41
|
-
throw new
|
|
42
|
+
throw new UsageError(ERR.UNIFORM_UNSUPPORTED, `Uniform field "${f.name}" has unsupported type "${f.kind}". Only scalars are currently supported.`);
|
|
42
43
|
}
|
|
43
44
|
const v = values[f.name];
|
|
44
45
|
if (v === undefined)
|
|
45
|
-
throw new
|
|
46
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Missing uniform value for "${f.name}"`);
|
|
46
47
|
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
47
|
-
throw new
|
|
48
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Uniform value for "${f.name}" must be a finite number, got: ${String(v)}`);
|
|
48
49
|
}
|
|
49
50
|
pack(view, f.offset, v);
|
|
50
51
|
}
|
|
@@ -56,13 +57,13 @@ export function packUniformInto(target, layout, values) {
|
|
|
56
57
|
for (const f of layout.fields) {
|
|
57
58
|
const pack = PACKERS[f.kind];
|
|
58
59
|
if (!pack) {
|
|
59
|
-
throw new
|
|
60
|
+
throw new UsageError(ERR.UNIFORM_UNSUPPORTED, `Uniform field "${f.name}" has unsupported type "${f.kind}". Only scalars are currently supported.`);
|
|
60
61
|
}
|
|
61
62
|
const v = values[f.name];
|
|
62
63
|
if (v === undefined)
|
|
63
|
-
throw new
|
|
64
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Missing uniform value for "${f.name}"`);
|
|
64
65
|
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
65
|
-
throw new
|
|
66
|
+
throw new UsageError(ERR.UNIFORM_FIELD, `Uniform value for "${f.name}" must be a finite number, got: ${String(v)}`);
|
|
66
67
|
}
|
|
67
68
|
pack(view, f.offset, v);
|
|
68
69
|
}
|
|
@@ -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/pingpong.js
CHANGED
|
@@ -20,7 +20,7 @@ export class PingPong {
|
|
|
20
20
|
static async create(kinds, length) {
|
|
21
21
|
const names = Object.keys(kinds);
|
|
22
22
|
if (names.length === 0)
|
|
23
|
-
throw new Error('PingPong
|
|
23
|
+
throw new Error('PingPong requires at least one field');
|
|
24
24
|
const make = async () => {
|
|
25
25
|
const side = {};
|
|
26
26
|
for (const name of names)
|
package/dist/core/raw.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { GpuContext } from "./context.js";
|
|
2
|
-
import { CompileError, UsageError } from "./errors.js";
|
|
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
|
-
throw new UsageError('rawKernel
|
|
6
|
+
throw new UsageError(ERR.USAGE, 'rawKernel requires WGSL code');
|
|
6
7
|
let pipelinePromise = null;
|
|
7
8
|
return {
|
|
8
9
|
async run(entries, workgroups) {
|
|
9
10
|
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
10
|
-
throw new UsageError(`rawKernel.run
|
|
11
|
+
throw new UsageError(ERR.USAGE, `rawKernel.run workgroups must be a positive integer, got ${String(workgroups)}`);
|
|
11
12
|
}
|
|
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);
|
package/dist/media.js
CHANGED
|
@@ -24,14 +24,14 @@ export class CanvasRecorder {
|
|
|
24
24
|
constructor() {
|
|
25
25
|
const mime = pickMime();
|
|
26
26
|
if (!mime)
|
|
27
|
-
throw new Error('
|
|
27
|
+
throw new Error('MediaRecorder is not supported in this environment');
|
|
28
28
|
this.#mime = mime;
|
|
29
29
|
}
|
|
30
30
|
get mimeType() { return this.#mime; }
|
|
31
31
|
get recording() { return this.#recorder?.state === 'recording'; }
|
|
32
32
|
start(canvas, videoBitsPerSecond = 12_000_000) {
|
|
33
33
|
if (this.#recorder)
|
|
34
|
-
throw new Error('
|
|
34
|
+
throw new Error('Recording already in progress');
|
|
35
35
|
const stream = canvas.captureStream(60);
|
|
36
36
|
this.#chunks = [];
|
|
37
37
|
this.#recorder = new MediaRecorder(stream, { mimeType: this.#mime, videoBitsPerSecond });
|
|
@@ -51,7 +51,7 @@ export class CanvasRecorder {
|
|
|
51
51
|
const blob = new Blob(this.#chunks, { type: this.#mime });
|
|
52
52
|
this.#recorder = null;
|
|
53
53
|
if (blob.size === 0) {
|
|
54
|
-
err(new Error(
|
|
54
|
+
err(new Error(`Recording produced 0 bytes (${this.#mime}); encoder may be unavailable`));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
ok({ blob, mimeType: this.#mime, seconds: (performance.now() - this.#startedAt) / 1000, bytes: blob.size });
|
package/dist/observe.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* —— 评审指出的"最大产品级缺口":GPGPU 库却看不到"这帧 GPU 花了多少"。
|
|
4
4
|
*/
|
|
5
5
|
import { GpuContext } from "./core/context.js";
|
|
6
|
-
import { UsageError } from "./core/errors.js";
|
|
6
|
+
import { ERR, UsageError } from "./core/errors.js";
|
|
7
7
|
/** 时间戳查询封装:测量一段 GPU 工作的真实耗时(毫秒)。
|
|
8
8
|
* Chrome/Edge 支持 timestamp-query;不支持的浏览器 reject。 */
|
|
9
9
|
export async function timeGpu(fn) {
|
|
@@ -11,7 +11,7 @@ export async function timeGpu(fn) {
|
|
|
11
11
|
const device = ctx.device;
|
|
12
12
|
const featureOk = device.features.has('timestamp-query');
|
|
13
13
|
if (!featureOk)
|
|
14
|
-
throw new UsageError('timestamp-query
|
|
14
|
+
throw new UsageError(ERR.TIMESTAMP_UNSUPPORTED, 'timestamp-query is not supported on this device (requires Chrome/Edge + a GPU with timestamp support)');
|
|
15
15
|
const QUERY_POOL = 2;
|
|
16
16
|
const querySet = device.createQuerySet({ type: 'timestamp', count: QUERY_POOL });
|
|
17
17
|
const resolveBuf = device.createBuffer({
|
|
@@ -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>;
|