wgpu-kit 0.9.11 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/core/buffer.d.ts +1 -1
- package/dist/core/buffer.js +109 -0
- package/dist/core/context.d.ts +2 -0
- package/dist/core/context.js +62 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/kernel.js +197 -0
- package/dist/core/layout.d.ts +5 -1
- package/dist/core/layout.js +69 -0
- package/dist/core/pingpong.js +61 -0
- package/dist/core/raw.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -1510
- package/dist/interop/three.js +29 -0
- package/dist/media.js +70 -0
- package/dist/observe.d.ts +19 -0
- package/dist/observe.js +76 -0
- package/dist/packs/fields/index.js +230 -0
- package/dist/packs/image/index.js +250 -0
- package/dist/packs/life/boids.js +367 -0
- package/dist/packs/life/index.js +8 -0
- package/dist/packs/life/map.js +110 -0
- package/dist/packs/life/physarum.js +228 -0
- package/dist/packs/life/tentacles.js +238 -0
- package/dist/packs/life/turing.js +203 -0
- package/dist/packs/particles/config.d.ts +1 -0
- package/dist/packs/particles/config.js +36 -0
- package/dist/packs/particles/grid.js +248 -0
- package/dist/packs/particles/index.js +361 -0
- package/dist/packs/particles/presets.js +75 -0
- package/dist/packs/particles/render.js +116 -0
- package/dist/packs/particles/wgsl.js +175 -0
- package/dist/react/index.js +36 -0
- package/dist/vite.js +27 -28
- package/package.json +1 -1
- package/dist/fields.js +0 -585
- package/dist/image.js +0 -318
- package/dist/life.js +0 -1407
- package/dist/particles.js +0 -1245
- package/dist/react.js +0 -1289
- package/dist/three.js +0 -33
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveMatrix, hashSeed } from "./presets.js";
|
|
2
|
+
import { UsageError } from "../../core/errors.js";
|
|
3
|
+
const MODES = ['n2', 'tiled', 'grid'];
|
|
4
|
+
export function resolveConfig(config = {}) {
|
|
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 = 32768, } = config;
|
|
7
|
+
if (!Number.isInteger(count) || count <= 0 || count > 1_000_000) {
|
|
8
|
+
throw new UsageError(`count 必须是 1..1_000_000 的整数,收到: ${String(count)}`);
|
|
9
|
+
}
|
|
10
|
+
if (!MODES.includes(mode)) {
|
|
11
|
+
throw new UsageError(`mode 必须是 ${MODES.join(' | ')},收到: "${String(mode)}"`);
|
|
12
|
+
}
|
|
13
|
+
if (mode === 'n2' && count > 32_000) {
|
|
14
|
+
throw new UsageError(`mode='n2' 建议 count ≤ 20000(当前 ${count});大规模请用 mode='tiled' 或 'grid'`);
|
|
15
|
+
}
|
|
16
|
+
const seedStr = String(seed);
|
|
17
|
+
return {
|
|
18
|
+
count,
|
|
19
|
+
forces: resolveMatrix(forces, hashSeed(seedStr)),
|
|
20
|
+
forcesName: typeof forces === 'string' ? forces : 'custom',
|
|
21
|
+
mode,
|
|
22
|
+
color,
|
|
23
|
+
bounds,
|
|
24
|
+
seed: seedStr,
|
|
25
|
+
seedHash: hashSeed(seedStr),
|
|
26
|
+
rMax,
|
|
27
|
+
beta,
|
|
28
|
+
forceFactor,
|
|
29
|
+
friction: Math.exp(-dt / frictionHalfLife),
|
|
30
|
+
frictionHalfLife,
|
|
31
|
+
dt,
|
|
32
|
+
pointSize,
|
|
33
|
+
maxNeighbors,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export { FORCE_PRESETS, mulberry32, hashSeed, randomMatrix, resolveMatrix } from "./presets.js";
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grid 模式(spatial hash / counting sort)的四个 kernel。
|
|
3
|
+
* 每帧在同一个 command encoder 里跑四遍:
|
|
4
|
+
* ① counts 原子计数每格粒子数
|
|
5
|
+
* ② scan 单 workgroup 分块前缀和(cellStart/cellFill),顺带把 cellCount 归零给下一帧
|
|
6
|
+
* ③ scatter 按格散射出有序索引表 order
|
|
7
|
+
* ④ force 与 tiled 相同的力计算,但邻域遍历改为 3×3 个 cell 的有序区间
|
|
8
|
+
* 相比 O(N²),复杂度 ≈ O(N · 邻域密度)——10 万粒子的大门。
|
|
9
|
+
*/
|
|
10
|
+
export const GRID_WORKGROUP = 64;
|
|
11
|
+
export const SCAN_WORKGROUP = 256;
|
|
12
|
+
/** cellOf:世界坐标 → 格子索引(世界固定 [-worldHalf, worldHalf],格宽 = 2*worldHalf/gridSize) */
|
|
13
|
+
export const CELL_OF = /* wgsl */ `
|
|
14
|
+
fn cellOf(p: vec2f) -> u32 {
|
|
15
|
+
let g = i32(params.gridSize);
|
|
16
|
+
let span = params.worldHalf * 2.0;
|
|
17
|
+
let cx = clamp(i32(floor((p.x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
18
|
+
let cy = clamp(i32(floor((p.y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
19
|
+
return u32(cy) * params.gridSize + u32(cx);
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
export function gridCountsWgsl() {
|
|
23
|
+
return /* wgsl */ `
|
|
24
|
+
struct Params {
|
|
25
|
+
count: u32, _pad0: u32,
|
|
26
|
+
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
27
|
+
worldHalf: f32, wrapEdge: f32,
|
|
28
|
+
gridSize: u32, cells: u32, maxCand: u32,
|
|
29
|
+
};
|
|
30
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
31
|
+
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
32
|
+
@group(0) @binding(2) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
33
|
+
${CELL_OF}
|
|
34
|
+
@compute @workgroup_size(${GRID_WORKGROUP})
|
|
35
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
36
|
+
let i = gid.x;
|
|
37
|
+
if (i >= params.count) { return; }
|
|
38
|
+
atomicAdd(&cellCount[cellOf(posIn[i])], 1u);
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
export function gridScanWgsl() {
|
|
43
|
+
// 两级扫描:① 各 workgroup 扫自己的 256-cell 块,块总和写入 blockSums;
|
|
44
|
+
// ② 单 workgroup 扫 blockSums(最多 SCAN_WORKGROUP 个块 = 65536 cell);
|
|
45
|
+
// ③ 各 workgroup 加上本块基址。256×256=65536 cell 内 O(1) 轮次,不再随规模线性退化。
|
|
46
|
+
return /* wgsl */ `
|
|
47
|
+
struct Params {
|
|
48
|
+
count: u32, _pad0: u32,
|
|
49
|
+
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
50
|
+
worldHalf: f32, wrapEdge: f32,
|
|
51
|
+
gridSize: u32, cells: u32, maxCand: u32,
|
|
52
|
+
};
|
|
53
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
54
|
+
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
55
|
+
@group(0) @binding(2) var<storage, read_write> cellStart: array<u32>;
|
|
56
|
+
@group(0) @binding(3) var<storage, read_write> cellFill: array<u32>;
|
|
57
|
+
@group(0) @binding(4) var<storage, read_write> blockSums: array<u32>;
|
|
58
|
+
|
|
59
|
+
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
60
|
+
|
|
61
|
+
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
62
|
+
fn main(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
|
|
63
|
+
let tid = lid.x;
|
|
64
|
+
let wg = ${SCAN_WORKGROUP}u;
|
|
65
|
+
let base = wid.x * wg;
|
|
66
|
+
let cells = params.cells;
|
|
67
|
+
|
|
68
|
+
// ① 块内 Hillis-Steele(块不足时以 0 填充)
|
|
69
|
+
let v0 = select(0u, atomicLoad(&cellCount[base + tid]), base + tid < cells);
|
|
70
|
+
partial[tid] = v0;
|
|
71
|
+
workgroupBarrier();
|
|
72
|
+
var offset = 1u;
|
|
73
|
+
loop {
|
|
74
|
+
if (offset >= wg) { break; }
|
|
75
|
+
var v = 0u;
|
|
76
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
77
|
+
workgroupBarrier();
|
|
78
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
79
|
+
workgroupBarrier();
|
|
80
|
+
offset = offset << 1u;
|
|
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;
|
|
96
|
+
loop {
|
|
97
|
+
if (off >= wg) { break; }
|
|
98
|
+
var v = 0u;
|
|
99
|
+
if (tid >= off) { v = blockSums[tid - off]; }
|
|
100
|
+
workgroupBarrier();
|
|
101
|
+
if (tid >= off) { blockSums[tid] = blockSums[tid] + v; }
|
|
102
|
+
workgroupBarrier();
|
|
103
|
+
off = off << 1u;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
workgroupBarrier();
|
|
107
|
+
|
|
108
|
+
// ③ 加块基址;cellCount 归零供下一帧
|
|
109
|
+
let blockBase = select(0u, blockSums[wid.x - 1u], wid.x > 0u);
|
|
110
|
+
if (base + tid < cells) {
|
|
111
|
+
cellStart[base + tid] = cellStart[base + tid] + blockBase;
|
|
112
|
+
cellFill[base + tid] = cellFill[base + tid] + blockBase;
|
|
113
|
+
atomicStore(&cellCount[base + tid], 0u);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
export function gridScatterWgsl() {
|
|
119
|
+
return /* wgsl */ `
|
|
120
|
+
struct Params {
|
|
121
|
+
count: u32, _pad0: u32,
|
|
122
|
+
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
123
|
+
worldHalf: f32, wrapEdge: f32,
|
|
124
|
+
gridSize: u32, cells: u32, maxCand: u32,
|
|
125
|
+
};
|
|
126
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
127
|
+
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
128
|
+
@group(0) @binding(2) var<storage, read> species: array<u32>;
|
|
129
|
+
@group(0) @binding(3) var<storage, read_write> cellFill: array<atomic<u32>>;
|
|
130
|
+
@group(0) @binding(4) var<storage, read_write> order: array<u32>;
|
|
131
|
+
@group(0) @binding(5) var<storage, read_write> sortedPos: array<vec2f>;
|
|
132
|
+
@group(0) @binding(6) var<storage, read_write> sortedSp: array<u32>;
|
|
133
|
+
${CELL_OF}
|
|
134
|
+
@compute @workgroup_size(${GRID_WORKGROUP})
|
|
135
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
136
|
+
let i = gid.x;
|
|
137
|
+
if (i >= params.count) { return; }
|
|
138
|
+
let slot = atomicAdd(&cellFill[cellOf(posIn[i])], 1u);
|
|
139
|
+
order[slot] = i;
|
|
140
|
+
sortedPos[slot] = posIn[i];
|
|
141
|
+
sortedSp[slot] = species[i];
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* grid 力核:拆成两趟以消除负载不均(v0.9.7 黑屏级卡顿的根治)。
|
|
147
|
+
* 抱团时单格可能挤几千粒子,"一个线程扫 9 格"会让重格子线程拖死整批;
|
|
148
|
+
* 拆成 (粒子 × 格子) 一线程后工作量均分,物理结果与单趟完全一致(仅求和顺序不同)。
|
|
149
|
+
* ① main_force_cell:线程 = (粒子 i × 邻域格 c),把该格贡献写入 partial[i*9+c]
|
|
150
|
+
* ② main_force_integrate:线程 = 粒子,汇总 9 份贡献并积分
|
|
151
|
+
* 同格候选数上限 maxCand(由 JS 侧传 maxNeighbors/9)仍是极端抱团的保险丝。
|
|
152
|
+
*/
|
|
153
|
+
export function gridForceWgsl(speciesCount) {
|
|
154
|
+
return /* wgsl */ `
|
|
155
|
+
struct Params {
|
|
156
|
+
count: u32, _pad0: u32,
|
|
157
|
+
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
158
|
+
worldHalf: f32, wrapEdge: f32,
|
|
159
|
+
gridSize: u32, cells: u32, maxCand: u32,
|
|
160
|
+
};
|
|
161
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
162
|
+
@group(0) @binding(1) var<storage, read> matrix: array<f32>;
|
|
163
|
+
@group(0) @binding(2) var<storage, read> species: array<u32>;
|
|
164
|
+
@group(0) @binding(3) var<storage, read> posIn: array<vec2f>;
|
|
165
|
+
@group(0) @binding(4) var<storage, read> sortedPos: array<vec2f>;
|
|
166
|
+
@group(0) @binding(5) var<storage, read> sortedSp: array<u32>;
|
|
167
|
+
@group(0) @binding(6) var<storage, read> cellStart: array<u32>;
|
|
168
|
+
@group(0) @binding(7) var<storage, read> cellFill: array<u32>;
|
|
169
|
+
@group(0) @binding(8) var<storage, read> order: array<u32>;
|
|
170
|
+
@group(0) @binding(9) var<storage, read_write> partial: array<vec2f>;
|
|
171
|
+
|
|
172
|
+
fn force(r: f32, a: f32) -> f32 {
|
|
173
|
+
if (r < params.beta) { return a / params.beta - 1.0; }
|
|
174
|
+
if (r < 1.0) { return a * (1.0 - abs(2.0 * r - 1.0 - params.beta) / (1.0 - params.beta)); }
|
|
175
|
+
return 0.0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@compute @workgroup_size(64)
|
|
179
|
+
fn main_force_cell(@builtin(global_invocation_id) gid: vec3u) {
|
|
180
|
+
let tid = gid.x;
|
|
181
|
+
let i = tid / 9u;
|
|
182
|
+
if (i >= params.count) { return; }
|
|
183
|
+
let c = tid % 9u;
|
|
184
|
+
let g = i32(params.gridSize);
|
|
185
|
+
// 与 CELL_OF 完全相同的浮点序列(除以 span 再乘 g)——路径不一致会让贴格粒子查询错位一格
|
|
186
|
+
let span = params.worldHalf * 2.0;
|
|
187
|
+
var cx = clamp(i32(floor((posIn[i].x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
188
|
+
var cy = clamp(i32(floor((posIn[i].y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
189
|
+
let dx = i32(c % 3u) - 1;
|
|
190
|
+
let dy = i32(c / 3u) - 1;
|
|
191
|
+
let nx = cx + dx;
|
|
192
|
+
let ny = cy + dy;
|
|
193
|
+
let out = i * 9u + c;
|
|
194
|
+
if (nx < 0 || ny < 0 || nx >= g || ny >= g) { partial[out] = vec2f(0.0); return; }
|
|
195
|
+
let cc = u32(ny) * u32(g) + u32(nx);
|
|
196
|
+
let s = cellStart[cc];
|
|
197
|
+
let e = cellFill[cc];
|
|
198
|
+
let myPos = posIn[i];
|
|
199
|
+
let mySp = species[i];
|
|
200
|
+
let rMax2 = params.rMax * params.rMax;
|
|
201
|
+
var accel = vec2f(0.0, 0.0);
|
|
202
|
+
var checked = 0u;
|
|
203
|
+
for (var k = s; k < e; k++) {
|
|
204
|
+
checked = checked + 1u;
|
|
205
|
+
if (checked > params.maxCand) { break; }
|
|
206
|
+
if (order[k] == i) { continue; }
|
|
207
|
+
let rel = sortedPos[k] - myPos;
|
|
208
|
+
let d2 = dot(rel, rel);
|
|
209
|
+
if (d2 > rMax2) { continue; }
|
|
210
|
+
let d = sqrt(d2);
|
|
211
|
+
let r = d / params.rMax;
|
|
212
|
+
if (r > 0.0 && r < 1.0) {
|
|
213
|
+
let f = force(r, matrix[mySp * ${speciesCount}u + sortedSp[k]]);
|
|
214
|
+
accel = accel + rel / d * f;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
partial[out] = accel;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@group(0) @binding(0) var<uniform> params2: Params;
|
|
221
|
+
@group(0) @binding(1) var<storage, read> velIn: array<vec2f>;
|
|
222
|
+
@group(0) @binding(2) var<storage, read> posIn2: array<vec2f>;
|
|
223
|
+
@group(0) @binding(3) var<storage, read> partialR: array<vec2f>;
|
|
224
|
+
@group(0) @binding(4) var<storage, read_write> posOut: array<vec2f>;
|
|
225
|
+
@group(0) @binding(5) var<storage, read_write> velOut: array<vec2f>;
|
|
226
|
+
|
|
227
|
+
@compute @workgroup_size(64)
|
|
228
|
+
fn main_force_integrate(@builtin(global_invocation_id) gid: vec3u) {
|
|
229
|
+
let i = gid.x;
|
|
230
|
+
if (i >= params2.count) { return; }
|
|
231
|
+
var accel = vec2f(0.0, 0.0);
|
|
232
|
+
for (var c = 0u; c < 9u; c++) {
|
|
233
|
+
accel = accel + partialR[i * 9u + c];
|
|
234
|
+
}
|
|
235
|
+
accel = accel * params2.forceFactor * params2.rMax;
|
|
236
|
+
var vel = (velIn[i] + accel * params2.dt) * params2.friction;
|
|
237
|
+
var pos = posIn2[i] + vel * params2.dt;
|
|
238
|
+
let span = params2.worldHalf * 2.0;
|
|
239
|
+
if (params2.wrapEdge > 0.5) {
|
|
240
|
+
pos = ((pos + params2.worldHalf) % span + span) % span - params2.worldHalf;
|
|
241
|
+
} else {
|
|
242
|
+
pos = clamp(pos, vec2f(-params2.worldHalf), vec2f(params2.worldHalf));
|
|
243
|
+
}
|
|
244
|
+
posOut[i] = pos;
|
|
245
|
+
velOut[i] = vel;
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { GpuContext } from "../../core/context.js";
|
|
2
|
+
import { Buffer } from "../../core/buffer.js";
|
|
3
|
+
import { PingPong } from "../../core/pingpong.js";
|
|
4
|
+
import { CompileError, createComputePipelineChecked } from "../../core/errors.js";
|
|
5
|
+
import { resolveConfig } from "./config.js";
|
|
6
|
+
import { mulberry32, resolveMatrix, hashSeed } from "./presets.js";
|
|
7
|
+
import { simWgsl, WORKGROUP } from "./wgsl.js";
|
|
8
|
+
import { gridCountsWgsl, gridScanWgsl, gridScatterWgsl, gridForceWgsl } from "./grid.js";
|
|
9
|
+
import { ParticlesRenderer } from "./render.js";
|
|
10
|
+
const USIZE = 48;
|
|
11
|
+
export async function particles(config = {}) {
|
|
12
|
+
const cfg = resolveConfig(config);
|
|
13
|
+
const ctx = await GpuContext.get();
|
|
14
|
+
const device = ctx.device;
|
|
15
|
+
// 世界自适应:面积随粒子数等比扩大(默认 16k 密度),密度/邻居数/力与 16k 版逐位一致,
|
|
16
|
+
// 只是"宇宙更大、相机更远"——大规模下不改变任何物理行为(用户可关)
|
|
17
|
+
const worldHalf = 1 * Math.sqrt(cfg.count / 16_000);
|
|
18
|
+
// —— 数据 ——
|
|
19
|
+
const pp = await PingPong.create({ pos: 'vec2f', vel: 'vec2f' }, cfg.count);
|
|
20
|
+
const sideA = { pos: pp.current.pos, vel: pp.current.vel }; // 固定引用;pp.current 随 swap 翻转
|
|
21
|
+
const sideB = { pos: pp.other.pos, vel: pp.other.vel };
|
|
22
|
+
const species = await Buffer.create('u32', cfg.count);
|
|
23
|
+
const matrix = await Buffer.create('f32', 16);
|
|
24
|
+
{
|
|
25
|
+
const rand = mulberry32(cfg.seedHash);
|
|
26
|
+
const pos0 = new Float32Array(cfg.count * 2);
|
|
27
|
+
for (let i = 0; i < pos0.length; i++)
|
|
28
|
+
pos0[i] = (rand() * 1.6 - 0.8) * worldHalf;
|
|
29
|
+
const vel0 = new Float32Array(cfg.count * 2);
|
|
30
|
+
const sp0 = new Uint32Array(cfg.count);
|
|
31
|
+
for (let i = 0; i < cfg.count; i++)
|
|
32
|
+
sp0[i] = Math.floor(rand() * 4);
|
|
33
|
+
sideA.pos.write(pos0);
|
|
34
|
+
sideA.vel.write(vel0);
|
|
35
|
+
species.write(sp0);
|
|
36
|
+
matrix.write(new Float32Array(cfg.forces));
|
|
37
|
+
}
|
|
38
|
+
const phys = { rMax: cfg.rMax, beta: cfg.beta, forceFactor: cfg.forceFactor, frictionHalfLife: cfg.frictionHalfLife, dt: cfg.dt };
|
|
39
|
+
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: 'particles-params' });
|
|
40
|
+
// grid 尺寸由 rMax 决定(格宽 ≈ rMax → 邻域恰好 3×3 格)
|
|
41
|
+
const gridSizeOf = (rMax, half = worldHalf) => Math.max(4, Math.ceil((2 * half) / Math.max(rMax, 1e-3)));
|
|
42
|
+
let gridSize = gridSizeOf(phys.rMax, worldHalf);
|
|
43
|
+
const writeUniform = (dt) => {
|
|
44
|
+
const buf = new ArrayBuffer(USIZE);
|
|
45
|
+
const v = new DataView(buf);
|
|
46
|
+
v.setUint32(0, cfg.count, true);
|
|
47
|
+
v.setUint32(4, 0, true);
|
|
48
|
+
v.setFloat32(8, dt, true);
|
|
49
|
+
v.setFloat32(12, phys.rMax, true);
|
|
50
|
+
v.setFloat32(16, phys.beta, true);
|
|
51
|
+
v.setFloat32(20, phys.forceFactor, true);
|
|
52
|
+
v.setFloat32(24, Math.exp(-dt / phys.frictionHalfLife), true);
|
|
53
|
+
v.setFloat32(28, worldHalf, true);
|
|
54
|
+
v.setFloat32(32, cfg.bounds === 'wrap' ? 1 : 0, true);
|
|
55
|
+
v.setUint32(36, gridSize, true);
|
|
56
|
+
v.setUint32(40, gridSize * gridSize, true);
|
|
57
|
+
v.setUint32(44, Math.ceil(cfg.maxNeighbors / 9), true); // 每格候选上限
|
|
58
|
+
device.queue.writeBuffer(uniform, 0, buf);
|
|
59
|
+
};
|
|
60
|
+
writeUniform(cfg.dt);
|
|
61
|
+
// —— 着色器模块(编译错误 → 行号映射) ——
|
|
62
|
+
const compile = async (code, label) => {
|
|
63
|
+
const module = device.createShaderModule({ code, label });
|
|
64
|
+
const info = await module.getCompilationInfo();
|
|
65
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
66
|
+
if (errors.length > 0)
|
|
67
|
+
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
68
|
+
return module;
|
|
69
|
+
};
|
|
70
|
+
const makePipeline = async (module, entryPoint, label) => createComputePipelineChecked(device, module, `${label}(${entryPoint})`, entryPoint);
|
|
71
|
+
// —— n2 / tiled:单 kernel + 双缓冲 ——
|
|
72
|
+
let simPipeline = null;
|
|
73
|
+
let bgAB = null;
|
|
74
|
+
let bgBA = null;
|
|
75
|
+
let grid = null;
|
|
76
|
+
let gridBuildVersion = 0;
|
|
77
|
+
let gridBindGroupsDirty = false;
|
|
78
|
+
const gridDestroy = (g) => {
|
|
79
|
+
g.count.destroy();
|
|
80
|
+
g.start.destroy();
|
|
81
|
+
g.fill.destroy();
|
|
82
|
+
g.order.destroy();
|
|
83
|
+
g.partial.destroy();
|
|
84
|
+
g.sortedPos.destroy();
|
|
85
|
+
g.sortedSp.destroy();
|
|
86
|
+
g.blockSums.destroy();
|
|
87
|
+
};
|
|
88
|
+
const buildGrid = async (size) => {
|
|
89
|
+
const cells = size * size;
|
|
90
|
+
const count = await Buffer.create('u32', cells);
|
|
91
|
+
const start = await Buffer.create('u32', cells);
|
|
92
|
+
const fill = await Buffer.create('u32', cells);
|
|
93
|
+
const order = await Buffer.create('u32', cfg.count);
|
|
94
|
+
count.write(new Uint32Array(cells)); // 归零
|
|
95
|
+
const mCounts = await compile(gridCountsWgsl(), 'grid-counts');
|
|
96
|
+
const mScan = await compile(gridScanWgsl(), 'grid-scan');
|
|
97
|
+
const mScatter = await compile(gridScatterWgsl(), 'grid-scatter');
|
|
98
|
+
const mForce = await compile(gridForceWgsl(4), 'grid-force');
|
|
99
|
+
const pCounts = await makePipeline(mCounts, 'main', 'grid-counts');
|
|
100
|
+
const pScan = await makePipeline(mScan, 'main', 'grid-scan');
|
|
101
|
+
const pScatter = await makePipeline(mScatter, 'main', 'grid-scatter');
|
|
102
|
+
const pForceCell = await makePipeline(mForce, 'main_force_cell', 'grid-force-cell');
|
|
103
|
+
const pForceInt = await makePipeline(mForce, 'main_force_integrate', 'grid-force-integrate');
|
|
104
|
+
const partial = await Buffer.create('vec2f', cfg.count * 9); // (粒子 × 3×3 格) 部分力
|
|
105
|
+
const blockSums = await Buffer.create('u32', Math.ceil(cells / 256)); // 二级扫描块和
|
|
106
|
+
const sortedPos = await Buffer.create('vec2f', cfg.count); // 按格子序重排的副本(合并访问)
|
|
107
|
+
const sortedSp = await Buffer.create('u32', cfg.count);
|
|
108
|
+
const bgCounts = (readPos) => device.createBindGroup({
|
|
109
|
+
layout: pCounts.getBindGroupLayout(0),
|
|
110
|
+
entries: [
|
|
111
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
112
|
+
{ binding: 1, resource: { buffer: readPos.gpuBuffer } },
|
|
113
|
+
{ binding: 2, resource: { buffer: count.gpuBuffer } },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const bgScan = device.createBindGroup({
|
|
117
|
+
layout: pScan.getBindGroupLayout(0),
|
|
118
|
+
entries: [
|
|
119
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
120
|
+
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
121
|
+
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
122
|
+
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
123
|
+
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
const bgScatter = (readPos) => device.createBindGroup({
|
|
127
|
+
layout: pScatter.getBindGroupLayout(0),
|
|
128
|
+
entries: [
|
|
129
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
130
|
+
{ binding: 1, resource: { buffer: readPos.gpuBuffer } },
|
|
131
|
+
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
132
|
+
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
133
|
+
{ binding: 4, resource: { buffer: order.gpuBuffer } },
|
|
134
|
+
{ binding: 5, resource: { buffer: sortedPos.gpuBuffer } },
|
|
135
|
+
{ binding: 6, resource: { buffer: sortedSp.gpuBuffer } },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
// 力核第一趟:(粒子 × 格子) 部分力 —— 负载均衡的关键;读按格子序重排的副本(合并访问)
|
|
139
|
+
const bgForceCell = (readPos) => device.createBindGroup({
|
|
140
|
+
layout: pForceCell.getBindGroupLayout(0),
|
|
141
|
+
entries: [
|
|
142
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
143
|
+
{ binding: 1, resource: { buffer: matrix.gpuBuffer } },
|
|
144
|
+
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
145
|
+
{ binding: 3, resource: { buffer: readPos.gpuBuffer } },
|
|
146
|
+
{ binding: 4, resource: { buffer: sortedPos.gpuBuffer } },
|
|
147
|
+
{ binding: 5, resource: { buffer: sortedSp.gpuBuffer } },
|
|
148
|
+
{ binding: 6, resource: { buffer: start.gpuBuffer } },
|
|
149
|
+
{ binding: 7, resource: { buffer: fill.gpuBuffer } },
|
|
150
|
+
{ binding: 8, resource: { buffer: order.gpuBuffer } },
|
|
151
|
+
{ binding: 9, resource: { buffer: partial.gpuBuffer } },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
// 力核第二趟:汇总 9 份贡献并积分
|
|
155
|
+
const bgIntegrate = (read, write) => device.createBindGroup({
|
|
156
|
+
layout: pForceInt.getBindGroupLayout(0),
|
|
157
|
+
entries: [
|
|
158
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
159
|
+
{ binding: 1, resource: { buffer: read.vel.gpuBuffer } },
|
|
160
|
+
{ binding: 2, resource: { buffer: read.pos.gpuBuffer } },
|
|
161
|
+
{ binding: 3, resource: { buffer: partial.gpuBuffer } },
|
|
162
|
+
{ binding: 4, resource: { buffer: write.pos.gpuBuffer } },
|
|
163
|
+
{ binding: 5, resource: { buffer: write.vel.gpuBuffer } },
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
const rebuildForceCellBG = (readPos) => {
|
|
167
|
+
const otherPos = readPos === sideA.pos ? sideB.pos : sideA.pos;
|
|
168
|
+
state.bgForceCellAB = bgForceCell(readPos);
|
|
169
|
+
state.bgForceCellBA = bgForceCell(otherPos);
|
|
170
|
+
};
|
|
171
|
+
const state = {
|
|
172
|
+
size,
|
|
173
|
+
count, start, fill, order, partial, sortedPos, sortedSp, blockSums,
|
|
174
|
+
pCounts, pScan, pScatter, pForceCell, pForceInt,
|
|
175
|
+
bgCountsA: bgCounts(sideA.pos), bgCountsB: bgCounts(sideB.pos),
|
|
176
|
+
bgScan,
|
|
177
|
+
bgScatterA: bgScatter(sideA.pos), bgScatterB: bgScatter(sideB.pos),
|
|
178
|
+
bgForceCellAB: bgForceCell(sideA.pos), bgForceCellBA: bgForceCell(sideB.pos),
|
|
179
|
+
bgIntegrateAB: bgIntegrate(sideA, sideB), bgIntegrateBA: bgIntegrate(sideB, sideA),
|
|
180
|
+
bgForceCellRebuild: rebuildForceCellBG,
|
|
181
|
+
};
|
|
182
|
+
return state;
|
|
183
|
+
};
|
|
184
|
+
if (cfg.mode === 'grid')
|
|
185
|
+
grid = await buildGrid(gridSize);
|
|
186
|
+
else {
|
|
187
|
+
const module = await compile(simWgsl(cfg.mode, 4), `particles-sim(${cfg.mode})`);
|
|
188
|
+
simPipeline = await makePipeline(module, 'main', `particles-sim(${cfg.mode})`);
|
|
189
|
+
const bg = (read, write) => device.createBindGroup({
|
|
190
|
+
layout: simPipeline.getBindGroupLayout(0),
|
|
191
|
+
entries: [
|
|
192
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
193
|
+
{ binding: 1, resource: { buffer: matrix.gpuBuffer } },
|
|
194
|
+
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
195
|
+
{ binding: 3, resource: { buffer: read.pos.gpuBuffer } },
|
|
196
|
+
{ binding: 4, resource: { buffer: read.vel.gpuBuffer } },
|
|
197
|
+
{ binding: 5, resource: { buffer: write.pos.gpuBuffer } },
|
|
198
|
+
{ binding: 6, resource: { buffer: write.vel.gpuBuffer } },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
bgAB = bg(sideA, sideB);
|
|
202
|
+
bgBA = bg(sideB, sideA);
|
|
203
|
+
}
|
|
204
|
+
let renderer = null;
|
|
205
|
+
let frame = 0;
|
|
206
|
+
let lastFps = 0;
|
|
207
|
+
let fpsFrames = 0;
|
|
208
|
+
let fpsAcc = 0;
|
|
209
|
+
let fpsLast = performance.now();
|
|
210
|
+
let gpuErrorCount = 0;
|
|
211
|
+
device.addEventListener?.('uncapturederror', (e) => {
|
|
212
|
+
gpuErrorCount++;
|
|
213
|
+
const msg = e.error?.message ?? String(e);
|
|
214
|
+
const g = globalThis;
|
|
215
|
+
g.__firstGpuError ??= msg.slice(0, 400);
|
|
216
|
+
g.__lastGpuError = msg.slice(0, 300);
|
|
217
|
+
console.error('[wgpu-kit particles] GPU 错误:', msg);
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
config: cfg,
|
|
221
|
+
async attach(canvas) {
|
|
222
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
223
|
+
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
224
|
+
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
225
|
+
renderer = await ParticlesRenderer.create(canvas, {
|
|
226
|
+
count: cfg.count, species, vel: sideA.vel,
|
|
227
|
+
color: cfg.color, pointSize: cfg.pointSize * worldHalf, worldHalf,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
tick(dtMultiplier = 1) {
|
|
231
|
+
const dt = cfg.dt * dtMultiplier;
|
|
232
|
+
writeUniform(dt);
|
|
233
|
+
const useAB = frame % 2 === 0;
|
|
234
|
+
const read = useAB ? sideA : sideB;
|
|
235
|
+
if (gridBindGroupsDirty && grid) {
|
|
236
|
+
grid.bgForceCellRebuild(sideA.pos);
|
|
237
|
+
gridBindGroupsDirty = false;
|
|
238
|
+
}
|
|
239
|
+
const enc = device.createCommandEncoder();
|
|
240
|
+
const pass = enc.beginComputePass();
|
|
241
|
+
if (grid) {
|
|
242
|
+
// 实测:同一 compute pass 内连续 dispatch 之间,后续 kernel 读到的可能
|
|
243
|
+
// 是前一 kernel 的旧数据(Dawn/Windows,partial 竞态)——各段独立 pass
|
|
244
|
+
// 提交以保证可见性。
|
|
245
|
+
// 单 encoder、五个独立 pass:同一 encoder 内 pass 天然按序执行且可见,
|
|
246
|
+
// 免掉每帧 2 次额外 enc.finish()/submit(评审:提交间隔调度空隙)
|
|
247
|
+
pass.setPipeline(grid.pCounts);
|
|
248
|
+
pass.setBindGroup(0, useAB ? grid.bgCountsA : grid.bgCountsB);
|
|
249
|
+
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
250
|
+
pass.setPipeline(grid.pScan);
|
|
251
|
+
pass.setBindGroup(0, grid.bgScan);
|
|
252
|
+
pass.dispatchWorkgroups(1);
|
|
253
|
+
pass.setPipeline(grid.pScatter);
|
|
254
|
+
pass.setBindGroup(0, useAB ? grid.bgScatterA : grid.bgScatterB);
|
|
255
|
+
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
256
|
+
pass.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()]);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
pass.setPipeline(simPipeline);
|
|
271
|
+
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
272
|
+
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
273
|
+
pass.end();
|
|
274
|
+
device.queue.submit([enc.finish()]);
|
|
275
|
+
}
|
|
276
|
+
// 渲染刚写入的一侧(队列顺序保证 compute 先行),再翻转
|
|
277
|
+
// 渲染刚写入的一侧(useAB 时写入 other;!useAB 时写入 current)——
|
|
278
|
+
// 旧代码相位错位,隔帧才显示新状态(v0.9.7 引入的回归,评审 #4)
|
|
279
|
+
const writtenSide = useAB ? pp.other : pp.current;
|
|
280
|
+
renderer?.render(writtenSide.pos, writtenSide.vel);
|
|
281
|
+
pp.swap();
|
|
282
|
+
frame++;
|
|
283
|
+
// 内置轻量 fps 统计(playground/harness 都直接读)
|
|
284
|
+
fpsFrames++;
|
|
285
|
+
const now = performance.now();
|
|
286
|
+
fpsAcc += now - fpsLast;
|
|
287
|
+
fpsLast = now;
|
|
288
|
+
if (fpsAcc >= 500) {
|
|
289
|
+
lastFps = fpsFrames / (fpsAcc / 1000);
|
|
290
|
+
fpsFrames = 0;
|
|
291
|
+
fpsAcc = 0;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
setForces(forces) {
|
|
295
|
+
const m = resolveMatrix(forces, hashSeed(cfg.seed));
|
|
296
|
+
matrix.write(new Float32Array(m));
|
|
297
|
+
cfg.forces = m;
|
|
298
|
+
cfg.forcesName = typeof forces === 'string' ? forces : 'custom';
|
|
299
|
+
},
|
|
300
|
+
setParams(p) {
|
|
301
|
+
Object.assign(phys, p);
|
|
302
|
+
if (grid && p.rMax !== undefined) {
|
|
303
|
+
const g = gridSizeOf(phys.rMax);
|
|
304
|
+
if (g !== gridSize) {
|
|
305
|
+
gridSize = g;
|
|
306
|
+
// 版本号 + 原子切换:重建期间旧 grid 继续服务(不再有"新 gridSize × 旧缓冲"窗口);
|
|
307
|
+
// 连续调用只有最后一次生效,被超越/被替换的 grid 统一销毁,不泄漏
|
|
308
|
+
const version = ++gridBuildVersion;
|
|
309
|
+
void (async () => {
|
|
310
|
+
const fresh = await buildGrid(gridSize);
|
|
311
|
+
if (version !== gridBuildVersion) {
|
|
312
|
+
gridDestroy(fresh);
|
|
313
|
+
return; // 已有更新版本的重建完成,丢弃本份
|
|
314
|
+
}
|
|
315
|
+
const old = grid;
|
|
316
|
+
grid = fresh;
|
|
317
|
+
fresh.bgForceCellRebuild(sideA.pos); // 用当前侧重建 forceCell 绑定组(AB/BA 双向齐备)
|
|
318
|
+
if (old)
|
|
319
|
+
gridDestroy(old);
|
|
320
|
+
})();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
snapshot() {
|
|
325
|
+
return JSON.stringify({
|
|
326
|
+
count: cfg.count, forces: cfg.forcesName, mode: cfg.mode, color: cfg.color,
|
|
327
|
+
bounds: cfg.bounds, seed: cfg.seed, rMax: phys.rMax, beta: phys.beta,
|
|
328
|
+
forceFactor: phys.forceFactor, frictionHalfLife: phys.frictionHalfLife, dt: phys.dt,
|
|
329
|
+
pointSize: cfg.pointSize,
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
stats() {
|
|
333
|
+
return { fps: lastFps, gpuErrors: gpuErrorCount };
|
|
334
|
+
},
|
|
335
|
+
debugGrid: grid
|
|
336
|
+
? () => {
|
|
337
|
+
const g = grid;
|
|
338
|
+
return { partial: g.partial, start: g.start, fill: g.fill, sortedPos: g.sortedPos, sortedSp: g.sortedSp, order: g.order };
|
|
339
|
+
}
|
|
340
|
+
: undefined,
|
|
341
|
+
buffers() {
|
|
342
|
+
return { pos: pp.current.pos, vel: pp.current.vel, species };
|
|
343
|
+
},
|
|
344
|
+
destroy() {
|
|
345
|
+
renderer?.destroy();
|
|
346
|
+
pp.destroy();
|
|
347
|
+
species.destroy();
|
|
348
|
+
matrix.destroy();
|
|
349
|
+
uniform.destroy();
|
|
350
|
+
if (grid) {
|
|
351
|
+
grid.count.destroy();
|
|
352
|
+
grid.start.destroy();
|
|
353
|
+
grid.fill.destroy();
|
|
354
|
+
grid.order.destroy();
|
|
355
|
+
grid.partial.destroy();
|
|
356
|
+
grid.sortedPos.destroy();
|
|
357
|
+
grid.sortedSp.destroy();
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|