wgpu-kit 1.0.3 → 1.1.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.cn.md +3 -3
- package/README.md +56 -16
- package/dist/packs/grid/index.d.ts +39 -0
- package/dist/packs/grid/index.js +187 -0
- package/dist/packs/life/boids.js +7 -103
- package/dist/packs/particles/config.d.ts +2 -1
- package/dist/packs/particles/config.js +1 -1
- package/dist/packs/particles/grid.js +44 -36
- package/dist/packs/particles/index.js +48 -33
- package/package.json +24 -10
package/README.cn.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wgpu-kit
|
|
2
2
|
|
|
3
|
-
> 浏览器创意编程 GPU 工具包:20 万粒子物理 120fps,只需 5 行代码。
|
|
3
|
+
> 浏览器创意编程 GPU 工具包:20 万粒子物理 120fps,只需 5 行代码。(所有 fps 均为**可见帧**)
|
|
4
4
|
> WebGPU 计算的全套样板——设备、缓冲、管线、dispatch、双缓冲、读回、错误行号映射——打包成两层简单 API。
|
|
5
5
|
|
|
6
6
|
[](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [](https://www.npmjs.com/package/wgpu-kit) [](LICENSE)
|
|
@@ -70,8 +70,8 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
|
|
|
70
70
|
|
|
71
71
|
| 指标 | 数值 | 环境 |
|
|
72
72
|
| --- | --- | --- |
|
|
73
|
-
| 粒子端到端 | 200,000 @ 122fps
|
|
74
|
-
| 粒子计算(grid) | 16k→262k 平坦,3.0→
|
|
73
|
+
| 粒子端到端 | 200,000 @ 122fps · 66,000 @ 144fps(**可见帧**)| RTX 4060 Laptop,playground 实测 |
|
|
74
|
+
| 粒子计算(grid) | 16k→262k 平坦,3.0→4.4ms/帧 | headless 基准,GPU 42°C |
|
|
75
75
|
| 邻域算法 | grid 近似 O(N),66k 时比暴力快 8.5× | 同会话 A/B |
|
|
76
76
|
| 库体积 | core gzip ~10kB(共享上下文构建) | gzip |
|
|
77
77
|
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# wgpu-kit
|
|
2
2
|
|
|
3
|
-
**Browser GPGPU middle layer. 200,000 particles at 142fps — in 5 lines of code.**
|
|
4
|
-
All the WebGPU boilerplate — device, buffers, pipelines, dispatch, readbacks — wrapped into two simple API layers.
|
|
5
|
-
|
|
6
3
|
[](https://github.com/nanfengw0w/wgpu-kit/actions/workflows/ci.yml) [](https://www.npmjs.com/package/wgpu-kit) [](LICENSE)
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
**Creative-coding GPU toolkit for the browser. 200,000-particle physics at 120fps — in 5 lines of code.**
|
|
6
|
+
All the WebGPU boilerplate — device, buffers, pipelines, dispatch, double
|
|
7
|
+
buffering, readbacks, error line-mapping — wrapped into two simple API layers.
|
|
8
|
+
|
|
9
|
+
[**API Reference**](docs/API.md) · [中文文档](README.cn.md) · **[LIVE DEMO](https://nanfengw0w.github.io/wgpu-kit/)**
|
|
9
10
|
|
|
10
11
|

|
|
11
12
|
|
|
@@ -18,7 +19,7 @@ npm i wgpu-kit
|
|
|
18
19
|
**100,000 particles in 5 lines:**
|
|
19
20
|
|
|
20
21
|
```ts
|
|
21
|
-
import { particles } from 'wgpu-kit';
|
|
22
|
+
import { particles } from 'wgpu-kit';
|
|
22
23
|
|
|
23
24
|
const sim = await particles({ count: 100_000, forces: 'cells' });
|
|
24
25
|
await sim.attach(canvas);
|
|
@@ -50,6 +51,42 @@ await integrate.run({ pos, vel }, { dt: 0.02 });
|
|
|
50
51
|
Device management, buffer sizing, pipeline creation, double buffering,
|
|
51
52
|
dispatch, readbacks, error line-mapping — all handled by the library.
|
|
52
53
|
|
|
54
|
+
### The same thing, with raw WebGPU
|
|
55
|
+
|
|
56
|
+
For honesty: here is the **heavily condensed** native equivalent (full
|
|
57
|
+
version is ~150 lines; this excerpt omits error handling, resize, double
|
|
58
|
+
buffering, staging readbacks and the render pipeline):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
62
|
+
const device = await adapter.requestDevice();
|
|
63
|
+
|
|
64
|
+
// buffers — sizes and usages hand-computed
|
|
65
|
+
const pos = device.createBuffer({ size: 100_000 * 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
66
|
+
const vel = device.createBuffer({ size: 100_000 * 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
67
|
+
|
|
68
|
+
// hand-written WGSL, uniform struct aligned to 16 bytes by hand
|
|
69
|
+
const module = device.createShaderModule({ code: `...struct Params {...}...` });
|
|
70
|
+
|
|
71
|
+
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
72
|
+
const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [/* every binding, exact order */] });
|
|
73
|
+
|
|
74
|
+
function tick(dt) {
|
|
75
|
+
const enc = device.createCommandEncoder();
|
|
76
|
+
const pass = enc.beginComputePass();
|
|
77
|
+
pass.setPipeline(pipeline);
|
|
78
|
+
pass.setBindGroup(0, bindGroup);
|
|
79
|
+
pass.dispatchWorkgroups(Math.ceil(100_000 / 64));
|
|
80
|
+
pass.end();
|
|
81
|
+
device.queue.submit([enc.finish()]);
|
|
82
|
+
}
|
|
83
|
+
// …plus: staging readbacks, device-lost handling, WGSL compile diagnostics,
|
|
84
|
+
// canvas resize — and a render pipeline before anything is visible.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With `wgpu-kit`, the kernel is the only code you write — and when WGSL fails
|
|
88
|
+
to compile, the error points at **your line**.
|
|
89
|
+
|
|
53
90
|
## Entry points
|
|
54
91
|
|
|
55
92
|
| import | purpose |
|
|
@@ -62,6 +99,7 @@ dispatch, readbacks, error line-mapping — all handled by the library.
|
|
|
62
99
|
| `wgpu-kit/react` | `<ParticleCanvas />` |
|
|
63
100
|
| `wgpu-kit/three` | three.js snapshot interop |
|
|
64
101
|
| `wgpu-kit/media` | canvas recording (webm/mp4) |
|
|
102
|
+
| `wgpu-kit/observe` | GPU timing / device diagnostics / canvas helpers |
|
|
65
103
|
| `wgpu-kit/vite` | WGSL kernel hot reload |
|
|
66
104
|
|
|
67
105
|

|
|
@@ -70,19 +108,14 @@ dispatch, readbacks, error line-mapping — all handled by the library.
|
|
|
70
108
|
|
|
71
109
|
## Numbers (reproducible)
|
|
72
110
|
|
|
111
|
+
All fps numbers are **visible frames** — every `tick()` renders fresh state.
|
|
112
|
+
|
|
73
113
|
| metric | value | environment |
|
|
74
114
|
| --- | --- | --- |
|
|
75
|
-
| particles end-to-end | 200,000 @
|
|
76
|
-
| particle compute (grid) |
|
|
77
|
-
| neighborhood algorithms | grid ~O(N)
|
|
78
|
-
| bundle size | core gzip
|
|
79
|
-
|
|
80
|
-
## Verification
|
|
81
|
-
|
|
82
|
-
41+ automated probes run on a real GPU via a headless Chromium harness
|
|
83
|
-
(included under `tests/` + `scripts/verify.mjs`) — including a physics
|
|
84
|
-
equivalence regression that fails if the neighborhood algorithms ever
|
|
85
|
-
produce divergent structures.
|
|
115
|
+
| particles end-to-end | 200,000 @ 122fps · 66,000 @ 144fps | RTX 4060 Laptop, playground |
|
|
116
|
+
| particle compute (grid) | 16k→262k flat, 3.0→4.4ms/frame | reproducible via `npm run bench` → docs/BENCHMARK.md |
|
|
117
|
+
| neighborhood algorithms | grid ~O(N), 8.5× faster than brute force at 66k | same-session A/B |
|
|
118
|
+
| bundle size | core gzip ~10kB (all entries share one context) | measured by `npm run build` |
|
|
86
119
|
|
|
87
120
|
## Three design rules
|
|
88
121
|
|
|
@@ -92,6 +125,13 @@ produce divergent structures.
|
|
|
92
125
|
3. **Benchmarks are documentation** — every published number is reproducible;
|
|
93
126
|
gzip budgets are enforced by `npm run build`.
|
|
94
127
|
|
|
128
|
+
## Verification
|
|
129
|
+
|
|
130
|
+
41+ automated probes run on a real GPU via a headless Chromium harness
|
|
131
|
+
(included: `tests/` + `scripts/verify.mjs`) — including a **physics
|
|
132
|
+
equivalence regression** that fails the build if the neighborhood algorithms
|
|
133
|
+
(n2 / tiled / grid) ever produce divergent structures.
|
|
134
|
+
|
|
95
135
|
## Support matrix
|
|
96
136
|
|
|
97
137
|
| browser | status |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Buffer } from '../../core/buffer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* NeighborGrid —— 通用空间邻域加速(计数排序 spatial hash)。
|
|
4
|
+
*
|
|
5
|
+
* 从粒子包的 grid 实现中提取的通用能力:任意"每帧需要查邻居"的模拟
|
|
6
|
+
* (流体 SPH / boids / 碰撞 / 聚类)都能用,实测 8.5× 于暴力解、近似 O(N)。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* const grid = await NeighborGrid.create({ count, worldHalf, cellSize });
|
|
10
|
+
* // 每帧:先 update(按位置建格),再让你的力 kernel 读 cellStart/cellFill/order
|
|
11
|
+
* grid.update(posBuffer);
|
|
12
|
+
* // 你的 kernel 通过 order[k] 解引用邻居(或直接用 grid.sortedPos 若启用了 payload)
|
|
13
|
+
*
|
|
14
|
+
* 设计说明:三个 build pass 在同一个 encoder 内提交(实测 pass 边界保证可见性);
|
|
15
|
+
* 粒子包保留其含 payload 排序的专用高性能变体,本包是无 payload 的通用版。
|
|
16
|
+
*/
|
|
17
|
+
export interface NeighborGridConfig {
|
|
18
|
+
/** 粒子/实体数量 */
|
|
19
|
+
count: number;
|
|
20
|
+
/** 世界半宽(世界 = [-worldHalf, worldHalf]) */
|
|
21
|
+
worldHalf: number;
|
|
22
|
+
/** 格子边长(通常 = 交互半径,使邻域恰为 3×3 格) */
|
|
23
|
+
cellSize: number;
|
|
24
|
+
workgroupSize?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface NeighborGrid {
|
|
27
|
+
readonly gridSize: number;
|
|
28
|
+
readonly cells: number;
|
|
29
|
+
/** 格内首个有序槽位 */
|
|
30
|
+
cellStart: Buffer;
|
|
31
|
+
/** 格内结束槽位(原子填充) */
|
|
32
|
+
cellFill: Buffer;
|
|
33
|
+
/** 按格子序排列的实体下标(order[slot] = 实体 i) */
|
|
34
|
+
order: Buffer;
|
|
35
|
+
/** 建格:counts → scan → scatter(三 pass,一 encoder,内部提交) */
|
|
36
|
+
update(pos: Buffer): void;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createNeighborGrid(config: NeighborGridConfig): Promise<NeighborGrid>;
|
|
@@ -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 必须是正整数,收到 ${String(count)}`);
|
|
11
|
+
if (!(cellSize > 0))
|
|
12
|
+
throw new Error(`cellSize 必须为正,收到 ${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
|
+
}
|
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 {
|
|
@@ -3,7 +3,7 @@ import { 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
8
|
throw new UsageError(`count 必须是 1..1_000_000 的整数,收到: ${String(count)}`);
|
|
9
9
|
}
|
|
@@ -40,9 +40,12 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
42
42
|
export function gridScanWgsl() {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
43
|
+
// 两级扫描,三 pass 结构(pass 边界保证跨 workgroup 可见性):
|
|
44
|
+
// A main_scan_blocks : 每 workgroup 对自己的 256-cell 块做排他扫描 → cellFill(临时),
|
|
45
|
+
// 块总和写入 blockSums[wid]
|
|
46
|
+
// B main_scan_bases : 单 workgroup 对 blockSums 做排他扫描 → 各块基址
|
|
47
|
+
// C main_scan_apply : start = cellFill + base;fill = start + count;counts 归零
|
|
48
|
+
// 支持至 65536 cell(gridSize ≤ 256);更大的世界需要多 pass 分块升级(路线图)。
|
|
46
49
|
return /* wgsl */ `
|
|
47
50
|
struct Params {
|
|
48
51
|
count: u32, _pad0: u32,
|
|
@@ -53,19 +56,18 @@ struct Params {
|
|
|
53
56
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
54
57
|
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
55
58
|
@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
|
|
59
|
+
@group(0) @binding(3) var<storage, read_write> cellFill: array<atomic<u32>>;
|
|
60
|
+
@group(0) @binding(4) var<storage, read_write> blockSums: array<atomic<u32>>;
|
|
58
61
|
|
|
59
62
|
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
60
63
|
|
|
64
|
+
// Pass A:块内排他扫描。cellFill[c] = 块内排他前缀(临时);blockSums[wid] = 块总和
|
|
61
65
|
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
62
|
-
fn
|
|
66
|
+
fn main_scan_blocks(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
|
|
63
67
|
let tid = lid.x;
|
|
64
68
|
let wg = ${SCAN_WORKGROUP}u;
|
|
65
69
|
let base = wid.x * wg;
|
|
66
70
|
let cells = params.cells;
|
|
67
|
-
|
|
68
|
-
// ① 块内 Hillis-Steele(块不足时以 0 填充)
|
|
69
71
|
let v0 = select(0u, atomicLoad(&cellCount[base + tid]), base + tid < cells);
|
|
70
72
|
partial[tid] = v0;
|
|
71
73
|
workgroupBarrier();
|
|
@@ -79,39 +81,45 @@ fn main(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: ve
|
|
|
79
81
|
workgroupBarrier();
|
|
80
82
|
offset = offset << 1u;
|
|
81
83
|
}
|
|
82
|
-
// 含前缀 → 排他:
|
|
83
|
-
let myCount = v0;
|
|
84
|
-
let myPrefix = select(partial[tid - 1u], 0u, tid == 0u);
|
|
84
|
+
// 含前缀 → 排他:excl = incl - own
|
|
85
85
|
if (base + tid < cells) {
|
|
86
|
-
|
|
87
|
-
cellFill[base + tid] = myPrefix + myCount;
|
|
86
|
+
atomicStore(&cellFill[base + tid], partial[tid] - v0);
|
|
88
87
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
workgroupBarrier();
|
|
88
|
+
if (tid == 0u) { atomicStore(&blockSums[wid.x], partial[wg - 1u]); }
|
|
89
|
+
}
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
workgroupBarrier();
|
|
101
|
-
if (tid >= off) { blockSums[tid] = blockSums[tid] + v; }
|
|
102
|
-
workgroupBarrier();
|
|
103
|
-
off = off << 1u;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
91
|
+
// Pass B:单 workgroup 对 blockSums 做排他扫描 → 各块基址
|
|
92
|
+
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
93
|
+
fn main_scan_bases(@builtin(local_invocation_id) lid: vec3u) {
|
|
94
|
+
let tid = lid.x;
|
|
95
|
+
let nBlocks = ceil(f32(params.cells) / ${SCAN_WORKGROUP}.0);
|
|
96
|
+
let v0 = select(atomicLoad(&blockSums[tid]), 0u, f32(tid) >= nBlocks);
|
|
97
|
+
partial[tid] = v0;
|
|
106
98
|
workgroupBarrier();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
99
|
+
var offset = 1u;
|
|
100
|
+
loop {
|
|
101
|
+
if (offset >= ${SCAN_WORKGROUP}u) { break; }
|
|
102
|
+
var v = 0u;
|
|
103
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
104
|
+
workgroupBarrier();
|
|
105
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
106
|
+
workgroupBarrier();
|
|
107
|
+
offset = offset << 1u;
|
|
114
108
|
}
|
|
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);
|
|
115
123
|
}
|
|
116
124
|
`;
|
|
117
125
|
}
|
|
@@ -97,7 +97,9 @@ 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
|
|
100
|
+
const pScanBlocks = await makePipeline(mScan, 'main_scan_blocks', 'grid-scan-blocks');
|
|
101
|
+
const pScanBases = await makePipeline(mScan, 'main_scan_bases', 'grid-scan-bases');
|
|
102
|
+
const pScanApply = await makePipeline(mScan, 'main_scan_apply', 'grid-scan-apply');
|
|
101
103
|
const pScatter = await makePipeline(mScatter, 'main', 'grid-scatter');
|
|
102
104
|
const pForceCell = await makePipeline(mForce, 'main_force_cell', 'grid-force-cell');
|
|
103
105
|
const pForceInt = await makePipeline(mForce, 'main_force_integrate', 'grid-force-integrate');
|
|
@@ -113,8 +115,28 @@ export async function particles(config = {}) {
|
|
|
113
115
|
{ binding: 2, resource: { buffer: count.gpuBuffer } },
|
|
114
116
|
],
|
|
115
117
|
});
|
|
116
|
-
const
|
|
117
|
-
layout:
|
|
118
|
+
const bgScanBlocks = device.createBindGroup({
|
|
119
|
+
layout: pScanBlocks.getBindGroupLayout(0),
|
|
120
|
+
entries: [
|
|
121
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
122
|
+
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
123
|
+
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
124
|
+
{ 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),
|
|
118
140
|
entries: [
|
|
119
141
|
{ binding: 0, resource: { buffer: uniform } },
|
|
120
142
|
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
@@ -170,10 +192,11 @@ export async function particles(config = {}) {
|
|
|
170
192
|
};
|
|
171
193
|
const state = {
|
|
172
194
|
size,
|
|
195
|
+
cells,
|
|
173
196
|
count, start, fill, order, partial, sortedPos, sortedSp, blockSums,
|
|
174
|
-
pCounts,
|
|
197
|
+
pCounts, pScanBlocks, pScanBases, pScanApply, pScatter, pForceCell, pForceInt,
|
|
175
198
|
bgCountsA: bgCounts(sideA.pos), bgCountsB: bgCounts(sideB.pos),
|
|
176
|
-
|
|
199
|
+
bgScanBlocks, bgScanBases, bgScanApply,
|
|
177
200
|
bgScatterA: bgScatter(sideA.pos), bgScatterB: bgScatter(sideB.pos),
|
|
178
201
|
bgForceCellAB: bgForceCell(sideA.pos), bgForceCellBA: bgForceCell(sideB.pos),
|
|
179
202
|
bgIntegrateAB: bgIntegrate(sideA, sideB), bgIntegrateBA: bgIntegrate(sideB, sideA),
|
|
@@ -236,37 +259,29 @@ export async function particles(config = {}) {
|
|
|
236
259
|
grid.bgForceCellRebuild(sideA.pos);
|
|
237
260
|
gridBindGroupsDirty = false;
|
|
238
261
|
}
|
|
239
|
-
const enc = device.createCommandEncoder();
|
|
240
|
-
const pass = enc.beginComputePass();
|
|
241
262
|
if (grid) {
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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()]);
|
|
263
|
+
// 五段各自独立 encoder+submit:WebGPU 同队列按提交序执行,
|
|
264
|
+
// pass 边界保证跨 workgroup 可见性(单 pass 内多 dispatch 曾实测读到旧数据)
|
|
265
|
+
const runPass = (pipeline, bg, wgs) => {
|
|
266
|
+
const e = device.createCommandEncoder();
|
|
267
|
+
const p = e.beginComputePass();
|
|
268
|
+
p.setPipeline(pipeline);
|
|
269
|
+
p.setBindGroup(0, bg);
|
|
270
|
+
p.dispatchWorkgroups(wgs);
|
|
271
|
+
p.end();
|
|
272
|
+
device.queue.submit([e.finish()]);
|
|
273
|
+
};
|
|
274
|
+
const nCellWg = Math.ceil(grid.cells / 256);
|
|
275
|
+
runPass(grid.pCounts, useAB ? grid.bgCountsA : grid.bgCountsB, Math.ceil(cfg.count / WORKGROUP));
|
|
276
|
+
runPass(grid.pScanBlocks, grid.bgScanBlocks, nCellWg);
|
|
277
|
+
runPass(grid.pScanBases, grid.bgScanBases, 1);
|
|
278
|
+
runPass(grid.pScatter, useAB ? grid.bgScatterA : grid.bgScatterB, Math.ceil(cfg.count / WORKGROUP));
|
|
279
|
+
runPass(grid.pForceCell, useAB ? grid.bgForceCellAB : grid.bgForceCellBA, Math.ceil((cfg.count * 9) / WORKGROUP));
|
|
280
|
+
runPass(grid.pForceInt, useAB ? grid.bgIntegrateAB : grid.bgIntegrateBA, Math.ceil(cfg.count / WORKGROUP));
|
|
268
281
|
}
|
|
269
282
|
else {
|
|
283
|
+
const enc = device.createCommandEncoder();
|
|
284
|
+
const pass = enc.beginComputePass();
|
|
270
285
|
pass.setPipeline(simPipeline);
|
|
271
286
|
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
272
287
|
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wgpu-kit",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
7
7
|
"keywords": [
|
|
@@ -25,31 +25,43 @@
|
|
|
25
25
|
},
|
|
26
26
|
"./particles": {
|
|
27
27
|
"types": "./dist/packs/particles/index.d.ts",
|
|
28
|
-
"import": "./dist/particles.js"
|
|
28
|
+
"import": "./dist/packs/particles/index.js"
|
|
29
29
|
},
|
|
30
30
|
"./three": {
|
|
31
31
|
"types": "./dist/interop/three.d.ts",
|
|
32
|
-
"import": "./dist/three.js"
|
|
32
|
+
"import": "./dist/interop/three.js"
|
|
33
33
|
},
|
|
34
34
|
"./life": {
|
|
35
35
|
"types": "./dist/packs/life/index.d.ts",
|
|
36
|
-
"import": "./dist/life.js"
|
|
36
|
+
"import": "./dist/packs/life/index.js"
|
|
37
37
|
},
|
|
38
38
|
"./react": {
|
|
39
39
|
"types": "./dist/react/index.d.ts",
|
|
40
|
-
"import": "./dist/react.js"
|
|
40
|
+
"import": "./dist/react/index.js"
|
|
41
41
|
},
|
|
42
42
|
"./fields": {
|
|
43
43
|
"types": "./dist/packs/fields/index.d.ts",
|
|
44
|
-
"import": "./dist/fields.js"
|
|
44
|
+
"import": "./dist/packs/fields/index.js"
|
|
45
45
|
},
|
|
46
46
|
"./image": {
|
|
47
47
|
"types": "./dist/packs/image/index.d.ts",
|
|
48
|
-
"import": "./dist/image.js"
|
|
48
|
+
"import": "./dist/packs/image/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./media": {
|
|
51
|
+
"types": "./dist/media.d.ts",
|
|
52
|
+
"import": "./dist/media.js"
|
|
53
|
+
},
|
|
54
|
+
"./observe": {
|
|
55
|
+
"types": "./dist/observe.d.ts",
|
|
56
|
+
"import": "./dist/observe.js"
|
|
49
57
|
},
|
|
50
58
|
"./vite": {
|
|
51
59
|
"types": "./dist/vite.d.ts",
|
|
52
60
|
"import": "./dist/vite.js"
|
|
61
|
+
},
|
|
62
|
+
"./grid": {
|
|
63
|
+
"types": "./dist/packs/grid/index.d.ts",
|
|
64
|
+
"import": "./dist/packs/grid/index.js"
|
|
53
65
|
}
|
|
54
66
|
},
|
|
55
67
|
"peerDependencies": {
|
|
@@ -64,8 +76,10 @@
|
|
|
64
76
|
"build:playground": "vite build playground",
|
|
65
77
|
"typecheck": "tsc --noEmit",
|
|
66
78
|
"test": "vitest run",
|
|
67
|
-
"bundle:tests": "esbuild tests/gpu/smoke.ts --bundle --format=esm --outfile=tests/gpu/smoke.bundle.js --log-level=warning && esbuild tests/gpu/bench.ts --bundle --format=esm --outfile=tests/gpu/bench.bundle.js --log-level=warning && esbuild tests/gpu/packages.ts --bundle --format=esm --outfile=tests/gpu/packages.bundle.js --log-level=warning",
|
|
68
|
-
"verify": "node scripts/verify.mjs"
|
|
79
|
+
"bundle:tests": "esbuild tests/gpu/smoke.ts --bundle --format=esm --outfile=tests/gpu/smoke.bundle.js --log-level=warning && esbuild tests/gpu/bench.ts --bundle --format=esm --outfile=tests/gpu/bench.bundle.js --log-level=warning && esbuild tests/gpu/packages.ts --bundle --format=esm --outfile=tests/gpu/packages.bundle.js --log-level=warning && esbuild tests/gpu/grid-debug.ts --bundle --format=esm --outfile=tests/gpu/grid-debug.bundle.js --log-level=warning",
|
|
80
|
+
"verify": "node scripts/verify.mjs",
|
|
81
|
+
"prepublishOnly": "npm run build && node scripts/audit-exports.mjs",
|
|
82
|
+
"bench": "node scripts/bench.mjs"
|
|
69
83
|
},
|
|
70
84
|
"devDependencies": {
|
|
71
85
|
"@types/node": "^24.0.0",
|