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/dist/index.js
CHANGED
|
@@ -1,484 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
\u6392\u67E5:\u2460 \u6D4F\u89C8\u5668\u9700 Chrome/Edge 113+ \u6216 Safari 18+;\u2461 \u65E0\u5934\u73AF\u5883\u9700\u5F00\u542F WebGPU;\u2462 \u68C0\u67E5 GPU \u9A71\u52A8\u4E0E\u786C\u4EF6\u52A0\u901F\u8BBE\u7F6E\u3002
|
|
13
|
-
\u53EF\u7528 navigator.gpu \u662F\u5426\u5B58\u5728\u5FEB\u901F\u5224\u65AD\u3002`
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
var CompileError = class extends WgpuKitError {
|
|
18
|
-
constructor(kernelName, messages, userCodeOffset) {
|
|
19
|
-
const mapped = messages.map((m) => {
|
|
20
|
-
const userLine = m.line - userCodeOffset;
|
|
21
|
-
const where = userLine > 0 ? `\u7528\u6237\u4EE3\u7801\u7B2C ${userLine} \u884C` : `\u751F\u6210\u4EE3\u7801\u7B2C ${m.line} \u884C(\u5E93\u7684\u95EE\u9898,\u6B22\u8FCE\u62A5 issue)`;
|
|
22
|
-
return ` ${where}: ${m.msg}`;
|
|
23
|
-
}).join("\n");
|
|
24
|
-
super(`kernel "${kernelName}" WGSL \u7F16\u8BD1\u5931\u8D25:
|
|
25
|
-
${mapped}`);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
var UsageError = class extends WgpuKitError {
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// src/core/context.ts
|
|
32
|
-
var GpuContext = class _GpuContext {
|
|
33
|
-
device;
|
|
34
|
-
adapterInfo;
|
|
35
|
-
constructor(device, adapterInfo) {
|
|
36
|
-
this.device = device;
|
|
37
|
-
this.adapterInfo = adapterInfo;
|
|
38
|
-
}
|
|
39
|
-
static #singleton = null;
|
|
40
|
-
static get() {
|
|
41
|
-
if (!_GpuContext.#singleton) {
|
|
42
|
-
_GpuContext.#singleton = _GpuContext.#create().catch((e) => {
|
|
43
|
-
_GpuContext.#singleton = null;
|
|
44
|
-
throw e;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
return _GpuContext.#singleton;
|
|
48
|
-
}
|
|
49
|
-
static async #create() {
|
|
50
|
-
if (typeof navigator === "undefined" || !("gpu" in navigator) || !navigator.gpu) {
|
|
51
|
-
throw new WebGPUUnavailableError("navigator.gpu \u4E0D\u5B58\u5728");
|
|
52
|
-
}
|
|
53
|
-
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
54
|
-
if (!adapter) throw new WebGPUUnavailableError("requestAdapter() \u8FD4\u56DE null");
|
|
55
|
-
const info = adapter.info;
|
|
56
|
-
const label = info ? [info.vendor, info.architecture, info.description].filter(Boolean).join(" / ") || "unknown" : "unknown";
|
|
57
|
-
const requiredLimits = {};
|
|
58
|
-
const want = [
|
|
59
|
-
"maxStorageBuffersPerShaderStage",
|
|
60
|
-
"maxStorageBuffersInVertexStage",
|
|
61
|
-
"maxStorageBufferBindingSize"
|
|
62
|
-
];
|
|
63
|
-
for (const key of want) {
|
|
64
|
-
const supported = adapter.limits[key];
|
|
65
|
-
if (typeof supported === "number") requiredLimits[key] = supported;
|
|
66
|
-
}
|
|
67
|
-
const device = await adapter.requestDevice({ label: "wgpu-kit", requiredLimits });
|
|
68
|
-
return new _GpuContext(device, label);
|
|
69
|
-
}
|
|
70
|
-
/** device lost 时 reject;调用方可 await 做清理/提示 */
|
|
71
|
-
get lost() {
|
|
72
|
-
return this.device.lost;
|
|
73
|
-
}
|
|
74
|
-
/** 等待队列中已提交的全部 GPU 工作完成(测试/读回前同步用) */
|
|
75
|
-
async sync() {
|
|
76
|
-
await this.device.queue.onSubmittedWorkDone();
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/core/layout.ts
|
|
81
|
-
var TYPES = {
|
|
82
|
-
f32: { size: 4, align: 4, comps: 1, typed: "Float32Array", wgsl: "f32" },
|
|
83
|
-
i32: { size: 4, align: 4, comps: 1, typed: "Int32Array", wgsl: "i32" },
|
|
84
|
-
u32: { size: 4, align: 4, comps: 1, typed: "Uint32Array", wgsl: "u32" },
|
|
85
|
-
vec2f: { size: 8, align: 8, comps: 2, typed: "Float32Array", wgsl: "vec2f" },
|
|
86
|
-
vec2i: { size: 8, align: 8, comps: 2, typed: "Int32Array", wgsl: "vec2i" },
|
|
87
|
-
vec2u: { size: 8, align: 8, comps: 2, typed: "Uint32Array", wgsl: "vec2u" },
|
|
88
|
-
vec3f: { size: 12, align: 16, comps: 3, typed: "Float32Array", wgsl: "vec3f" },
|
|
89
|
-
vec4f: { size: 16, align: 16, comps: 4, typed: "Float32Array", wgsl: "vec4f" }
|
|
90
|
-
};
|
|
91
|
-
function alignTo(offset, align) {
|
|
92
|
-
return Math.ceil(offset / align) * align;
|
|
93
|
-
}
|
|
94
|
-
function planUniform(entries) {
|
|
95
|
-
const fields = [];
|
|
96
|
-
let cursor = 0;
|
|
97
|
-
for (const [name, kind] of entries) {
|
|
98
|
-
const def = TYPES[kind];
|
|
99
|
-
cursor = alignTo(cursor, def.align);
|
|
100
|
-
fields.push({ name, kind, offset: cursor });
|
|
101
|
-
cursor += def.size;
|
|
102
|
-
}
|
|
103
|
-
return { fields, size: alignTo(Math.max(cursor, 1), 16) };
|
|
104
|
-
}
|
|
105
|
-
var PACKERS = {
|
|
106
|
-
f32: (v, o, x) => v.setFloat32(o, x, true),
|
|
107
|
-
i32: (v, o, x) => v.setInt32(o, x, true),
|
|
108
|
-
u32: (v, o, x) => v.setUint32(o, x, true)
|
|
109
|
-
};
|
|
110
|
-
function packUniform(layout, values) {
|
|
111
|
-
const buf = new ArrayBuffer(layout.size);
|
|
112
|
-
const view = new DataView(buf);
|
|
113
|
-
for (const f of layout.fields) {
|
|
114
|
-
const pack = PACKERS[f.kind];
|
|
115
|
-
if (!pack) {
|
|
116
|
-
throw new Error(`uniform \u5B57\u6BB5 ${f.name} \u7684\u7C7B\u578B ${f.kind} \u6682\u4E0D\u652F\u6301(\u5F53\u524D\u4EC5\u652F\u6301\u6807\u91CF)`);
|
|
117
|
-
}
|
|
118
|
-
const v = values[f.name];
|
|
119
|
-
if (v === void 0) throw new Error(`\u7F3A\u5C11 uniform \u503C: ${f.name}`);
|
|
120
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
121
|
-
throw new Error(`uniform \u503C ${f.name} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57,\u6536\u5230: ${String(v)}`);
|
|
122
|
-
}
|
|
123
|
-
pack(view, f.offset, v);
|
|
124
|
-
}
|
|
125
|
-
return buf;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/core/buffer.ts
|
|
129
|
-
var TYPED_CTORS = {
|
|
130
|
-
Float32Array,
|
|
131
|
-
Int32Array,
|
|
132
|
-
Uint32Array
|
|
133
|
-
};
|
|
134
|
-
var Buffer = class _Buffer {
|
|
135
|
-
kind;
|
|
136
|
-
length;
|
|
137
|
-
gpuBuffer;
|
|
138
|
-
#ctx;
|
|
139
|
-
#byteLength;
|
|
140
|
-
#staging = null;
|
|
141
|
-
constructor(ctx, kind, length, gpuBuffer) {
|
|
142
|
-
this.#ctx = ctx;
|
|
143
|
-
this.kind = kind;
|
|
144
|
-
this.length = length;
|
|
145
|
-
this.gpuBuffer = gpuBuffer;
|
|
146
|
-
this.#byteLength = length * TYPES[kind].size;
|
|
147
|
-
}
|
|
148
|
-
static async create(kind, length) {
|
|
149
|
-
if (!Number.isInteger(length) || length <= 0) {
|
|
150
|
-
throw new UsageError(`Buffer \u957F\u5EA6\u5FC5\u987B\u662F\u6B63\u6574\u6570,\u6536\u5230: ${String(length)}`);
|
|
151
|
-
}
|
|
152
|
-
const def = TYPES[kind];
|
|
153
|
-
if (!def) throw new UsageError(`\u672A\u77E5\u7C7B\u578B "${String(kind)}",\u53EF\u7528: ${Object.keys(TYPES).join(", ")}`);
|
|
154
|
-
const ctx = await GpuContext.get();
|
|
155
|
-
const gpuBuffer = ctx.device.createBuffer({
|
|
156
|
-
size: length * def.size,
|
|
157
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
158
|
-
label: `wgpu-kit Buffer<${kind}>[${length}]`
|
|
159
|
-
});
|
|
160
|
-
return new _Buffer(ctx, kind, length, gpuBuffer);
|
|
161
|
-
}
|
|
162
|
-
/** 校验并写入(CPU → GPU) */
|
|
163
|
-
write(data) {
|
|
164
|
-
const ctor = TYPED_CTORS[TYPES[this.kind].typed];
|
|
165
|
-
if (!(data instanceof ctor)) {
|
|
166
|
-
throw new UsageError(`Buffer<${this.kind}>.write \u9700\u8981 ${TYPES[this.kind].typed},\u6536\u5230 ${data.constructor?.name ?? typeof data}`);
|
|
167
|
-
}
|
|
168
|
-
const expected = this.length * TYPES[this.kind].comps;
|
|
169
|
-
if (data.length !== expected) {
|
|
170
|
-
throw new UsageError(`Buffer<${this.kind}>[${this.length}].write \u9700\u8981 ${expected} \u4E2A\u5206\u91CF,\u6536\u5230 ${data.length}`);
|
|
171
|
-
}
|
|
172
|
-
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, data);
|
|
173
|
-
}
|
|
174
|
-
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
175
|
-
async read() {
|
|
176
|
-
const def = TYPES[this.kind];
|
|
177
|
-
if (!this.#staging) {
|
|
178
|
-
this.#staging = this.#ctx.device.createBuffer({
|
|
179
|
-
size: this.#byteLength,
|
|
180
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
181
|
-
label: `wgpu-kit staging[${this.length}]`
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
const enc = this.#ctx.device.createCommandEncoder();
|
|
185
|
-
enc.copyBufferToBuffer(this.gpuBuffer, 0, this.#staging, 0, this.#byteLength);
|
|
186
|
-
this.#ctx.device.queue.submit([enc.finish()]);
|
|
187
|
-
await this.#staging.mapAsync(GPUMapMode.READ);
|
|
188
|
-
const ab = this.#staging.getMappedRange().slice(0);
|
|
189
|
-
this.#staging.unmap();
|
|
190
|
-
if (def.typed === "Float32Array") return new Float32Array(ab);
|
|
191
|
-
if (def.typed === "Int32Array") return new Int32Array(ab);
|
|
192
|
-
return new Uint32Array(ab);
|
|
193
|
-
}
|
|
194
|
-
destroy() {
|
|
195
|
-
if (this.#staging) {
|
|
196
|
-
this.#staging.destroy();
|
|
197
|
-
this.#staging = null;
|
|
198
|
-
}
|
|
199
|
-
this.gpuBuffer.destroy();
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// src/core/kernel.ts
|
|
204
|
-
var RESERVED = /* @__PURE__ */ new Set(["count"]);
|
|
205
|
-
var nextBufferId = 0;
|
|
206
|
-
var bufferIds = /* @__PURE__ */ new WeakMap();
|
|
207
|
-
function bufId(b) {
|
|
208
|
-
let id = bufferIds.get(b);
|
|
209
|
-
if (id === void 0) {
|
|
210
|
-
id = ++nextBufferId;
|
|
211
|
-
bufferIds.set(b, id);
|
|
212
|
-
}
|
|
213
|
-
return id;
|
|
214
|
-
}
|
|
215
|
-
function generateElementKernel(spec) {
|
|
216
|
-
const name = spec.name ?? "kernel";
|
|
217
|
-
const workgroupSize = spec.workgroupSize ?? 64;
|
|
218
|
-
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
219
|
-
throw new UsageError(`workgroupSize \u5FC5\u987B\u5728 1..512,\u6536\u5230: ${String(workgroupSize)}`);
|
|
220
|
-
}
|
|
221
|
-
const state = Object.entries(spec.state ?? {});
|
|
222
|
-
const inputs = Object.entries(spec.inputs ?? {});
|
|
223
|
-
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
224
|
-
if (state.length + inputs.length === 0) {
|
|
225
|
-
throw new UsageError(`elementKernel "${name}" \u81F3\u5C11\u9700\u8981\u4E00\u4E2A state \u6216 inputs \u5B57\u6BB5`);
|
|
226
|
-
}
|
|
227
|
-
for (const [uName] of uniforms) {
|
|
228
|
-
if (RESERVED.has(uName)) throw new UsageError(`uniform \u540D "${uName}" \u662F\u4FDD\u7559\u540D(count \u7531\u5E93\u81EA\u52A8\u6CE8\u5165)`);
|
|
229
|
-
}
|
|
230
|
-
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
231
|
-
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
232
|
-
throw new UsageError(`elementKernel "${name}" \u7684 state/inputs/uniforms \u5B58\u5728\u91CD\u540D\u5B57\u6BB5`);
|
|
233
|
-
}
|
|
234
|
-
if (typeof spec.code !== "string" || spec.code.trim().length === 0) {
|
|
235
|
-
throw new UsageError(`elementKernel "${name}" \u7F3A\u5C11 code(\u7528\u6237 WGSL \u51FD\u6570)`);
|
|
236
|
-
}
|
|
237
|
-
const uniformEntries = [...uniforms, ["count", "u32"]];
|
|
238
|
-
const uniformLayout = planUniform(uniformEntries);
|
|
239
|
-
const header = [];
|
|
240
|
-
header.push("// \u7531 wgpu-kit elementKernel \u751F\u6210");
|
|
241
|
-
header.push("struct Params {");
|
|
242
|
-
for (const [n, k] of uniformEntries) header.push(` ${n}: ${TYPES[k].wgsl},`);
|
|
243
|
-
header.push("};");
|
|
244
|
-
header.push("@group(0) @binding(0) var<uniform> params: Params;");
|
|
245
|
-
let binding = 1;
|
|
246
|
-
for (const [n, k] of state) header.push(`@group(0) @binding(${binding++}) var<storage, read_write> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
247
|
-
for (const [n, k] of inputs) header.push(`@group(0) @binding(${binding++}) var<storage, read> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
248
|
-
header.push("");
|
|
249
|
-
header.push(`@compute @workgroup_size(${workgroupSize})`);
|
|
250
|
-
header.push("fn main(@builtin(global_invocation_id) gid: vec3u) {");
|
|
251
|
-
header.push(" let idx = gid.x;");
|
|
252
|
-
header.push(" if (idx >= params.count) { return; }");
|
|
253
|
-
const uniformArgs = uniforms.map(([n]) => `params.${n}`).join(", ");
|
|
254
|
-
header.push(` userFn(idx${uniformArgs ? ", " + uniformArgs : ""});`);
|
|
255
|
-
header.push("}");
|
|
256
|
-
const userCodeLineOffset = header.length;
|
|
257
|
-
const source = [...header, spec.code].join("\n");
|
|
258
|
-
return {
|
|
259
|
-
normalized: { name, workgroupSize, state, inputs, uniforms, code: spec.code },
|
|
260
|
-
source,
|
|
261
|
-
uniformLayout,
|
|
262
|
-
userCodeLineOffset
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function elementKernel(spec) {
|
|
266
|
-
const first = generateElementKernel(spec);
|
|
267
|
-
const uniformLayout = first.uniformLayout;
|
|
268
|
-
let normalized = first.normalized;
|
|
269
|
-
let source = first.source;
|
|
270
|
-
let userCodeLineOffset = first.userCodeLineOffset;
|
|
271
|
-
const uniformBufferName = `${normalized.name}:uniform`;
|
|
272
|
-
let pipelinePromise = null;
|
|
273
|
-
const bindGroupCache = /* @__PURE__ */ new Map();
|
|
274
|
-
let uniformBuffer = null;
|
|
275
|
-
const compilePipeline = async () => {
|
|
276
|
-
const ctx = await GpuContext.get();
|
|
277
|
-
const device = ctx.device;
|
|
278
|
-
const module = device.createShaderModule({ code: source, label: normalized.name });
|
|
279
|
-
const info = await module.getCompilationInfo();
|
|
280
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
281
|
-
if (errors.length > 0) {
|
|
282
|
-
throw new CompileError(
|
|
283
|
-
normalized.name,
|
|
284
|
-
errors.map((m) => ({ line: m.lineNum, msg: m.message })),
|
|
285
|
-
userCodeLineOffset
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
return device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
|
|
289
|
-
};
|
|
290
|
-
async function getPipeline() {
|
|
291
|
-
if (!pipelinePromise) {
|
|
292
|
-
pipelinePromise = compilePipeline().catch((e) => {
|
|
293
|
-
pipelinePromise = null;
|
|
294
|
-
throw e;
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
return pipelinePromise;
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
get name() {
|
|
301
|
-
return normalized.name;
|
|
302
|
-
},
|
|
303
|
-
get source() {
|
|
304
|
-
return source;
|
|
305
|
-
},
|
|
306
|
-
get uniformLayout() {
|
|
307
|
-
return first.uniformLayout;
|
|
308
|
-
},
|
|
309
|
-
get workgroupSize() {
|
|
310
|
-
return normalized.workgroupSize;
|
|
311
|
-
},
|
|
312
|
-
async replace(code) {
|
|
313
|
-
const regen = generateElementKernel({ ...spec, code });
|
|
314
|
-
const savedSource = source;
|
|
315
|
-
const savedOffset = userCodeLineOffset;
|
|
316
|
-
source = regen.source;
|
|
317
|
-
userCodeLineOffset = regen.userCodeLineOffset;
|
|
318
|
-
try {
|
|
319
|
-
const p = await compilePipeline();
|
|
320
|
-
pipelinePromise = Promise.resolve(p);
|
|
321
|
-
bindGroupCache.clear();
|
|
322
|
-
} catch (e) {
|
|
323
|
-
source = savedSource;
|
|
324
|
-
userCodeLineOffset = savedOffset;
|
|
325
|
-
throw e;
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
async run(resources, uniforms = {}) {
|
|
329
|
-
const ctx = await GpuContext.get();
|
|
330
|
-
const device = ctx.device;
|
|
331
|
-
const pipeline = await getPipeline();
|
|
332
|
-
const ordered = [];
|
|
333
|
-
for (const [key] of [...normalized.state, ...normalized.inputs]) {
|
|
334
|
-
const buf = resources[key];
|
|
335
|
-
if (!buf) throw new UsageError(`kernel "${normalized.name}".run \u7F3A\u5C11\u8D44\u6E90 "${key}"`);
|
|
336
|
-
const want = [...normalized.state, ...normalized.inputs].find(([n]) => n === key)?.[1];
|
|
337
|
-
if (buf.kind !== want) {
|
|
338
|
-
throw new UsageError(`\u8D44\u6E90 "${key}" \u7C7B\u578B\u4E0D\u5339\u914D: \u9700\u8981 ${want},\u6536\u5230 ${buf.kind}`);
|
|
339
|
-
}
|
|
340
|
-
ordered.push({ key, buffer: buf });
|
|
341
|
-
}
|
|
342
|
-
const count = ordered[0]?.buffer.length ?? 0;
|
|
343
|
-
for (const { key, buffer } of ordered) {
|
|
344
|
-
if (buffer.length !== count) {
|
|
345
|
-
throw new UsageError(`\u8D44\u6E90 "${key}" \u957F\u5EA6 ${buffer.length} \u4E0E "${ordered[0].key}" \u7684 ${count} \u4E0D\u4E00\u81F4`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const uBytes = packUniform(uniformLayout, { ...uniforms, count });
|
|
349
|
-
if (!uniformBuffer) {
|
|
350
|
-
uniformBuffer = device.createBuffer({
|
|
351
|
-
size: uniformLayout.size,
|
|
352
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
353
|
-
label: uniformBufferName
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
device.queue.writeBuffer(uniformBuffer, 0, uBytes);
|
|
357
|
-
const cacheKey = ordered.map(({ key, buffer }) => `${key}:${bufId(buffer.gpuBuffer)}`).join("|");
|
|
358
|
-
let bg = bindGroupCache.get(cacheKey);
|
|
359
|
-
if (!bg) {
|
|
360
|
-
const entries = [{ binding: 0, resource: { buffer: uniformBuffer } }];
|
|
361
|
-
ordered.forEach(({ buffer }, i) => entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } }));
|
|
362
|
-
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
363
|
-
bindGroupCache.set(cacheKey, bg);
|
|
364
|
-
}
|
|
365
|
-
const enc = device.createCommandEncoder();
|
|
366
|
-
const pass = enc.beginComputePass();
|
|
367
|
-
pass.setPipeline(pipeline);
|
|
368
|
-
pass.setBindGroup(0, bg);
|
|
369
|
-
pass.dispatchWorkgroups(Math.ceil(count / normalized.workgroupSize));
|
|
370
|
-
pass.end();
|
|
371
|
-
device.queue.submit([enc.finish()]);
|
|
372
|
-
},
|
|
373
|
-
destroy() {
|
|
374
|
-
pipelinePromise = null;
|
|
375
|
-
bindGroupCache.clear();
|
|
376
|
-
uniformBuffer?.destroy();
|
|
377
|
-
uniformBuffer = null;
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// src/core/pingpong.ts
|
|
383
|
-
var PingPong = class _PingPong {
|
|
384
|
-
#sides;
|
|
385
|
-
#names;
|
|
386
|
-
#length;
|
|
387
|
-
#kinds;
|
|
388
|
-
#index = 0;
|
|
389
|
-
constructor(names, kinds, length, a, b) {
|
|
390
|
-
this.#names = names;
|
|
391
|
-
this.#kinds = kinds;
|
|
392
|
-
this.#length = length;
|
|
393
|
-
this.#sides = [a, b];
|
|
394
|
-
}
|
|
395
|
-
static async create(kinds, length) {
|
|
396
|
-
const names = Object.keys(kinds);
|
|
397
|
-
if (names.length === 0) throw new Error("PingPong \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
398
|
-
const make = async () => {
|
|
399
|
-
const side = {};
|
|
400
|
-
for (const name of names) side[name] = await Buffer.create(kinds[name], length);
|
|
401
|
-
return side;
|
|
402
|
-
};
|
|
403
|
-
return new _PingPong(names, kinds, length, await make(), await make());
|
|
404
|
-
}
|
|
405
|
-
/** 当前帧的数据侧(渲染/读回用) */
|
|
406
|
-
get current() {
|
|
407
|
-
return this.#sides[this.#index];
|
|
408
|
-
}
|
|
409
|
-
/** 另一侧(kernel 写入目标) */
|
|
410
|
-
get other() {
|
|
411
|
-
return this.#sides[1 - this.#index];
|
|
412
|
-
}
|
|
413
|
-
/** 帧末翻转 */
|
|
414
|
-
swap() {
|
|
415
|
-
this.#index = 1 - this.#index;
|
|
416
|
-
}
|
|
417
|
-
/** 以 (写侧, 读侧) 调用 fn 后自动 swap 的语法糖 */
|
|
418
|
-
async runWith(fn) {
|
|
419
|
-
await fn(this.other, this.current);
|
|
420
|
-
this.swap();
|
|
421
|
-
}
|
|
422
|
-
destroy() {
|
|
423
|
-
for (const side of this.#sides) for (const b of Object.values(side)) b.destroy();
|
|
424
|
-
}
|
|
425
|
-
/** 克隆一份同构 PingPong(同字段同长度) */
|
|
426
|
-
async clone() {
|
|
427
|
-
return _PingPong.create(this.#kinds, this.#length);
|
|
428
|
-
}
|
|
429
|
-
get names() {
|
|
430
|
-
return this.#names;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// src/core/raw.ts
|
|
435
|
-
function rawKernel(code, entryPoint = "main", label = "rawKernel") {
|
|
436
|
-
if (typeof code !== "string" || code.trim().length === 0) throw new UsageError("rawKernel \u9700\u8981 WGSL \u4EE3\u7801");
|
|
437
|
-
let pipelinePromise = null;
|
|
438
|
-
return {
|
|
439
|
-
async run(entries, workgroups) {
|
|
440
|
-
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
441
|
-
throw new UsageError(`rawKernel.run \u7684 workgroups \u5FC5\u987B\u662F\u6B63\u6574\u6570,\u6536\u5230 ${String(workgroups)}`);
|
|
442
|
-
}
|
|
443
|
-
const ctx = await GpuContext.get();
|
|
444
|
-
if (!pipelinePromise) {
|
|
445
|
-
pipelinePromise = (async () => {
|
|
446
|
-
const module = ctx.device.createShaderModule({ code, label });
|
|
447
|
-
const info = await module.getCompilationInfo();
|
|
448
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
449
|
-
if (errors.length > 0) {
|
|
450
|
-
pipelinePromise = null;
|
|
451
|
-
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
452
|
-
}
|
|
453
|
-
return ctx.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint } });
|
|
454
|
-
})();
|
|
455
|
-
}
|
|
456
|
-
const pipeline = await pipelinePromise;
|
|
457
|
-
const bg = ctx.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
458
|
-
const enc = ctx.device.createCommandEncoder();
|
|
459
|
-
const pass = enc.beginComputePass();
|
|
460
|
-
pass.setPipeline(pipeline);
|
|
461
|
-
pass.setBindGroup(0, bg);
|
|
462
|
-
pass.dispatchWorkgroups(workgroups);
|
|
463
|
-
pass.end();
|
|
464
|
-
ctx.device.queue.submit([enc.finish()]);
|
|
465
|
-
},
|
|
466
|
-
destroy() {
|
|
467
|
-
pipelinePromise = null;
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
export {
|
|
472
|
-
Buffer,
|
|
473
|
-
CompileError,
|
|
474
|
-
GpuContext,
|
|
475
|
-
PingPong,
|
|
476
|
-
TYPES,
|
|
477
|
-
UsageError,
|
|
478
|
-
WebGPUUnavailableError,
|
|
479
|
-
WgpuKitError,
|
|
480
|
-
elementKernel,
|
|
481
|
-
packUniform,
|
|
482
|
-
planUniform,
|
|
483
|
-
rawKernel
|
|
484
|
-
};
|
|
1
|
+
import { GpuContext } from "./core/context.js";
|
|
2
|
+
import { Buffer } from "./core/buffer.js";
|
|
3
|
+
import { elementKernel } from "./core/kernel.js";
|
|
4
|
+
import { PingPong } from "./core/pingpong.js";
|
|
5
|
+
import { rawKernel } from "./core/raw.js";
|
|
6
|
+
import { UsageError } from "./core/errors.js";
|
|
7
|
+
export { GpuContext, Buffer, elementKernel, PingPong, rawKernel };
|
|
8
|
+
// 主入口直达旗舰包:import { particles } from 'wgpu-kit' 开箱即用
|
|
9
|
+
export { particles } from "./packs/particles/index.js";
|
|
10
|
+
export { TYPES, planUniform, packUniform, packUniformInto } from "./core/layout.js";
|
|
11
|
+
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from "./core/errors.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function threePoints(sim, THREE, opts = {}) {
|
|
2
|
+
const { pos } = sim.buffers();
|
|
3
|
+
const count = pos.length;
|
|
4
|
+
const positions = new Float32Array(count * 3); // three 需要 vec3,z=0
|
|
5
|
+
const geometry = new THREE.BufferGeometry();
|
|
6
|
+
const attr = new THREE.BufferAttribute(positions, 3);
|
|
7
|
+
geometry.setAttribute('position', attr);
|
|
8
|
+
const material = new THREE.PointsMaterial({
|
|
9
|
+
size: opts.size ?? 0.015,
|
|
10
|
+
color: opts.color ?? 0x8fb4ff,
|
|
11
|
+
sizeAttenuation: true,
|
|
12
|
+
});
|
|
13
|
+
const points = new THREE.Points(geometry, material);
|
|
14
|
+
return {
|
|
15
|
+
points,
|
|
16
|
+
async update() {
|
|
17
|
+
const data = (await pos.read());
|
|
18
|
+
for (let i = 0; i < count; i++) {
|
|
19
|
+
positions[i * 3] = data[i * 2] ?? 0;
|
|
20
|
+
positions[i * 3 + 1] = data[i * 2 + 1] ?? 0;
|
|
21
|
+
positions[i * 3 + 2] = 0;
|
|
22
|
+
}
|
|
23
|
+
attr.needsUpdate = true;
|
|
24
|
+
},
|
|
25
|
+
dispose() {
|
|
26
|
+
sim.destroy();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const CANDIDATES = [
|
|
2
|
+
// webm 在无头/软件编码环境下最可靠;mp4 的 isTypeSupported 可能"说行但编不动"
|
|
3
|
+
// (实测 headless Edge:mp4 协商成功但产物 0 字节),故排后
|
|
4
|
+
'video/webm;codecs=vp9',
|
|
5
|
+
'video/webm;codecs=vp8',
|
|
6
|
+
'video/webm',
|
|
7
|
+
'video/mp4;codecs=avc1.42E01E',
|
|
8
|
+
'video/mp4',
|
|
9
|
+
];
|
|
10
|
+
export function pickMime() {
|
|
11
|
+
if (typeof MediaRecorder === 'undefined')
|
|
12
|
+
return null;
|
|
13
|
+
for (const m of CANDIDATES) {
|
|
14
|
+
if (MediaRecorder.isTypeSupported(m))
|
|
15
|
+
return m;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export class CanvasRecorder {
|
|
20
|
+
#recorder = null;
|
|
21
|
+
#chunks = [];
|
|
22
|
+
#startedAt = 0;
|
|
23
|
+
#mime;
|
|
24
|
+
constructor() {
|
|
25
|
+
const mime = pickMime();
|
|
26
|
+
if (!mime)
|
|
27
|
+
throw new Error('当前环境不支持 MediaRecorder 录制(无可用编码)');
|
|
28
|
+
this.#mime = mime;
|
|
29
|
+
}
|
|
30
|
+
get mimeType() { return this.#mime; }
|
|
31
|
+
get recording() { return this.#recorder?.state === 'recording'; }
|
|
32
|
+
start(canvas, videoBitsPerSecond = 12_000_000) {
|
|
33
|
+
if (this.#recorder)
|
|
34
|
+
throw new Error('已在录制中');
|
|
35
|
+
const stream = canvas.captureStream(60);
|
|
36
|
+
this.#chunks = [];
|
|
37
|
+
this.#recorder = new MediaRecorder(stream, { mimeType: this.#mime, videoBitsPerSecond });
|
|
38
|
+
this.#recorder.ondataavailable = (e) => { if (e.data.size > 0)
|
|
39
|
+
this.#chunks.push(e.data); };
|
|
40
|
+
this.#startedAt = performance.now();
|
|
41
|
+
this.#recorder.start(250);
|
|
42
|
+
}
|
|
43
|
+
stop() {
|
|
44
|
+
return new Promise((ok, err) => {
|
|
45
|
+
const r = this.#recorder;
|
|
46
|
+
if (!r || r.state !== 'recording') {
|
|
47
|
+
err(new Error('没有进行中的录制'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
r.onstop = () => {
|
|
51
|
+
const blob = new Blob(this.#chunks, { type: this.#mime });
|
|
52
|
+
this.#recorder = null;
|
|
53
|
+
if (blob.size === 0) {
|
|
54
|
+
err(new Error(`录制产物为空(${this.#mime});编码器可能不可用,换浏览器或网络前重试`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
ok({ blob, mimeType: this.#mime, seconds: (performance.now() - this.#startedAt) / 1000, bytes: blob.size });
|
|
58
|
+
};
|
|
59
|
+
r.stop();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** 触发浏览器下载(录完即得的最后一公里) */
|
|
64
|
+
export function downloadBlob(blob, filename) {
|
|
65
|
+
const a = document.createElement('a');
|
|
66
|
+
a.href = URL.createObjectURL(blob);
|
|
67
|
+
a.download = filename;
|
|
68
|
+
a.click();
|
|
69
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可观测性与健壮性助手(批次 2)。
|
|
3
|
+
* —— 评审指出的"最大产品级缺口":GPGPU 库却看不到"这帧 GPU 花了多少"。
|
|
4
|
+
*/
|
|
5
|
+
import { GpuContext } from './core/context.ts';
|
|
6
|
+
/** 时间戳查询封装:测量一段 GPU 工作的真实耗时(毫秒)。
|
|
7
|
+
* Chrome/Edge 支持 timestamp-query;不支持的浏览器 reject。 */
|
|
8
|
+
export declare function timeGpu(fn: (ctx: GpuContext) => void | Promise<void>): Promise<number>;
|
|
9
|
+
/** 设备诊断:注册错误/丢失回调;设备丢失时自动重建上下文并调用 onRebuild。
|
|
10
|
+
* 长跑页面(展览/大屏)的必需品。 */
|
|
11
|
+
export declare function watchDevice(opts: {
|
|
12
|
+
onError?: (message: string, recoverable: boolean) => void;
|
|
13
|
+
onRebuild?: (ctx: GpuContext) => void;
|
|
14
|
+
}): void;
|
|
15
|
+
/** 推荐的画布 canvas 格式(一行助手的语义化封装)。 */
|
|
16
|
+
export declare function preferredCanvasFormat(): GPUTextureFormat;
|
|
17
|
+
/** 画布 resize(DPR 上限策略):宽高物理像素 = clientSize × min(dpr, cap)。
|
|
18
|
+
* 返回是否实际改变了尺寸(没变则无需重建)。 */
|
|
19
|
+
export declare function resizeCanvas(canvas: HTMLCanvasElement, dprCap?: number): boolean;
|
package/dist/observe.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可观测性与健壮性助手(批次 2)。
|
|
3
|
+
* —— 评审指出的"最大产品级缺口":GPGPU 库却看不到"这帧 GPU 花了多少"。
|
|
4
|
+
*/
|
|
5
|
+
import { GpuContext } from "./core/context.js";
|
|
6
|
+
import { UsageError } from "./core/errors.js";
|
|
7
|
+
/** 时间戳查询封装:测量一段 GPU 工作的真实耗时(毫秒)。
|
|
8
|
+
* Chrome/Edge 支持 timestamp-query;不支持的浏览器 reject。 */
|
|
9
|
+
export async function timeGpu(fn) {
|
|
10
|
+
const ctx = await GpuContext.get();
|
|
11
|
+
const device = ctx.device;
|
|
12
|
+
const featureOk = device.features.has('timestamp-query');
|
|
13
|
+
if (!featureOk)
|
|
14
|
+
throw new UsageError('timestamp-query 在当前设备不可用(需 Chrome/Edge + 支持时间戳的 GPU)');
|
|
15
|
+
const QUERY_POOL = 2;
|
|
16
|
+
const querySet = device.createQuerySet({ type: 'timestamp', count: QUERY_POOL });
|
|
17
|
+
const resolveBuf = device.createBuffer({
|
|
18
|
+
size: QUERY_POOL * 8,
|
|
19
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
20
|
+
});
|
|
21
|
+
const readBuf = device.createBuffer({ size: QUERY_POOL * 8, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
22
|
+
// 新版 WebGPU 规范:时间戳通过 pass 的 timestampWrites 写入(无 encoder.writeTimestamp)
|
|
23
|
+
const enc = device.createCommandEncoder();
|
|
24
|
+
{
|
|
25
|
+
const p0 = enc.beginComputePass({ timestampWrites: { querySet, beginningOfPassWriteIndex: 0 } });
|
|
26
|
+
p0.end();
|
|
27
|
+
}
|
|
28
|
+
await fn(ctx);
|
|
29
|
+
{
|
|
30
|
+
const p1 = enc.beginComputePass({ timestampWrites: { querySet, beginningOfPassWriteIndex: 1 } });
|
|
31
|
+
p1.end();
|
|
32
|
+
}
|
|
33
|
+
enc.resolveQuerySet(querySet, 0, QUERY_POOL, resolveBuf, 0);
|
|
34
|
+
enc.copyBufferToBuffer(resolveBuf, 0, readBuf, 0, QUERY_POOL * 8);
|
|
35
|
+
device.queue.submit([enc.finish()]);
|
|
36
|
+
await readBuf.mapAsync(GPUMapMode.READ);
|
|
37
|
+
const times = new BigInt64Array(readBuf.getMappedRange().slice(0));
|
|
38
|
+
readBuf.unmap();
|
|
39
|
+
querySet.destroy();
|
|
40
|
+
resolveBuf.destroy();
|
|
41
|
+
readBuf.destroy();
|
|
42
|
+
// period = 1ns(按规范),换算毫秒
|
|
43
|
+
const deltaNs = Number(times[1] - times[0]);
|
|
44
|
+
return deltaNs / 1e6;
|
|
45
|
+
}
|
|
46
|
+
/** 设备诊断:注册错误/丢失回调;设备丢失时自动重建上下文并调用 onRebuild。
|
|
47
|
+
* 长跑页面(展览/大屏)的必需品。 */
|
|
48
|
+
export function watchDevice(opts) {
|
|
49
|
+
void GpuContext.get().then((ctx) => {
|
|
50
|
+
ctx.device.addEventListener?.('uncapturederror', (e) => {
|
|
51
|
+
opts.onError?.(String(e.error?.message ?? e), true);
|
|
52
|
+
});
|
|
53
|
+
void ctx.lost.then((info) => {
|
|
54
|
+
opts.onError?.(`GPU device lost: ${info.reason}`, false);
|
|
55
|
+
// 丢失后重置单例,下一次 GpuContext.get() 走全新设备
|
|
56
|
+
GpuContext.resetForTests?.();
|
|
57
|
+
void GpuContext.get().then((fresh) => opts.onRebuild?.(fresh)).catch(() => undefined);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** 推荐的画布 canvas 格式(一行助手的语义化封装)。 */
|
|
62
|
+
export function preferredCanvasFormat() {
|
|
63
|
+
return navigator.gpu.getPreferredCanvasFormat();
|
|
64
|
+
}
|
|
65
|
+
/** 画布 resize(DPR 上限策略):宽高物理像素 = clientSize × min(dpr, cap)。
|
|
66
|
+
* 返回是否实际改变了尺寸(没变则无需重建)。 */
|
|
67
|
+
export function resizeCanvas(canvas, dprCap = 2) {
|
|
68
|
+
const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
|
69
|
+
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
70
|
+
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
71
|
+
if (canvas.width === w && canvas.height === h)
|
|
72
|
+
return false;
|
|
73
|
+
canvas.width = w;
|
|
74
|
+
canvas.height = h;
|
|
75
|
+
return true;
|
|
76
|
+
}
|