wgpu-kit 0.9.10
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/LICENSE +21 -0
- package/README.md +96 -0
- package/README.zh-CN.md +99 -0
- package/dist/core/buffer.d.ts +21 -0
- package/dist/core/context.d.ts +15 -0
- package/dist/core/errors.d.ts +20 -0
- package/dist/core/kernel.d.ts +52 -0
- package/dist/core/layout.d.ts +32 -0
- package/dist/core/pingpong.d.ts +24 -0
- package/dist/core/raw.d.ts +9 -0
- package/dist/fields.js +585 -0
- package/dist/image.js +318 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +484 -0
- package/dist/interop/three.d.ts +37 -0
- package/dist/life.js +1407 -0
- package/dist/media.d.ts +22 -0
- package/dist/packs/fields/index.d.ts +27 -0
- package/dist/packs/image/index.d.ts +32 -0
- package/dist/packs/life/boids.d.ts +28 -0
- package/dist/packs/life/index.d.ts +9 -0
- package/dist/packs/life/map.d.ts +20 -0
- package/dist/packs/life/physarum.d.ts +28 -0
- package/dist/packs/life/tentacles.d.ts +29 -0
- package/dist/packs/life/turing.d.ts +40 -0
- package/dist/packs/particles/config.d.ts +47 -0
- package/dist/packs/particles/grid.d.ts +25 -0
- package/dist/packs/particles/index.d.ts +51 -0
- package/dist/packs/particles/presets.d.ts +25 -0
- package/dist/packs/particles/render.d.ts +20 -0
- package/dist/packs/particles/wgsl.d.ts +13 -0
- package/dist/particles.js +1245 -0
- package/dist/react/index.d.ts +18 -0
- package/dist/react.js +1289 -0
- package/dist/three.js +33 -0
- package/dist/vite.d.ts +38 -0
- package/dist/vite.js +30 -0
- package/package.json +93 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var WgpuKitError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var WebGPUUnavailableError = class extends WgpuKitError {
|
|
9
|
+
constructor(reason) {
|
|
10
|
+
super(
|
|
11
|
+
`\u5F53\u524D\u73AF\u5883\u4E0D\u53EF\u7528 WebGPU: ${reason}
|
|
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
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ParticlesSim } from '../packs/particles/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* three.js 互通(interop)—— 快照模式,零依赖、通用任何渲染器(WebGL/WebGPU)。
|
|
4
|
+
*
|
|
5
|
+
* const { points, update } = threePoints(sim, THREE, { size: 0.02 });
|
|
6
|
+
* scene.add(points);
|
|
7
|
+
* // 每帧渲染前:
|
|
8
|
+
* await update();
|
|
9
|
+
*
|
|
10
|
+
* 原理:从 GPU 读回位置快照填进 BufferAttribute(每帧一次 readback)。
|
|
11
|
+
* 零拷贝的 TSL storage 直通路径在路线图上(对渲染器有强绑定,不在 MVP 承诺内)。
|
|
12
|
+
*/
|
|
13
|
+
/** three.js 最小表面(按需注入,避免硬依赖) */
|
|
14
|
+
export interface ThreeAPI {
|
|
15
|
+
Points: new (geometry: unknown, material: unknown) => unknown;
|
|
16
|
+
BufferGeometry: new () => unknown;
|
|
17
|
+
BufferAttribute: new (array: Float32Array, itemSize: number) => {
|
|
18
|
+
set: (arr: ArrayLike<number>) => void;
|
|
19
|
+
needsUpdate: boolean;
|
|
20
|
+
};
|
|
21
|
+
PointsMaterial: new (params: {
|
|
22
|
+
size?: number;
|
|
23
|
+
color?: number;
|
|
24
|
+
sizeAttenuation?: boolean;
|
|
25
|
+
}) => unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface ThreePointsHandle {
|
|
28
|
+
/** 加进场景的 THREE.Points 实例 */
|
|
29
|
+
points: unknown;
|
|
30
|
+
/** 同步一帧位置快照(await;每帧调用) */
|
|
31
|
+
update(): Promise<void>;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function threePoints(sim: ParticlesSim, THREE: ThreeAPI, opts?: {
|
|
35
|
+
size?: number;
|
|
36
|
+
color?: number;
|
|
37
|
+
}): ThreePointsHandle;
|