wgpu-kit 1.1.0 → 1.1.1
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/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 +1 -1
- package/dist/core/errors.d.ts +18 -6
- package/dist/core/errors.js +28 -13
- package/dist/core/kernel.js +14 -11
- package/dist/core/layout.js +7 -6
- package/dist/core/pingpong.js +1 -1
- package/dist/core/raw.js +3 -3
- package/dist/media.js +3 -3
- package/dist/observe.js +2 -2
- package/dist/packs/grid/index.js +2 -2
- package/dist/packs/image/index.js +1 -1
- package/dist/packs/particles/config.js +4 -4
- package/dist/packs/particles/grid.js +39 -58
- package/dist/packs/particles/index.js +39 -53
- package/dist/packs/particles/presets.js +1 -1
- package/package.json +7 -2
package/dist/core/buffer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TYPES } from "./layout.js";
|
|
2
2
|
import { GpuContext } from "./context.js";
|
|
3
|
-
import { UsageError } from "./errors.js";
|
|
3
|
+
import { ERR, UsageError } from "./errors.js";
|
|
4
4
|
const TYPED_CTORS = {
|
|
5
5
|
Float32Array, Int32Array, Uint32Array,
|
|
6
6
|
};
|
|
@@ -16,6 +16,7 @@ export class Buffer {
|
|
|
16
16
|
#ctx;
|
|
17
17
|
#byteLength;
|
|
18
18
|
#stride;
|
|
19
|
+
#reading = null;
|
|
19
20
|
#staging = null;
|
|
20
21
|
constructor(ctx, kind, length, gpuBuffer) {
|
|
21
22
|
this.#ctx = ctx;
|
|
@@ -27,11 +28,11 @@ export class Buffer {
|
|
|
27
28
|
}
|
|
28
29
|
static async create(kind, length) {
|
|
29
30
|
if (!Number.isInteger(length) || length <= 0) {
|
|
30
|
-
throw new UsageError(`Buffer
|
|
31
|
+
throw new UsageError(ERR.BUFFER_CREATE, `Buffer length must be a positive integer, got: ${String(length)}`);
|
|
31
32
|
}
|
|
32
33
|
const def = TYPES[kind];
|
|
33
34
|
if (!def)
|
|
34
|
-
throw new UsageError(
|
|
35
|
+
throw new UsageError(ERR.BUFFER_CREATE, `Unknown Buffer kind "${String(kind)}". Available: ${Object.keys(TYPES).join(", ")}`);
|
|
35
36
|
const ctx = await GpuContext.get();
|
|
36
37
|
const gpuBuffer = ctx.device.createBuffer({
|
|
37
38
|
size: length * def.stride,
|
|
@@ -45,11 +46,11 @@ export class Buffer {
|
|
|
45
46
|
const def = TYPES[this.kind];
|
|
46
47
|
const ctor = TYPED_CTORS[def.typed];
|
|
47
48
|
if (!(data instanceof ctor)) {
|
|
48
|
-
throw new UsageError(`Buffer<${this.kind}>.write
|
|
49
|
+
throw new UsageError(ERR.BUFFER_WRITE, `Buffer<${this.kind}>.write expects ${def.typed}, got ${data.constructor?.name ?? typeof data}`);
|
|
49
50
|
}
|
|
50
51
|
const expected = this.length * def.comps;
|
|
51
52
|
if (data.length !== expected) {
|
|
52
|
-
throw new UsageError(`Buffer<${this.kind}>[${this.length}].write
|
|
53
|
+
throw new UsageError(ERR.BUFFER_WRITE, `Buffer<${this.kind}>[${this.length}].write expects ${expected} components, got ${data.length}`);
|
|
53
54
|
}
|
|
54
55
|
if (def.stride === def.size || def.comps === 1) {
|
|
55
56
|
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, data);
|
|
@@ -67,6 +68,17 @@ export class Buffer {
|
|
|
67
68
|
}
|
|
68
69
|
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
69
70
|
async read() {
|
|
71
|
+
if (this.#reading)
|
|
72
|
+
return this.#reading;
|
|
73
|
+
this.#reading = this.#doRead();
|
|
74
|
+
try {
|
|
75
|
+
return await this.#reading;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.#reading = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async #doRead() {
|
|
70
82
|
const def = TYPES[this.kind];
|
|
71
83
|
if (!this.#staging) {
|
|
72
84
|
this.#staging = this.#ctx.device.createBuffer({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for programmatic identification of wgpu-kit errors.
|
|
3
|
+
* These are stable string constants — safe to switch on in user code.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ERR: {
|
|
6
|
+
/** WebGPU is unavailable or the adapter could not be acquired */
|
|
7
|
+
readonly WGPU_UNAVAILABLE: "ERR_WGPU_UNAVAILABLE";
|
|
8
|
+
/** WGSL compilation failed */
|
|
9
|
+
readonly COMPILE: "ERR_COMPILE";
|
|
10
|
+
/** Invalid argument passed by the caller */
|
|
11
|
+
readonly USAGE: "ERR_USAGE";
|
|
12
|
+
/** A required uniform field is missing or has an invalid value */
|
|
13
|
+
readonly UNIFORM_FIELD: "ERR_UNIFORM_FIELD";
|
|
14
|
+
/** Uniform scalar types are not yet supported */
|
|
15
|
+
readonly UNIFORM_UNSUPPORTED: "ERR_UNIFORM_UNSUPPORTED";
|
|
16
|
+
/** workgroupSize is outside the valid range */
|
|
17
|
+
readonly WORKGROUP_SIZE: "ERR_WORKGROUP_SIZE";
|
|
18
|
+
/** A resource (Buffer) is missing from the resources map */
|
|
19
|
+
readonly RESOURCE_MISSING: "ERR_RESOURCE_MISSING";
|
|
20
|
+
/** A resource type does not match the kernel declaration */
|
|
21
|
+
readonly RESOURCE_TYPE: "ERR_RESOURCE_TYPE";
|
|
22
|
+
/** Resource lengths are inconsistent across state/inputs */
|
|
23
|
+
readonly RESOURCE_LENGTH: "ERR_RESOURCE_LENGTH";
|
|
24
|
+
/** Buffer.create received an invalid kind or length */
|
|
25
|
+
readonly BUFFER_CREATE: "ERR_BUFFER_CREATE";
|
|
26
|
+
/** Buffer.write type or component count mismatch */
|
|
27
|
+
readonly BUFFER_WRITE: "ERR_BUFFER_WRITE";
|
|
28
|
+
/** MediaRecorder is unavailable or the recording failed */
|
|
29
|
+
readonly MEDIA: "ERR_MEDIA";
|
|
30
|
+
/** timestamp-query is not supported on this device */
|
|
31
|
+
readonly TIMESTAMP_UNSUPPORTED: "ERR_TIMESTAMP_UNSUPPORTED";
|
|
32
|
+
/** A generic library error that doesn't fit any specific code */
|
|
33
|
+
readonly GENERIC: "ERR_GENERIC";
|
|
34
|
+
};
|
|
35
|
+
export type ErrorCode = (typeof ERR)[keyof typeof ERR];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for programmatic identification of wgpu-kit errors.
|
|
3
|
+
* These are stable string constants — safe to switch on in user code.
|
|
4
|
+
*/
|
|
5
|
+
export const ERR = {
|
|
6
|
+
/** WebGPU is unavailable or the adapter could not be acquired */
|
|
7
|
+
WGPU_UNAVAILABLE: 'ERR_WGPU_UNAVAILABLE',
|
|
8
|
+
/** WGSL compilation failed */
|
|
9
|
+
COMPILE: 'ERR_COMPILE',
|
|
10
|
+
/** Invalid argument passed by the caller */
|
|
11
|
+
USAGE: 'ERR_USAGE',
|
|
12
|
+
/** A required uniform field is missing or has an invalid value */
|
|
13
|
+
UNIFORM_FIELD: 'ERR_UNIFORM_FIELD',
|
|
14
|
+
/** Uniform scalar types are not yet supported */
|
|
15
|
+
UNIFORM_UNSUPPORTED: 'ERR_UNIFORM_UNSUPPORTED',
|
|
16
|
+
/** workgroupSize is outside the valid range */
|
|
17
|
+
WORKGROUP_SIZE: 'ERR_WORKGROUP_SIZE',
|
|
18
|
+
/** A resource (Buffer) is missing from the resources map */
|
|
19
|
+
RESOURCE_MISSING: 'ERR_RESOURCE_MISSING',
|
|
20
|
+
/** A resource type does not match the kernel declaration */
|
|
21
|
+
RESOURCE_TYPE: 'ERR_RESOURCE_TYPE',
|
|
22
|
+
/** Resource lengths are inconsistent across state/inputs */
|
|
23
|
+
RESOURCE_LENGTH: 'ERR_RESOURCE_LENGTH',
|
|
24
|
+
/** Buffer.create received an invalid kind or length */
|
|
25
|
+
BUFFER_CREATE: 'ERR_BUFFER_CREATE',
|
|
26
|
+
/** Buffer.write type or component count mismatch */
|
|
27
|
+
BUFFER_WRITE: 'ERR_BUFFER_WRITE',
|
|
28
|
+
/** MediaRecorder is unavailable or the recording failed */
|
|
29
|
+
MEDIA: 'ERR_MEDIA',
|
|
30
|
+
/** timestamp-query is not supported on this device */
|
|
31
|
+
TIMESTAMP_UNSUPPORTED: 'ERR_TIMESTAMP_UNSUPPORTED',
|
|
32
|
+
/** A generic library error that doesn't fit any specific code */
|
|
33
|
+
GENERIC: 'ERR_GENERIC',
|
|
34
|
+
};
|
package/dist/core/context.js
CHANGED
|
@@ -26,7 +26,7 @@ export class GpuContext {
|
|
|
26
26
|
}
|
|
27
27
|
static async #create() {
|
|
28
28
|
if (typeof navigator === 'undefined' || !('gpu' in navigator) || !navigator.gpu) {
|
|
29
|
-
throw new WebGPUUnavailableError('navigator.gpu
|
|
29
|
+
throw new WebGPUUnavailableError('navigator.gpu is not available');
|
|
30
30
|
}
|
|
31
31
|
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
32
32
|
if (!adapter)
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ErrorCode } from './codes.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Error hierarchy for wgpu-kit. All errors extend WgpuKitError and carry a
|
|
4
|
+
* stable machine-readable `code` for programmatic handling.
|
|
5
|
+
*/
|
|
2
6
|
export declare class WgpuKitError extends Error {
|
|
3
|
-
|
|
7
|
+
/** Stable error code, e.g. 'ERR_WGPU_UNAVAILABLE' — safe to switch on. */
|
|
8
|
+
readonly code: ErrorCode;
|
|
9
|
+
constructor(code: ErrorCode, message: string);
|
|
4
10
|
}
|
|
5
|
-
/**
|
|
11
|
+
/** WebGPU is unavailable or the adapter could not be acquired. */
|
|
6
12
|
export declare class WebGPUUnavailableError extends WgpuKitError {
|
|
7
13
|
constructor(reason: string);
|
|
8
14
|
}
|
|
9
|
-
/** WGSL
|
|
15
|
+
/** WGSL compilation failed. Line numbers are mapped back to user code. */
|
|
10
16
|
export declare class CompileError extends WgpuKitError {
|
|
11
17
|
constructor(kernelName: string, messages: readonly {
|
|
12
18
|
line: number;
|
|
13
19
|
msg: string;
|
|
14
20
|
}[], userCodeOffset: number);
|
|
15
21
|
}
|
|
16
|
-
/**
|
|
22
|
+
/** Caller passed an invalid argument (missing resource, type mismatch, bad length, etc.). */
|
|
17
23
|
export declare class UsageError extends WgpuKitError {
|
|
24
|
+
constructor(code: ErrorCode, message: string);
|
|
18
25
|
}
|
|
19
|
-
/**
|
|
26
|
+
/** Compute pipeline creation failed validation (e.g. too many storage buffers). */
|
|
27
|
+
export declare class PipelineError extends WgpuKitError {
|
|
28
|
+
constructor(label: string, detail: string);
|
|
29
|
+
}
|
|
30
|
+
export { ERR, type ErrorCode } from './codes.ts';
|
|
31
|
+
/** Create a compute pipeline with pushErrorScope to surface async validation errors. */
|
|
20
32
|
export declare function createComputePipelineChecked(device: GPUDevice, module: GPUShaderModule, label: string, entryPoint?: string): Promise<GPUComputePipeline>;
|
package/dist/core/errors.js
CHANGED
|
@@ -1,42 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { ERR } from "./codes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error hierarchy for wgpu-kit. All errors extend WgpuKitError and carry a
|
|
4
|
+
* stable machine-readable `code` for programmatic handling.
|
|
5
|
+
*/
|
|
2
6
|
export class WgpuKitError extends Error {
|
|
3
|
-
|
|
7
|
+
/** Stable error code, e.g. 'ERR_WGPU_UNAVAILABLE' — safe to switch on. */
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
4
10
|
super(message);
|
|
5
11
|
this.name = new.target.name;
|
|
12
|
+
this.code = code;
|
|
6
13
|
}
|
|
7
14
|
}
|
|
8
|
-
/**
|
|
15
|
+
/** WebGPU is unavailable or the adapter could not be acquired. */
|
|
9
16
|
export class WebGPUUnavailableError extends WgpuKitError {
|
|
10
17
|
constructor(reason) {
|
|
11
|
-
super(
|
|
12
|
-
'
|
|
13
|
-
' 可用 navigator.gpu 是否存在快速判断。');
|
|
18
|
+
super(ERR.WGPU_UNAVAILABLE, `WebGPU is unavailable: ${reason}\n` +
|
|
19
|
+
' Check: 1) Use Chrome/Edge 113+ or Safari 18+. 2) Enable WebGPU in headless mode. 3) Verify GPU drivers and hardware acceleration.');
|
|
14
20
|
}
|
|
15
21
|
}
|
|
16
|
-
/** WGSL
|
|
22
|
+
/** WGSL compilation failed. Line numbers are mapped back to user code. */
|
|
17
23
|
export class CompileError extends WgpuKitError {
|
|
18
24
|
constructor(kernelName, messages, userCodeOffset) {
|
|
19
25
|
const mapped = messages
|
|
20
26
|
.map((m) => {
|
|
21
27
|
const userLine = m.line - userCodeOffset;
|
|
22
|
-
const where = userLine > 0 ?
|
|
28
|
+
const where = userLine > 0 ? `your code, line ${userLine}` : `generated code, line ${m.line} (library issue — please file an issue)`;
|
|
23
29
|
return ` ${where}: ${m.msg}`;
|
|
24
30
|
})
|
|
25
31
|
.join('\n');
|
|
26
|
-
super(`kernel "${kernelName}" WGSL
|
|
32
|
+
super(ERR.COMPILE, `kernel "${kernelName}" WGSL compilation failed:\n${mapped}`);
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
|
-
/**
|
|
35
|
+
/** Caller passed an invalid argument (missing resource, type mismatch, bad length, etc.). */
|
|
30
36
|
export class UsageError extends WgpuKitError {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(code, message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Compute pipeline creation failed validation (e.g. too many storage buffers). */
|
|
42
|
+
export class PipelineError extends WgpuKitError {
|
|
43
|
+
constructor(label, detail) {
|
|
44
|
+
super(ERR.GENERIC, `compute pipeline "${label}" creation failed: ${detail}`);
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
|
-
|
|
47
|
+
export { ERR } from "./codes.js";
|
|
48
|
+
/** Create a compute pipeline with pushErrorScope to surface async validation errors. */
|
|
33
49
|
export async function createComputePipelineChecked(device, module, label, entryPoint = 'main') {
|
|
34
50
|
device.pushErrorScope('validation');
|
|
35
51
|
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint } });
|
|
36
52
|
const err = await device.popErrorScope();
|
|
37
53
|
if (err) {
|
|
38
|
-
throw new
|
|
39
|
-
常见原因:storage buffer 数超过每阶段上限(可向本库提 issue 申请 limits 支持)`);
|
|
54
|
+
throw new PipelineError(label, err.message);
|
|
40
55
|
}
|
|
41
56
|
return pipeline;
|
|
42
57
|
}
|
package/dist/core/kernel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
5
|
const RESERVED = new Set(['count']);
|
|
6
6
|
/** GPUBuffer 的稳定数字身份(WeakMap 分配,用于 bind group 缓存键) */
|
|
7
7
|
let nextBufferId = 0;
|
|
@@ -19,24 +19,24 @@ export function generateElementKernel(spec) {
|
|
|
19
19
|
const name = spec.name ?? 'kernel';
|
|
20
20
|
const workgroupSize = spec.workgroupSize ?? 64;
|
|
21
21
|
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
22
|
-
throw new UsageError(`workgroupSize
|
|
22
|
+
throw new UsageError(ERR.WORKGROUP_SIZE, `workgroupSize must be in 1..512, got: ${String(workgroupSize)}`);
|
|
23
23
|
}
|
|
24
24
|
const state = Object.entries(spec.state ?? {});
|
|
25
25
|
const inputs = Object.entries(spec.inputs ?? {});
|
|
26
26
|
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
27
27
|
if (state.length + inputs.length === 0) {
|
|
28
|
-
throw new UsageError(`elementKernel "${name}"
|
|
28
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `elementKernel "${name}" requires at least one state or inputs field`);
|
|
29
29
|
}
|
|
30
30
|
for (const [uName] of uniforms) {
|
|
31
31
|
if (RESERVED.has(uName))
|
|
32
|
-
throw new UsageError(`uniform
|
|
32
|
+
throw new UsageError(ERR.USAGE, `uniform name "${uName}" is reserved (count is auto-injected by the library)`);
|
|
33
33
|
}
|
|
34
34
|
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
35
35
|
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
36
|
-
throw new UsageError(`elementKernel "${name}"
|
|
36
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" has duplicate field names across state/inputs/uniforms`);
|
|
37
37
|
}
|
|
38
38
|
if (typeof spec.code !== 'string' || spec.code.trim().length === 0) {
|
|
39
|
-
throw new UsageError(`elementKernel "${name}"
|
|
39
|
+
throw new UsageError(ERR.USAGE, `elementKernel "${name}" is missing code (user WGSL function)`);
|
|
40
40
|
}
|
|
41
41
|
const uniformEntries = [...uniforms, ['count', 'u32']];
|
|
42
42
|
const uniformLayout = planUniform(uniformEntries);
|
|
@@ -143,17 +143,17 @@ export function elementKernel(spec) {
|
|
|
143
143
|
const f = orderedFields[i];
|
|
144
144
|
const buf = resources[f.key];
|
|
145
145
|
if (!buf)
|
|
146
|
-
throw new UsageError(`kernel "${normalized.name}".run
|
|
146
|
+
throw new UsageError(ERR.RESOURCE_MISSING, `kernel "${normalized.name}".run is missing resource "${f.key}"`);
|
|
147
147
|
const want = expectedKinds.get(f.key);
|
|
148
148
|
if (buf.kind !== want) {
|
|
149
|
-
throw new UsageError(
|
|
149
|
+
throw new UsageError(ERR.RESOURCE_TYPE, `Resource "${f.key}" type mismatch: expected ${want}, got ${buf.kind}`);
|
|
150
150
|
}
|
|
151
151
|
if (count === -1) {
|
|
152
152
|
count = buf.length;
|
|
153
153
|
firstKey = f.key;
|
|
154
154
|
}
|
|
155
155
|
else if (buf.length !== count) {
|
|
156
|
-
throw new UsageError(
|
|
156
|
+
throw new UsageError(ERR.RESOURCE_LENGTH, `Resource "${f.key}" length ${buf.length} does not match "${firstKey}" length ${count}`);
|
|
157
157
|
}
|
|
158
158
|
ordered.push(buf);
|
|
159
159
|
}
|
|
@@ -171,12 +171,15 @@ export function elementKernel(spec) {
|
|
|
171
171
|
let cacheKey = 0;
|
|
172
172
|
for (let i = 0; i < ordered.length; i++)
|
|
173
173
|
cacheKey = (cacheKey * 31 + bufId(ordered[i].gpuBuffer)) | 0;
|
|
174
|
-
|
|
174
|
+
const ids = ordered.map((b) => bufId(b.gpuBuffer));
|
|
175
|
+
const cached = bindGroupCache.get(cacheKey);
|
|
176
|
+
const identityMatch = cached && cached.ids.length === ids.length && cached.ids.every((id, idx) => id === ids[idx]);
|
|
177
|
+
let bg = identityMatch ? cached.bg : undefined;
|
|
175
178
|
if (!bg) {
|
|
176
179
|
const entries = [{ binding: 0, resource: { buffer: uniformBuffer } }];
|
|
177
180
|
ordered.forEach((buffer, i) => entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } }));
|
|
178
181
|
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
179
|
-
bindGroupCache.set(cacheKey, bg);
|
|
182
|
+
bindGroupCache.set(cacheKey, { bg, ids });
|
|
180
183
|
}
|
|
181
184
|
// —— 编码提交 ——
|
|
182
185
|
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
|
}
|
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,13 +1,13 @@
|
|
|
1
1
|
import { GpuContext } from "./context.js";
|
|
2
|
-
import { CompileError, UsageError } from "./errors.js";
|
|
2
|
+
import { CompileError, ERR, UsageError } from "./errors.js";
|
|
3
3
|
export function rawKernel(code, entryPoint = 'main', label = 'rawKernel') {
|
|
4
4
|
if (typeof code !== 'string' || code.trim().length === 0)
|
|
5
|
-
throw new UsageError('rawKernel
|
|
5
|
+
throw new UsageError(ERR.USAGE, 'rawKernel requires WGSL code');
|
|
6
6
|
let pipelinePromise = null;
|
|
7
7
|
return {
|
|
8
8
|
async run(entries, workgroups) {
|
|
9
9
|
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
10
|
-
throw new UsageError(`rawKernel.run
|
|
10
|
+
throw new UsageError(ERR.USAGE, `rawKernel.run workgroups must be a positive integer, got ${String(workgroups)}`);
|
|
11
11
|
}
|
|
12
12
|
const ctx = await GpuContext.get();
|
|
13
13
|
if (!pipelinePromise) {
|
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({
|
package/dist/packs/grid/index.js
CHANGED
|
@@ -7,9 +7,9 @@ const USIZE = 32;
|
|
|
7
7
|
export async function createNeighborGrid(config) {
|
|
8
8
|
const { count, worldHalf, cellSize, workgroupSize = WG } = config;
|
|
9
9
|
if (!Number.isInteger(count) || count <= 0)
|
|
10
|
-
throw new Error(`count
|
|
10
|
+
throw new Error(`count must be a positive integer, got ${String(count)}`);
|
|
11
11
|
if (!(cellSize > 0))
|
|
12
|
-
throw new Error(`cellSize
|
|
12
|
+
throw new Error(`cellSize must be positive, got ${String(cellSize)}`);
|
|
13
13
|
const gridSize = Math.max(1, Math.ceil((2 * worldHalf) / cellSize));
|
|
14
14
|
const cells = gridSize * gridSize;
|
|
15
15
|
const ctx = await GpuContext.get();
|
|
@@ -4,7 +4,7 @@ const OPS = ['grayscale', 'invert', 'edge', 'blur', 'sharpen', 'brightness', 'co
|
|
|
4
4
|
const OP_IDS = { grayscale: 0, invert: 1, edge: 2, blur: 3, sharpen: 4, brightness: 5, contrast: 6 };
|
|
5
5
|
export async function applyImage(source, target, ops) {
|
|
6
6
|
if (ops.length === 0)
|
|
7
|
-
throw new Error('applyImage
|
|
7
|
+
throw new Error('applyImage requires at least one operator');
|
|
8
8
|
const width = 'naturalWidth' in source ? source.naturalWidth : source.width;
|
|
9
9
|
const height = 'naturalHeight' in source ? source.naturalHeight : source.height;
|
|
10
10
|
const ctx = await GpuContext.get();
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { resolveMatrix, hashSeed } from "./presets.js";
|
|
2
|
-
import { UsageError } from "../../core/errors.js";
|
|
2
|
+
import { ERR, UsageError } from "../../core/errors.js";
|
|
3
3
|
const MODES = ['n2', 'tiled', 'grid'];
|
|
4
4
|
export function resolveConfig(config = {}) {
|
|
5
5
|
const { count = 8192, forces = 'cells', mode = 'grid', // 基准数据驱动:v0.4 起 grid 全面优于 tiled(0.54ms vs 3.62ms @16k),见 benchmarks.md
|
|
6
6
|
color = 'species', bounds = 'wrap', seed = 'wgpu-kit', rMax = 0.12, beta = 0.3, forceFactor = 10, frictionHalfLife = 0.04, dt = 0.02, pointSize = 0.004, maxNeighbors = 8100, } = config;
|
|
7
7
|
if (!Number.isInteger(count) || count <= 0 || count > 1_000_000) {
|
|
8
|
-
throw new UsageError(`count
|
|
8
|
+
throw new UsageError(ERR.USAGE, `count must be an integer in 1..1_000_000, got: ${String(count)}`);
|
|
9
9
|
}
|
|
10
10
|
if (!MODES.includes(mode)) {
|
|
11
|
-
throw new UsageError(`mode
|
|
11
|
+
throw new UsageError(ERR.USAGE, `mode must be one of ${MODES.join(" | ")}, got: "${String(mode)}"`);
|
|
12
12
|
}
|
|
13
13
|
if (mode === 'n2' && count > 32_000) {
|
|
14
|
-
throw new UsageError(`mode='n2'
|
|
14
|
+
throw new UsageError(ERR.USAGE, `mode='n2' is recommended for count <= 20000 (got ${count}); use 'tiled' or 'grid' for larger counts`);
|
|
15
15
|
}
|
|
16
16
|
const seedStr = String(seed);
|
|
17
17
|
return {
|
|
@@ -40,12 +40,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
42
42
|
export function gridScanWgsl() {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
43
|
+
// 单 workgroup 分块扫描:256-cell 块循环推进,块内 Hillis-Steele,块间用
|
|
44
|
+
// workgroup 级 carry 串接。此前版本按多 workgroup 分两级写,但 dispatch(1)
|
|
45
|
+
// 只有 block 0 在跑 —— 256 格之外 cellStart 永远是 0、cellCount 永不清零,
|
|
46
|
+
// 对应粒子受力恒零被摩擦冻住(屏幕上出现水平"冻结带")。改成单 workgroup
|
|
47
|
+
// 循环后 barrier 是合法同步,正确性不依赖任何跨 workgroup 时序;
|
|
48
|
+
// cell 上限 65536 = 256 块,每块一轮微秒级,性能无虞。顺带把 cellCount
|
|
49
|
+
// 归零给下一帧。
|
|
49
50
|
return /* wgsl */ `
|
|
50
51
|
struct Params {
|
|
51
52
|
count: u32, _pad0: u32,
|
|
@@ -56,70 +57,50 @@ struct Params {
|
|
|
56
57
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
57
58
|
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
58
59
|
@group(0) @binding(2) var<storage, read_write> cellStart: array<u32>;
|
|
59
|
-
@group(0) @binding(3) var<storage, read_write> cellFill: array<
|
|
60
|
-
@group(0) @binding(4) var<storage, read_write> blockSums: array<atomic<u32>>;
|
|
60
|
+
@group(0) @binding(3) var<storage, read_write> cellFill: array<u32>;
|
|
61
61
|
|
|
62
62
|
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
63
|
+
var<workgroup> carry: u32;
|
|
63
64
|
|
|
64
|
-
// Pass A:块内排他扫描。cellFill[c] = 块内排他前缀(临时);blockSums[wid] = 块总和
|
|
65
65
|
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
66
|
-
fn
|
|
66
|
+
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
|
67
67
|
let tid = lid.x;
|
|
68
68
|
let wg = ${SCAN_WORKGROUP}u;
|
|
69
|
-
let base = wid.x * wg;
|
|
70
69
|
let cells = params.cells;
|
|
71
|
-
let
|
|
72
|
-
|
|
70
|
+
let numChunks = (cells + wg - 1u) / wg;
|
|
71
|
+
if (tid == 0u) { carry = 0u; }
|
|
73
72
|
workgroupBarrier();
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
workgroupBarrier();
|
|
80
|
-
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
73
|
+
for (var ch = 0u; ch < numChunks; ch++) {
|
|
74
|
+
let idx = ch * wg + tid;
|
|
75
|
+
let inRange = idx < cells;
|
|
76
|
+
let v0 = select(0u, atomicLoad(&cellCount[idx]), inRange);
|
|
77
|
+
partial[tid] = v0;
|
|
81
78
|
workgroupBarrier();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (tid >= offset) { v = partial[tid - offset]; }
|
|
79
|
+
// 块内含前缀(Hillis-Steele)
|
|
80
|
+
var offset = 1u;
|
|
81
|
+
loop {
|
|
82
|
+
if (offset >= wg) { break; }
|
|
83
|
+
var v = 0u;
|
|
84
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
85
|
+
workgroupBarrier();
|
|
86
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
87
|
+
workgroupBarrier();
|
|
88
|
+
offset = offset << 1u;
|
|
89
|
+
}
|
|
90
|
+
// 排他:start = carry + 块内前缀(不含自身);fill 是 scatter 的原子填充
|
|
91
|
+
// 游标,初始化为段起点(与通用包 NeighborGrid 同语义)——scatter 填完一格
|
|
92
|
+
// 后 fill 恰好 = start + count,力核读 [start, fill) 才不会多扫下一格的粒子。
|
|
93
|
+
if (inRange) {
|
|
94
|
+
let excl = partial[tid] - v0;
|
|
95
|
+
cellStart[idx] = carry + excl;
|
|
96
|
+
cellFill[idx] = carry + excl;
|
|
97
|
+
atomicStore(&cellCount[idx], 0u);
|
|
98
|
+
}
|
|
99
|
+
// 所有线程读完 partial/写完 carry 后才能进入下一块
|
|
104
100
|
workgroupBarrier();
|
|
105
|
-
if (tid
|
|
101
|
+
if (tid == wg - 1u) { carry = carry + partial[wg - 1u]; }
|
|
106
102
|
workgroupBarrier();
|
|
107
|
-
offset = offset << 1u;
|
|
108
103
|
}
|
|
109
|
-
atomicStore(&blockSums[tid], partial[tid] - v0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Pass C:加块基址 → 最终 start/fill;counts 归零供下一帧
|
|
113
|
-
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
114
|
-
fn main_scan_apply(@builtin(global_invocation_id) gid: vec3u) {
|
|
115
|
-
let i = gid.x;
|
|
116
|
-
if (i >= params.cells) { return; }
|
|
117
|
-
let block = i / ${SCAN_WORKGROUP}u;
|
|
118
|
-
let base = atomicLoad(&blockSums[block]);
|
|
119
|
-
let excl = atomicLoad(&cellFill[i]);
|
|
120
|
-
cellStart[i] = excl + base;
|
|
121
|
-
atomicExchange(&cellFill[i], excl + base);
|
|
122
|
-
atomicStore(&cellCount[i], 0u);
|
|
123
104
|
}
|
|
124
105
|
`;
|
|
125
106
|
}
|
|
@@ -37,8 +37,9 @@ export async function particles(config = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
const phys = { rMax: cfg.rMax, beta: cfg.beta, forceFactor: cfg.forceFactor, frictionHalfLife: cfg.frictionHalfLife, dt: cfg.dt };
|
|
39
39
|
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: 'particles-params' });
|
|
40
|
-
// grid 尺寸由 rMax 决定(
|
|
41
|
-
|
|
40
|
+
// grid 尺寸由 rMax 决定(floor 确保格宽 ≥ rMax → 3×3 邻域完整覆盖交互半径;
|
|
41
|
+
// ceil 会导致格宽 < rMax,3×3 不够覆盖,边界粒子漏配邻居)
|
|
42
|
+
const gridSizeOf = (rMax, half = worldHalf) => Math.max(4, Math.floor((2 * half) / Math.max(rMax, 1e-3)));
|
|
42
43
|
let gridSize = gridSizeOf(phys.rMax, worldHalf);
|
|
43
44
|
const writeUniform = (dt) => {
|
|
44
45
|
const buf = new ArrayBuffer(USIZE);
|
|
@@ -83,7 +84,6 @@ export async function particles(config = {}) {
|
|
|
83
84
|
g.partial.destroy();
|
|
84
85
|
g.sortedPos.destroy();
|
|
85
86
|
g.sortedSp.destroy();
|
|
86
|
-
g.blockSums.destroy();
|
|
87
87
|
};
|
|
88
88
|
const buildGrid = async (size) => {
|
|
89
89
|
const cells = size * size;
|
|
@@ -97,14 +97,11 @@ export async function particles(config = {}) {
|
|
|
97
97
|
const mScatter = await compile(gridScatterWgsl(), 'grid-scatter');
|
|
98
98
|
const mForce = await compile(gridForceWgsl(4), 'grid-force');
|
|
99
99
|
const pCounts = await makePipeline(mCounts, 'main', 'grid-counts');
|
|
100
|
-
const
|
|
101
|
-
const pScanBases = await makePipeline(mScan, 'main_scan_bases', 'grid-scan-bases');
|
|
102
|
-
const pScanApply = await makePipeline(mScan, 'main_scan_apply', 'grid-scan-apply');
|
|
100
|
+
const pScan = await makePipeline(mScan, 'main', 'grid-scan');
|
|
103
101
|
const pScatter = await makePipeline(mScatter, 'main', 'grid-scatter');
|
|
104
102
|
const pForceCell = await makePipeline(mForce, 'main_force_cell', 'grid-force-cell');
|
|
105
103
|
const pForceInt = await makePipeline(mForce, 'main_force_integrate', 'grid-force-integrate');
|
|
106
104
|
const partial = await Buffer.create('vec2f', cfg.count * 9); // (粒子 × 3×3 格) 部分力
|
|
107
|
-
const blockSums = await Buffer.create('u32', Math.ceil(cells / 256)); // 二级扫描块和
|
|
108
105
|
const sortedPos = await Buffer.create('vec2f', cfg.count); // 按格子序重排的副本(合并访问)
|
|
109
106
|
const sortedSp = await Buffer.create('u32', cfg.count);
|
|
110
107
|
const bgCounts = (readPos) => device.createBindGroup({
|
|
@@ -115,34 +112,13 @@ export async function particles(config = {}) {
|
|
|
115
112
|
{ binding: 2, resource: { buffer: count.gpuBuffer } },
|
|
116
113
|
],
|
|
117
114
|
});
|
|
118
|
-
const
|
|
119
|
-
layout:
|
|
115
|
+
const bgScan = device.createBindGroup({
|
|
116
|
+
layout: pScan.getBindGroupLayout(0),
|
|
120
117
|
entries: [
|
|
121
118
|
{ binding: 0, resource: { buffer: uniform } },
|
|
122
119
|
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
123
120
|
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
124
121
|
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
125
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
const bgScanBases = device.createBindGroup({
|
|
129
|
-
layout: pScanBases.getBindGroupLayout(0),
|
|
130
|
-
entries: [
|
|
131
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
132
|
-
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
133
|
-
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
134
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
135
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
136
|
-
],
|
|
137
|
-
});
|
|
138
|
-
const bgScanApply = device.createBindGroup({
|
|
139
|
-
layout: pScanApply.getBindGroupLayout(0),
|
|
140
|
-
entries: [
|
|
141
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
142
|
-
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
143
|
-
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
144
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
145
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
146
122
|
],
|
|
147
123
|
});
|
|
148
124
|
const bgScatter = (readPos) => device.createBindGroup({
|
|
@@ -192,11 +168,10 @@ export async function particles(config = {}) {
|
|
|
192
168
|
};
|
|
193
169
|
const state = {
|
|
194
170
|
size,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
pCounts, pScanBlocks, pScanBases, pScanApply, pScatter, pForceCell, pForceInt,
|
|
171
|
+
count, start, fill, order, partial, sortedPos, sortedSp,
|
|
172
|
+
pCounts, pScan, pScatter, pForceCell, pForceInt,
|
|
198
173
|
bgCountsA: bgCounts(sideA.pos), bgCountsB: bgCounts(sideB.pos),
|
|
199
|
-
|
|
174
|
+
bgScan,
|
|
200
175
|
bgScatterA: bgScatter(sideA.pos), bgScatterB: bgScatter(sideB.pos),
|
|
201
176
|
bgForceCellAB: bgForceCell(sideA.pos), bgForceCellBA: bgForceCell(sideB.pos),
|
|
202
177
|
bgIntegrateAB: bgIntegrate(sideA, sideB), bgIntegrateBA: bgIntegrate(sideB, sideA),
|
|
@@ -259,28 +234,39 @@ export async function particles(config = {}) {
|
|
|
259
234
|
grid.bgForceCellRebuild(sideA.pos);
|
|
260
235
|
gridBindGroupsDirty = false;
|
|
261
236
|
}
|
|
237
|
+
const enc = device.createCommandEncoder();
|
|
262
238
|
if (grid) {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
239
|
+
// 每个 build 阶段独立 pass:同 pass 内跨 dispatch 的存储可见性规范不保证
|
|
240
|
+
// (Dawn/Windows 实测出现过旧数据),counts→scan→scatter 是严格串行依赖。
|
|
241
|
+
// 单 encoder 内 pass 按序执行且天然可见,不增加 submit 次数。
|
|
242
|
+
const passCounts = enc.beginComputePass();
|
|
243
|
+
passCounts.setPipeline(grid.pCounts);
|
|
244
|
+
passCounts.setBindGroup(0, useAB ? grid.bgCountsA : grid.bgCountsB);
|
|
245
|
+
passCounts.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
246
|
+
passCounts.end();
|
|
247
|
+
const passScan = enc.beginComputePass();
|
|
248
|
+
passScan.setPipeline(grid.pScan);
|
|
249
|
+
passScan.setBindGroup(0, grid.bgScan);
|
|
250
|
+
passScan.dispatchWorkgroups(1);
|
|
251
|
+
passScan.end();
|
|
252
|
+
const passScatter = enc.beginComputePass();
|
|
253
|
+
passScatter.setPipeline(grid.pScatter);
|
|
254
|
+
passScatter.setBindGroup(0, useAB ? grid.bgScatterA : grid.bgScatterB);
|
|
255
|
+
passScatter.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
256
|
+
passScatter.end();
|
|
257
|
+
const passB = enc.beginComputePass();
|
|
258
|
+
passB.setPipeline(grid.pForceCell);
|
|
259
|
+
passB.setBindGroup(0, useAB ? grid.bgForceCellAB : grid.bgForceCellBA);
|
|
260
|
+
passB.dispatchWorkgroups(Math.ceil((cfg.count * 9) / WORKGROUP));
|
|
261
|
+
passB.end();
|
|
262
|
+
const passC = enc.beginComputePass();
|
|
263
|
+
passC.setPipeline(grid.pForceInt);
|
|
264
|
+
passC.setBindGroup(0, useAB ? grid.bgIntegrateAB : grid.bgIntegrateBA);
|
|
265
|
+
passC.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
266
|
+
passC.end();
|
|
267
|
+
device.queue.submit([enc.finish()]);
|
|
281
268
|
}
|
|
282
269
|
else {
|
|
283
|
-
const enc = device.createCommandEncoder();
|
|
284
270
|
const pass = enc.beginComputePass();
|
|
285
271
|
pass.setPipeline(simPipeline);
|
|
286
272
|
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
@@ -67,7 +67,7 @@ export function resolveMatrix(forces, seed) {
|
|
|
67
67
|
if (typeof forces === 'string') {
|
|
68
68
|
const preset = FORCE_PRESETS[forces];
|
|
69
69
|
if (!preset) {
|
|
70
|
-
throw new Error(
|
|
70
|
+
throw new Error(`Unknown force preset "${forces}". Available: ${Object.keys(FORCE_PRESETS).join(", ")}, random`);
|
|
71
71
|
}
|
|
72
72
|
return preset;
|
|
73
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wgpu-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Creative-coding GPU toolkit for the browser. 200k-particle physics at 120fps in 5 lines of code. WebGPU compute without the boilerplate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -103,5 +103,10 @@
|
|
|
103
103
|
"bugs": {
|
|
104
104
|
"url": "https://github.com/nanfengw0w/wgpu-kit/issues"
|
|
105
105
|
},
|
|
106
|
-
"homepage": "https://github.com/nanfengw0w/wgpu-kit#readme"
|
|
106
|
+
"homepage": "https://github.com/nanfengw0w/wgpu-kit#readme",
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"react": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
107
112
|
}
|