wgpu-kit 1.0.3 → 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/README.cn.md +3 -3
- package/README.md +56 -16
- 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.d.ts +39 -0
- package/dist/packs/grid/index.js +187 -0
- package/dist/packs/image/index.js +1 -1
- package/dist/packs/life/boids.js +7 -103
- package/dist/packs/particles/config.d.ts +2 -1
- package/dist/packs/particles/config.js +5 -5
- package/dist/packs/particles/grid.js +35 -46
- package/dist/packs/particles/index.js +23 -22
- package/dist/packs/particles/presets.js +1 -1
- package/package.json +30 -11
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { GpuContext } from "../../core/context.js";
|
|
2
|
+
import { Buffer } from "../../core/buffer.js";
|
|
3
|
+
import { CompileError } from "../../core/errors.js";
|
|
4
|
+
const WG = 64;
|
|
5
|
+
const SCAN = 256;
|
|
6
|
+
const USIZE = 32;
|
|
7
|
+
export async function createNeighborGrid(config) {
|
|
8
|
+
const { count, worldHalf, cellSize, workgroupSize = WG } = config;
|
|
9
|
+
if (!Number.isInteger(count) || count <= 0)
|
|
10
|
+
throw new Error(`count must be a positive integer, got ${String(count)}`);
|
|
11
|
+
if (!(cellSize > 0))
|
|
12
|
+
throw new Error(`cellSize must be positive, got ${String(cellSize)}`);
|
|
13
|
+
const gridSize = Math.max(1, Math.ceil((2 * worldHalf) / cellSize));
|
|
14
|
+
const cells = gridSize * gridSize;
|
|
15
|
+
const ctx = await GpuContext.get();
|
|
16
|
+
const device = ctx.device;
|
|
17
|
+
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: 'ngrid-params' });
|
|
18
|
+
const writeUniform = () => {
|
|
19
|
+
const b = new ArrayBuffer(USIZE);
|
|
20
|
+
const v = new DataView(b);
|
|
21
|
+
v.setUint32(0, count, true);
|
|
22
|
+
v.setUint32(4, 0, true);
|
|
23
|
+
v.setFloat32(8, worldHalf, true);
|
|
24
|
+
v.setUint32(12, gridSize, true);
|
|
25
|
+
v.setUint32(16, cells, true);
|
|
26
|
+
v.setUint32(20, 0, true);
|
|
27
|
+
v.setUint32(24, 0, true);
|
|
28
|
+
v.setUint32(28, 0, true);
|
|
29
|
+
device.queue.writeBuffer(uniform, 0, b);
|
|
30
|
+
};
|
|
31
|
+
writeUniform();
|
|
32
|
+
const cellCount = await Buffer.create('u32', cells);
|
|
33
|
+
const cellStart = await Buffer.create('u32', cells);
|
|
34
|
+
const cellFill = await Buffer.create('u32', cells);
|
|
35
|
+
const order = await Buffer.create('u32', count);
|
|
36
|
+
cellCount.write(new Uint32Array(cells));
|
|
37
|
+
const module = device.createShaderModule({ code: gridWgsl(), label: 'ngrid' });
|
|
38
|
+
const info = await module.getCompilationInfo();
|
|
39
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
40
|
+
if (errors.length > 0)
|
|
41
|
+
throw new CompileError('ngrid', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
42
|
+
const pCounts = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_counts' } });
|
|
43
|
+
const pScan = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scan' } });
|
|
44
|
+
const pScatter = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scatter' } });
|
|
45
|
+
const bgCounts = (pos) => device.createBindGroup({
|
|
46
|
+
layout: pCounts.getBindGroupLayout(0),
|
|
47
|
+
entries: [
|
|
48
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
49
|
+
{ binding: 1, resource: { buffer: pos.gpuBuffer } },
|
|
50
|
+
{ binding: 2, resource: { buffer: cellCount.gpuBuffer } },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
const bgScan = device.createBindGroup({
|
|
54
|
+
layout: pScan.getBindGroupLayout(0),
|
|
55
|
+
entries: [
|
|
56
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
57
|
+
{ binding: 1, resource: { buffer: cellCount.gpuBuffer } },
|
|
58
|
+
{ binding: 2, resource: { buffer: cellStart.gpuBuffer } },
|
|
59
|
+
{ binding: 3, resource: { buffer: cellFill.gpuBuffer } },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
const bgScatter = (pos) => device.createBindGroup({
|
|
63
|
+
layout: pScatter.getBindGroupLayout(0),
|
|
64
|
+
entries: [
|
|
65
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
66
|
+
{ binding: 1, resource: { buffer: pos.gpuBuffer } },
|
|
67
|
+
{ binding: 2, resource: { buffer: cellFill.gpuBuffer } },
|
|
68
|
+
{ binding: 3, resource: { buffer: order.gpuBuffer } },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
gridSize,
|
|
73
|
+
cells,
|
|
74
|
+
cellStart,
|
|
75
|
+
cellFill,
|
|
76
|
+
order,
|
|
77
|
+
update(pos) {
|
|
78
|
+
writeUniform();
|
|
79
|
+
const enc = device.createCommandEncoder();
|
|
80
|
+
const pass = enc.beginComputePass();
|
|
81
|
+
// 独立 pass:实测同 pass 连续 dispatch 存在旧数据可见性问题(Dawn/Windows)
|
|
82
|
+
pass.setPipeline(pCounts);
|
|
83
|
+
pass.setBindGroup(0, bgCounts(pos));
|
|
84
|
+
pass.dispatchWorkgroups(Math.ceil(count / WG));
|
|
85
|
+
pass.end();
|
|
86
|
+
const pass2 = enc.beginComputePass();
|
|
87
|
+
pass2.setPipeline(pScan);
|
|
88
|
+
pass2.setBindGroup(0, bgScan);
|
|
89
|
+
pass2.dispatchWorkgroups(1);
|
|
90
|
+
pass2.end();
|
|
91
|
+
const pass3 = enc.beginComputePass();
|
|
92
|
+
pass3.setPipeline(pScatter);
|
|
93
|
+
pass3.setBindGroup(0, bgScatter(pos));
|
|
94
|
+
pass3.dispatchWorkgroups(Math.ceil(count / WG));
|
|
95
|
+
pass3.end();
|
|
96
|
+
device.queue.submit([enc.finish()]);
|
|
97
|
+
},
|
|
98
|
+
destroy() {
|
|
99
|
+
cellCount.destroy();
|
|
100
|
+
cellStart.destroy();
|
|
101
|
+
cellFill.destroy();
|
|
102
|
+
order.destroy();
|
|
103
|
+
uniform.destroy();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function gridWgsl() {
|
|
108
|
+
return /* wgsl */ `
|
|
109
|
+
struct Params {
|
|
110
|
+
count: u32, _pad0: u32,
|
|
111
|
+
worldHalf: f32, gridSize: u32, cells: u32,
|
|
112
|
+
_p0: u32, _p1: u32, _p2: u32,
|
|
113
|
+
};
|
|
114
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
115
|
+
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
116
|
+
@group(0) @binding(2) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
117
|
+
@group(0) @binding(3) var<storage, read_write> cellStart: array<u32>;
|
|
118
|
+
@group(0) @binding(4) var<storage, read_write> cellFill: array<atomic<u32>>;
|
|
119
|
+
@group(0) @binding(5) var<storage, read_write> order: array<u32>;
|
|
120
|
+
|
|
121
|
+
fn cellOf(p: vec2f) -> u32 {
|
|
122
|
+
let g = i32(params.gridSize);
|
|
123
|
+
let span = params.worldHalf * 2.0;
|
|
124
|
+
let cx = clamp(i32(floor((p.x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
125
|
+
let cy = clamp(i32(floor((p.y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
126
|
+
return u32(cy) * u32(g) + u32(cx);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@compute @workgroup_size(${WG})
|
|
130
|
+
fn main_counts(@builtin(global_invocation_id) gid: vec3u) {
|
|
131
|
+
let i = gid.x;
|
|
132
|
+
if (i >= params.count) { return; }
|
|
133
|
+
atomicAdd(&cellCount[cellOf(posIn[i])], 1u);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
var<workgroup> partial: array<u32, ${SCAN}>;
|
|
137
|
+
@compute @workgroup_size(${SCAN})
|
|
138
|
+
fn main_scan(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
|
|
139
|
+
let tid = lid.x;
|
|
140
|
+
let cells = params.cells;
|
|
141
|
+
let wg = ${SCAN}u;
|
|
142
|
+
let chunks = (cells + wg - 1u) / wg;
|
|
143
|
+
|
|
144
|
+
// ① 本 workgroup 负责的 chunk 局部和
|
|
145
|
+
var local = 0u;
|
|
146
|
+
for (var c = 0u; c < chunks; c++) {
|
|
147
|
+
let idx = c * wg + tid;
|
|
148
|
+
if (idx < cells) { local = local + atomicLoad(&cellCount[idx]); }
|
|
149
|
+
}
|
|
150
|
+
partial[tid] = local;
|
|
151
|
+
workgroupBarrier();
|
|
152
|
+
|
|
153
|
+
// ② 局部和的含前缀扫描(Hillis-Steele)
|
|
154
|
+
var offset = 1u;
|
|
155
|
+
loop {
|
|
156
|
+
if (offset >= wg) { break; }
|
|
157
|
+
var v = 0u;
|
|
158
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
159
|
+
workgroupBarrier();
|
|
160
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
161
|
+
workgroupBarrier();
|
|
162
|
+
offset = offset << 1u;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ③ chunk 基址 → start/fill,顺带把 count 归零给下一帧
|
|
166
|
+
var run = 0u;
|
|
167
|
+
if (tid > 0u) { run = partial[tid - 1u]; }
|
|
168
|
+
for (var c = 0u; c < chunks; c++) {
|
|
169
|
+
let idx = c * wg + tid;
|
|
170
|
+
if (idx < cells) {
|
|
171
|
+
cellStart[idx] = run;
|
|
172
|
+
atomicStore(&cellFill[idx], run);
|
|
173
|
+
run = run + atomicLoad(&cellCount[idx]);
|
|
174
|
+
atomicStore(&cellCount[idx], 0u);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@compute @workgroup_size(${WG})
|
|
180
|
+
fn main_scatter(@builtin(global_invocation_id) gid: vec3u) {
|
|
181
|
+
let i = gid.x;
|
|
182
|
+
if (i >= params.count) { return; }
|
|
183
|
+
let slot = atomicAdd(&cellFill[cellOf(posIn[i])], 1u);
|
|
184
|
+
order[slot] = i;
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
@@ -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();
|
package/dist/packs/life/boids.js
CHANGED
|
@@ -2,10 +2,11 @@ import { GpuContext } from "../../core/context.js";
|
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { PingPong } from "../../core/pingpong.js";
|
|
4
4
|
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { createNeighborGrid } from "../grid/index.js";
|
|
5
6
|
import { mulberry32 } from "../particles/presets.js";
|
|
6
7
|
const WG = 64;
|
|
7
8
|
export async function boids(config = {}) {
|
|
8
|
-
const { count: N =
|
|
9
|
+
const { count: N = 1200, perception = 0.05, maxSpeed = 0.012, wSep = 1.6, wAli = 1.0, wCoh = 0.8, size = 0.009, seed = 'boids', } = config;
|
|
9
10
|
const seedHash = typeof seed === 'string' ? hashStr(seed) : (seed ?? 3);
|
|
10
11
|
const gridSize = Math.max(4, Math.ceil(2 / perception));
|
|
11
12
|
const ctx = await GpuContext.get();
|
|
@@ -46,20 +47,12 @@ export async function boids(config = {}) {
|
|
|
46
47
|
device.queue.writeBuffer(uniform, 0, b);
|
|
47
48
|
};
|
|
48
49
|
writeUniform();
|
|
49
|
-
const cells = gridSize * gridSize;
|
|
50
|
-
const cellCount = await Buffer.create('u32', cells);
|
|
51
|
-
const cellStart = await Buffer.create('u32', cells);
|
|
52
|
-
const cellFill = await Buffer.create('u32', cells);
|
|
53
|
-
const order = await Buffer.create('u32', N);
|
|
54
|
-
cellCount.write(new Uint32Array(cells));
|
|
55
50
|
const module = device.createShaderModule({ code: boidsWgsl(size), label: 'boids' });
|
|
56
51
|
const info = await module.getCompilationInfo();
|
|
57
52
|
const errors = info.messages.filter((m) => m.type === 'error');
|
|
58
53
|
if (errors.length > 0)
|
|
59
54
|
throw new CompileError('boids', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
60
|
-
const
|
|
61
|
-
const pScan = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scan' } });
|
|
62
|
-
const pScatter = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scatter' } });
|
|
55
|
+
const neighborGrid = await createNeighborGrid({ count: N, worldHalf: 1.0, cellSize: perception });
|
|
63
56
|
const pForce = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_force' } });
|
|
64
57
|
const pRender = device.createRenderPipeline({
|
|
65
58
|
layout: 'auto',
|
|
@@ -67,32 +60,6 @@ export async function boids(config = {}) {
|
|
|
67
60
|
fragment: { module, entryPoint: 'fs', targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }] },
|
|
68
61
|
primitive: { topology: 'triangle-list' },
|
|
69
62
|
});
|
|
70
|
-
const bgCounts = (read) => device.createBindGroup({
|
|
71
|
-
layout: pCounts.getBindGroupLayout(0),
|
|
72
|
-
entries: [
|
|
73
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
74
|
-
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
75
|
-
{ binding: 2, resource: { buffer: cellCount.gpuBuffer } },
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
const bgScan = device.createBindGroup({
|
|
79
|
-
layout: pScan.getBindGroupLayout(0),
|
|
80
|
-
entries: [
|
|
81
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
82
|
-
{ binding: 2, resource: { buffer: cellCount.gpuBuffer } },
|
|
83
|
-
{ binding: 3, resource: { buffer: cellStart.gpuBuffer } },
|
|
84
|
-
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
85
|
-
],
|
|
86
|
-
});
|
|
87
|
-
const bgScatter = (read) => device.createBindGroup({
|
|
88
|
-
layout: pScatter.getBindGroupLayout(0),
|
|
89
|
-
entries: [
|
|
90
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
91
|
-
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
92
|
-
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
93
|
-
{ binding: 5, resource: { buffer: order.gpuBuffer } },
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
63
|
const bgForce = (read, write) => device.createBindGroup({
|
|
97
64
|
layout: pForce.getBindGroupLayout(0),
|
|
98
65
|
entries: [
|
|
@@ -101,9 +68,9 @@ export async function boids(config = {}) {
|
|
|
101
68
|
{ binding: 6, resource: { buffer: read.vel.gpuBuffer } },
|
|
102
69
|
{ binding: 7, resource: { buffer: write.pos.gpuBuffer } },
|
|
103
70
|
{ binding: 8, resource: { buffer: write.vel.gpuBuffer } },
|
|
104
|
-
{ binding: 3, resource: { buffer: cellStart.gpuBuffer } },
|
|
105
|
-
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
106
|
-
{ binding: 5, resource: { buffer: order.gpuBuffer } },
|
|
71
|
+
{ binding: 3, resource: { buffer: neighborGrid.cellStart.gpuBuffer } },
|
|
72
|
+
{ binding: 4, resource: { buffer: neighborGrid.cellFill.gpuBuffer } },
|
|
73
|
+
{ binding: 5, resource: { buffer: neighborGrid.order.gpuBuffer } },
|
|
107
74
|
],
|
|
108
75
|
});
|
|
109
76
|
const bgRender = (read) => device.createBindGroup({
|
|
@@ -137,15 +104,6 @@ export async function boids(config = {}) {
|
|
|
137
104
|
const write = useAB ? sideB : sideA;
|
|
138
105
|
const enc = device.createCommandEncoder();
|
|
139
106
|
const pass = enc.beginComputePass();
|
|
140
|
-
pass.setPipeline(pCounts);
|
|
141
|
-
pass.setBindGroup(0, bgCounts(read));
|
|
142
|
-
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
143
|
-
pass.setPipeline(pScan);
|
|
144
|
-
pass.setBindGroup(0, bgScan);
|
|
145
|
-
pass.dispatchWorkgroups(1);
|
|
146
|
-
pass.setPipeline(pScatter);
|
|
147
|
-
pass.setBindGroup(0, bgScatter(read));
|
|
148
|
-
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
149
107
|
pass.setPipeline(pForce);
|
|
150
108
|
pass.setBindGroup(0, bgForce(read, write));
|
|
151
109
|
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
@@ -172,10 +130,7 @@ export async function boids(config = {}) {
|
|
|
172
130
|
buffers() { return { pos: pp.current.pos, vel: pp.current.vel }; },
|
|
173
131
|
destroy() {
|
|
174
132
|
pp.destroy();
|
|
175
|
-
|
|
176
|
-
cellStart.destroy();
|
|
177
|
-
cellFill.destroy();
|
|
178
|
-
order.destroy();
|
|
133
|
+
neighborGrid.destroy();
|
|
179
134
|
uniform.destroy();
|
|
180
135
|
},
|
|
181
136
|
};
|
|
@@ -223,57 +178,6 @@ fn cellOf(p: vec2f) -> u32 {
|
|
|
223
178
|
return u32(cy) * u32(g) + u32(cx);
|
|
224
179
|
}
|
|
225
180
|
|
|
226
|
-
@compute @workgroup_size(${WG})
|
|
227
|
-
fn main_counts(@builtin(global_invocation_id) gid: vec3u) {
|
|
228
|
-
let i = gid.x;
|
|
229
|
-
if (i >= params.count) { return; }
|
|
230
|
-
atomicAdd(&cellCount[cellOf(posIn[i])], 1u);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
var<workgroup> partial: array<u32, 256>;
|
|
234
|
-
@compute @workgroup_size(256)
|
|
235
|
-
fn main_scan(@builtin(local_invocation_id) lid: vec3u) {
|
|
236
|
-
let tid = lid.x;
|
|
237
|
-
let cells = params.gridSize * params.gridSize;
|
|
238
|
-
let chunks = (cells + 255u) / 256u;
|
|
239
|
-
var local = 0u;
|
|
240
|
-
for (var c = 0u; c < chunks; c++) {
|
|
241
|
-
let idx = c * 256u + tid;
|
|
242
|
-
if (idx < cells) { local = local + atomicLoad(&cellCount[idx]); }
|
|
243
|
-
}
|
|
244
|
-
partial[tid] = local;
|
|
245
|
-
workgroupBarrier();
|
|
246
|
-
var offset = 1u;
|
|
247
|
-
loop {
|
|
248
|
-
if (offset >= 256u) { break; }
|
|
249
|
-
var v = 0u;
|
|
250
|
-
if (tid >= offset) { v = partial[tid - offset]; }
|
|
251
|
-
workgroupBarrier();
|
|
252
|
-
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
253
|
-
workgroupBarrier();
|
|
254
|
-
offset = offset << 1u;
|
|
255
|
-
}
|
|
256
|
-
var run = 0u;
|
|
257
|
-
if (tid > 0u) { run = partial[tid - 1u]; }
|
|
258
|
-
for (var c = 0u; c < chunks; c++) {
|
|
259
|
-
let idx = c * 256u + tid;
|
|
260
|
-
if (idx < cells) {
|
|
261
|
-
cellStart[idx] = run;
|
|
262
|
-
atomicStore(&cellFill[idx], run);
|
|
263
|
-
run = run + atomicLoad(&cellCount[idx]);
|
|
264
|
-
atomicStore(&cellCount[idx], 0u);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
@compute @workgroup_size(${WG})
|
|
270
|
-
fn main_scatter(@builtin(global_invocation_id) gid: vec3u) {
|
|
271
|
-
let i = gid.x;
|
|
272
|
-
if (i >= params.count) { return; }
|
|
273
|
-
let slot = atomicAdd(&cellFill[cellOf(posIn[i])], 1u);
|
|
274
|
-
order[slot] = i;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
181
|
@compute @workgroup_size(${WG})
|
|
278
182
|
fn main_force(@builtin(global_invocation_id) gid: vec3u) {
|
|
279
183
|
let i = gid.x;
|
|
@@ -22,7 +22,8 @@ export interface ParticlesConfig {
|
|
|
22
22
|
dt?: number;
|
|
23
23
|
/** 点大小(canvas 像素单位的比例,默认 0.004;大规模下自动缩小) */
|
|
24
24
|
pointSize?: number;
|
|
25
|
-
/**
|
|
25
|
+
/** 每粒子邻域候选总上限(grid 模式,均摊到 3×3=9 格)。默认 8100:正常密度永不触发,
|
|
26
|
+
* 极端抱团时以轻微方向偏差换取帧率稳定(实测 30ms → 7.5ms @ 200k 抱团态)。设 Infinity 可禁用。 */
|
|
26
27
|
maxNeighbors?: number;
|
|
27
28
|
}
|
|
28
29
|
export interface ResolvedConfig {
|
|
@@ -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
|
-
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 =
|
|
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,9 +40,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
42
42
|
export function gridScanWgsl() {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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
|
+
// 归零给下一帧。
|
|
46
50
|
return /* wgsl */ `
|
|
47
51
|
struct Params {
|
|
48
52
|
count: u32, _pad0: u32,
|
|
@@ -54,63 +58,48 @@ struct Params {
|
|
|
54
58
|
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
55
59
|
@group(0) @binding(2) var<storage, read_write> cellStart: array<u32>;
|
|
56
60
|
@group(0) @binding(3) var<storage, read_write> cellFill: array<u32>;
|
|
57
|
-
@group(0) @binding(4) var<storage, read_write> blockSums: array<u32>;
|
|
58
61
|
|
|
59
62
|
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
63
|
+
var<workgroup> carry: u32;
|
|
60
64
|
|
|
61
65
|
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
62
|
-
fn main(@builtin(local_invocation_id) lid: vec3u
|
|
66
|
+
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
|
63
67
|
let tid = lid.x;
|
|
64
68
|
let wg = ${SCAN_WORKGROUP}u;
|
|
65
|
-
let base = wid.x * wg;
|
|
66
69
|
let cells = params.cells;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let v0 = select(0u, atomicLoad(&cellCount[base + tid]), base + tid < cells);
|
|
70
|
-
partial[tid] = v0;
|
|
70
|
+
let numChunks = (cells + wg - 1u) / wg;
|
|
71
|
+
if (tid == 0u) { carry = 0u; }
|
|
71
72
|
workgroupBarrier();
|
|
72
|
-
var
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
workgroupBarrier();
|
|
78
|
-
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;
|
|
79
78
|
workgroupBarrier();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// 含前缀 → 排他:start = 块内前缀(不含自身),fill = start + count
|
|
83
|
-
let myCount = v0;
|
|
84
|
-
let myPrefix = select(partial[tid - 1u], 0u, tid == 0u);
|
|
85
|
-
if (base + tid < cells) {
|
|
86
|
-
cellStart[base + tid] = myPrefix;
|
|
87
|
-
cellFill[base + tid] = myPrefix + myCount;
|
|
88
|
-
}
|
|
89
|
-
// 块总和 → blockSums(含)
|
|
90
|
-
if (tid == 0u) { blockSums[wid.x] = partial[wg - 1u]; }
|
|
91
|
-
workgroupBarrier();
|
|
92
|
-
|
|
93
|
-
// ② 块间扫描(单 workgroup;块数 = ceil(cells/wg) ≤ SCAN_WORKGROUP)
|
|
94
|
-
if (wid.x == 0u) {
|
|
95
|
-
var off = 1u;
|
|
79
|
+
// 块内含前缀(Hillis-Steele)
|
|
80
|
+
var offset = 1u;
|
|
96
81
|
loop {
|
|
97
|
-
if (
|
|
82
|
+
if (offset >= wg) { break; }
|
|
98
83
|
var v = 0u;
|
|
99
|
-
if (tid >=
|
|
84
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
100
85
|
workgroupBarrier();
|
|
101
|
-
if (tid >=
|
|
86
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
102
87
|
workgroupBarrier();
|
|
103
|
-
|
|
88
|
+
offset = offset << 1u;
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 后才能进入下一块
|
|
100
|
+
workgroupBarrier();
|
|
101
|
+
if (tid == wg - 1u) { carry = carry + partial[wg - 1u]; }
|
|
102
|
+
workgroupBarrier();
|
|
114
103
|
}
|
|
115
104
|
}
|
|
116
105
|
`;
|
|
@@ -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;
|
|
@@ -102,7 +102,6 @@ export async function particles(config = {}) {
|
|
|
102
102
|
const pForceCell = await makePipeline(mForce, 'main_force_cell', 'grid-force-cell');
|
|
103
103
|
const pForceInt = await makePipeline(mForce, 'main_force_integrate', 'grid-force-integrate');
|
|
104
104
|
const partial = await Buffer.create('vec2f', cfg.count * 9); // (粒子 × 3×3 格) 部分力
|
|
105
|
-
const blockSums = await Buffer.create('u32', Math.ceil(cells / 256)); // 二级扫描块和
|
|
106
105
|
const sortedPos = await Buffer.create('vec2f', cfg.count); // 按格子序重排的副本(合并访问)
|
|
107
106
|
const sortedSp = await Buffer.create('u32', cfg.count);
|
|
108
107
|
const bgCounts = (readPos) => device.createBindGroup({
|
|
@@ -120,7 +119,6 @@ export async function particles(config = {}) {
|
|
|
120
119
|
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
121
120
|
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
122
121
|
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
123
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
124
122
|
],
|
|
125
123
|
});
|
|
126
124
|
const bgScatter = (readPos) => device.createBindGroup({
|
|
@@ -170,7 +168,7 @@ export async function particles(config = {}) {
|
|
|
170
168
|
};
|
|
171
169
|
const state = {
|
|
172
170
|
size,
|
|
173
|
-
count, start, fill, order, partial, sortedPos, sortedSp,
|
|
171
|
+
count, start, fill, order, partial, sortedPos, sortedSp,
|
|
174
172
|
pCounts, pScan, pScatter, pForceCell, pForceInt,
|
|
175
173
|
bgCountsA: bgCounts(sideA.pos), bgCountsB: bgCounts(sideB.pos),
|
|
176
174
|
bgScan,
|
|
@@ -237,23 +235,25 @@ export async function particles(config = {}) {
|
|
|
237
235
|
gridBindGroupsDirty = false;
|
|
238
236
|
}
|
|
239
237
|
const enc = device.createCommandEncoder();
|
|
240
|
-
const pass = enc.beginComputePass();
|
|
241
238
|
if (grid) {
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
257
|
const passB = enc.beginComputePass();
|
|
258
258
|
passB.setPipeline(grid.pForceCell);
|
|
259
259
|
passB.setBindGroup(0, useAB ? grid.bgForceCellAB : grid.bgForceCellBA);
|
|
@@ -267,6 +267,7 @@ export async function particles(config = {}) {
|
|
|
267
267
|
device.queue.submit([enc.finish()]);
|
|
268
268
|
}
|
|
269
269
|
else {
|
|
270
|
+
const pass = enc.beginComputePass();
|
|
270
271
|
pass.setPipeline(simPipeline);
|
|
271
272
|
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
272
273
|
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
@@ -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
|
}
|