wgpu-kit 1.1.0 → 1.1.2
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 +79 -7
- package/README.md +80 -9
- 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 +16 -4
- package/dist/core/errors.d.ts +18 -6
- package/dist/core/errors.js +28 -13
- package/dist/core/kernel.d.ts +2 -0
- package/dist/core/kernel.js +48 -17
- package/dist/core/layout.js +7 -6
- package/dist/core/pack.d.ts +37 -0
- package/dist/core/pack.js +35 -0
- package/dist/core/pingpong.js +1 -1
- package/dist/core/raw.js +6 -6
- package/dist/core/schema.d.ts +71 -0
- package/dist/core/schema.js +118 -0
- package/dist/core/shader.d.ts +4 -0
- package/dist/core/shader.js +38 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -0
- package/dist/media.js +3 -3
- package/dist/observe.js +2 -2
- package/dist/packs/fields/index.d.ts +11 -1
- package/dist/packs/fields/index.js +27 -3
- package/dist/packs/grid/index.js +5 -5
- package/dist/packs/image/index.js +4 -4
- package/dist/packs/life/boids.js +3 -3
- package/dist/packs/life/physarum.js +3 -3
- package/dist/packs/life/turing.js +3 -3
- package/dist/packs/particles/config.js +4 -4
- package/dist/packs/particles/grid.js +39 -58
- package/dist/packs/particles/index.js +42 -56
- package/dist/packs/particles/presets.js +1 -1
- package/package.json +7 -2
|
@@ -2,6 +2,8 @@ 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 { createShaderModuleChecked } from "../../core/shader.js";
|
|
6
|
+
import { definePack } from "../../core/pack.js";
|
|
5
7
|
import { MapRenderer } from "../life/map.js";
|
|
6
8
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
9
|
const FIELD_FNS = {
|
|
@@ -79,9 +81,8 @@ export async function flow(config = {}) {
|
|
|
79
81
|
device.queue.writeBuffer(diffuseUniform, 0, new Uint32Array([mapSize, mapSize]));
|
|
80
82
|
device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
|
|
81
83
|
const compile = async (code, label) => {
|
|
82
|
-
const m = device
|
|
83
|
-
const
|
|
84
|
-
const errors = info.messages.filter((x) => x.type === 'error');
|
|
84
|
+
const { module: m, messages } = await createShaderModuleChecked(device, code, label);
|
|
85
|
+
const errors = messages.filter((x) => x.type === 'error');
|
|
85
86
|
if (errors.length > 0)
|
|
86
87
|
throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
|
|
87
88
|
return m;
|
|
@@ -151,6 +152,23 @@ export async function flow(config = {}) {
|
|
|
151
152
|
},
|
|
152
153
|
stats() { return { fps: lastFps }; },
|
|
153
154
|
async sampleTrail() { return (await trail.current.t.read()); },
|
|
155
|
+
async probe() {
|
|
156
|
+
const t = (await trail.current.t.read());
|
|
157
|
+
let finite = true;
|
|
158
|
+
let maxv = 0;
|
|
159
|
+
let sum = 0;
|
|
160
|
+
for (let i = 0; i < t.length; i++) {
|
|
161
|
+
const v = t[i];
|
|
162
|
+
if (!Number.isFinite(v)) {
|
|
163
|
+
finite = false;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (v > maxv)
|
|
167
|
+
maxv = v;
|
|
168
|
+
sum += v;
|
|
169
|
+
}
|
|
170
|
+
return { finite, trailMax: maxv, trailMean: sum / Math.max(t.length, 1), frames: frame };
|
|
171
|
+
},
|
|
154
172
|
destroy() {
|
|
155
173
|
posBuf.destroy();
|
|
156
174
|
trail.destroy();
|
|
@@ -159,6 +177,12 @@ export async function flow(config = {}) {
|
|
|
159
177
|
},
|
|
160
178
|
};
|
|
161
179
|
}
|
|
180
|
+
/** fields 包的平台注册形态:第三方包与它长得一模一样(见 docs "Writing a pack") */
|
|
181
|
+
export const fieldsPack = definePack({
|
|
182
|
+
name: 'fields',
|
|
183
|
+
description: 'Vector-field advection trails (vortex / curl / twin)',
|
|
184
|
+
create: (config) => flow(config ?? {}),
|
|
185
|
+
});
|
|
162
186
|
function advectWgsl(fieldFn, mapSize) {
|
|
163
187
|
return /* wgsl */ `
|
|
164
188
|
struct Params {
|
package/dist/packs/grid/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { GpuContext } from "../../core/context.js";
|
|
2
2
|
import { Buffer } from "../../core/buffer.js";
|
|
3
3
|
import { CompileError } from "../../core/errors.js";
|
|
4
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
4
5
|
const WG = 64;
|
|
5
6
|
const SCAN = 256;
|
|
6
7
|
const USIZE = 32;
|
|
7
8
|
export async function createNeighborGrid(config) {
|
|
8
9
|
const { count, worldHalf, cellSize, workgroupSize = WG } = config;
|
|
9
10
|
if (!Number.isInteger(count) || count <= 0)
|
|
10
|
-
throw new Error(`count
|
|
11
|
+
throw new Error(`count must be a positive integer, got ${String(count)}`);
|
|
11
12
|
if (!(cellSize > 0))
|
|
12
|
-
throw new Error(`cellSize
|
|
13
|
+
throw new Error(`cellSize must be positive, got ${String(cellSize)}`);
|
|
13
14
|
const gridSize = Math.max(1, Math.ceil((2 * worldHalf) / cellSize));
|
|
14
15
|
const cells = gridSize * gridSize;
|
|
15
16
|
const ctx = await GpuContext.get();
|
|
@@ -34,9 +35,8 @@ export async function createNeighborGrid(config) {
|
|
|
34
35
|
const cellFill = await Buffer.create('u32', cells);
|
|
35
36
|
const order = await Buffer.create('u32', count);
|
|
36
37
|
cellCount.write(new Uint32Array(cells));
|
|
37
|
-
const module = device
|
|
38
|
-
const
|
|
39
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
38
|
+
const { module, messages } = await createShaderModuleChecked(device, gridWgsl(), 'ngrid');
|
|
39
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
40
40
|
if (errors.length > 0)
|
|
41
41
|
throw new CompileError('ngrid', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
42
42
|
const pCounts = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_counts' } });
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { GpuContext } from "../../core/context.js";
|
|
2
2
|
import { CompileError } from "../../core/errors.js";
|
|
3
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
3
4
|
const OPS = ['grayscale', 'invert', 'edge', 'blur', 'sharpen', 'brightness', 'contrast'];
|
|
4
5
|
const OP_IDS = { grayscale: 0, invert: 1, edge: 2, blur: 3, sharpen: 4, brightness: 5, contrast: 6 };
|
|
5
6
|
export async function applyImage(source, target, ops) {
|
|
6
7
|
if (ops.length === 0)
|
|
7
|
-
throw new Error('applyImage
|
|
8
|
+
throw new Error('applyImage requires at least one operator');
|
|
8
9
|
const width = 'naturalWidth' in source ? source.naturalWidth : source.width;
|
|
9
10
|
const height = 'naturalHeight' in source ? source.naturalHeight : source.height;
|
|
10
11
|
const ctx = await GpuContext.get();
|
|
@@ -34,9 +35,8 @@ export async function applyImage(source, target, ops) {
|
|
|
34
35
|
});
|
|
35
36
|
return t;
|
|
36
37
|
};
|
|
37
|
-
const module = device
|
|
38
|
-
const
|
|
39
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
38
|
+
const { module, messages } = await createShaderModuleChecked(device, shader(), 'image-filters');
|
|
39
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
40
40
|
if (errors.length > 0)
|
|
41
41
|
throw new CompileError('image-filters', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
42
42
|
const pipeline = device.createRenderPipeline({
|
package/dist/packs/life/boids.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { createNeighborGrid } from "../grid/index.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const WG = 64;
|
|
@@ -47,9 +48,8 @@ export async function boids(config = {}) {
|
|
|
47
48
|
device.queue.writeBuffer(uniform, 0, b);
|
|
48
49
|
};
|
|
49
50
|
writeUniform();
|
|
50
|
-
const module = device
|
|
51
|
-
const
|
|
52
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
51
|
+
const { module, messages } = await createShaderModuleChecked(device, boidsWgsl(size), 'boids');
|
|
52
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
53
53
|
if (errors.length > 0)
|
|
54
54
|
throw new CompileError('boids', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
55
55
|
const neighborGrid = await createNeighborGrid({ count: N, worldHalf: 1.0, cellSize: perception });
|
|
@@ -2,6 +2,7 @@ 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 { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { MapRenderer } from "./map.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const AWG = 32; // count(u32) pad(u32) + sensorAngle, sensorDist, turnAngle, step, deposit, worldHalf (f32×6) = 32
|
|
@@ -51,9 +52,8 @@ export async function physarum(config = {}) {
|
|
|
51
52
|
// decay 写进 diffuse kernel 的第二个 uniform?并成 16B:w,h,decayFrac,pad
|
|
52
53
|
device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
|
|
53
54
|
const compile = async (code, label) => {
|
|
54
|
-
const m = device
|
|
55
|
-
const
|
|
56
|
-
const errors = info.messages.filter((x) => x.type === 'error');
|
|
55
|
+
const { module: m, messages } = await createShaderModuleChecked(device, code, label);
|
|
56
|
+
const errors = messages.filter((x) => x.type === 'error');
|
|
57
57
|
if (errors.length > 0)
|
|
58
58
|
throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
|
|
59
59
|
return m;
|
|
@@ -2,6 +2,7 @@ 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 { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { MapRenderer } from "./map.js";
|
|
6
7
|
import { mulberry32 } from "../particles/presets.js";
|
|
7
8
|
const PRESETS = {
|
|
@@ -58,9 +59,8 @@ export async function turing(config = {}) {
|
|
|
58
59
|
device.queue.writeBuffer(uniform, 0, buf);
|
|
59
60
|
};
|
|
60
61
|
writeUniform();
|
|
61
|
-
const module = device
|
|
62
|
-
const
|
|
63
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
62
|
+
const { module, messages } = await createShaderModuleChecked(device, updateWgsl(), 'turing-update');
|
|
63
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
64
64
|
if (errors.length > 0)
|
|
65
65
|
throw new CompileError('turing-update', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
66
66
|
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { resolveMatrix, hashSeed } from "./presets.js";
|
|
2
|
-
import { UsageError } from "../../core/errors.js";
|
|
2
|
+
import { ERR, UsageError } from "../../core/errors.js";
|
|
3
3
|
const MODES = ['n2', 'tiled', 'grid'];
|
|
4
4
|
export function resolveConfig(config = {}) {
|
|
5
5
|
const { count = 8192, forces = 'cells', mode = 'grid', // 基准数据驱动:v0.4 起 grid 全面优于 tiled(0.54ms vs 3.62ms @16k),见 benchmarks.md
|
|
6
6
|
color = 'species', bounds = 'wrap', seed = 'wgpu-kit', rMax = 0.12, beta = 0.3, forceFactor = 10, frictionHalfLife = 0.04, dt = 0.02, pointSize = 0.004, maxNeighbors = 8100, } = config;
|
|
7
7
|
if (!Number.isInteger(count) || count <= 0 || count > 1_000_000) {
|
|
8
|
-
throw new UsageError(`count
|
|
8
|
+
throw new UsageError(ERR.USAGE, `count must be an integer in 1..1_000_000, got: ${String(count)}`);
|
|
9
9
|
}
|
|
10
10
|
if (!MODES.includes(mode)) {
|
|
11
|
-
throw new UsageError(`mode
|
|
11
|
+
throw new UsageError(ERR.USAGE, `mode must be one of ${MODES.join(" | ")}, got: "${String(mode)}"`);
|
|
12
12
|
}
|
|
13
13
|
if (mode === 'n2' && count > 32_000) {
|
|
14
|
-
throw new UsageError(`mode='n2'
|
|
14
|
+
throw new UsageError(ERR.USAGE, `mode='n2' is recommended for count <= 20000 (got ${count}); use 'tiled' or 'grid' for larger counts`);
|
|
15
15
|
}
|
|
16
16
|
const seedStr = String(seed);
|
|
17
17
|
return {
|
|
@@ -40,12 +40,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
42
42
|
export function gridScanWgsl() {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
43
|
+
// 单 workgroup 分块扫描:256-cell 块循环推进,块内 Hillis-Steele,块间用
|
|
44
|
+
// workgroup 级 carry 串接。此前版本按多 workgroup 分两级写,但 dispatch(1)
|
|
45
|
+
// 只有 block 0 在跑 —— 256 格之外 cellStart 永远是 0、cellCount 永不清零,
|
|
46
|
+
// 对应粒子受力恒零被摩擦冻住(屏幕上出现水平"冻结带")。改成单 workgroup
|
|
47
|
+
// 循环后 barrier 是合法同步,正确性不依赖任何跨 workgroup 时序;
|
|
48
|
+
// cell 上限 65536 = 256 块,每块一轮微秒级,性能无虞。顺带把 cellCount
|
|
49
|
+
// 归零给下一帧。
|
|
49
50
|
return /* wgsl */ `
|
|
50
51
|
struct Params {
|
|
51
52
|
count: u32, _pad0: u32,
|
|
@@ -56,70 +57,50 @@ struct Params {
|
|
|
56
57
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
57
58
|
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
58
59
|
@group(0) @binding(2) var<storage, read_write> cellStart: array<u32>;
|
|
59
|
-
@group(0) @binding(3) var<storage, read_write> cellFill: array<
|
|
60
|
-
@group(0) @binding(4) var<storage, read_write> blockSums: array<atomic<u32>>;
|
|
60
|
+
@group(0) @binding(3) var<storage, read_write> cellFill: array<u32>;
|
|
61
61
|
|
|
62
62
|
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
63
|
+
var<workgroup> carry: u32;
|
|
63
64
|
|
|
64
|
-
// Pass A:块内排他扫描。cellFill[c] = 块内排他前缀(临时);blockSums[wid] = 块总和
|
|
65
65
|
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
66
|
-
fn
|
|
66
|
+
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
|
67
67
|
let tid = lid.x;
|
|
68
68
|
let wg = ${SCAN_WORKGROUP}u;
|
|
69
|
-
let base = wid.x * wg;
|
|
70
69
|
let cells = params.cells;
|
|
71
|
-
let
|
|
72
|
-
|
|
70
|
+
let numChunks = (cells + wg - 1u) / wg;
|
|
71
|
+
if (tid == 0u) { carry = 0u; }
|
|
73
72
|
workgroupBarrier();
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
workgroupBarrier();
|
|
80
|
-
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
73
|
+
for (var ch = 0u; ch < numChunks; ch++) {
|
|
74
|
+
let idx = ch * wg + tid;
|
|
75
|
+
let inRange = idx < cells;
|
|
76
|
+
let v0 = select(0u, atomicLoad(&cellCount[idx]), inRange);
|
|
77
|
+
partial[tid] = v0;
|
|
81
78
|
workgroupBarrier();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (tid >= offset) { v = partial[tid - offset]; }
|
|
79
|
+
// 块内含前缀(Hillis-Steele)
|
|
80
|
+
var offset = 1u;
|
|
81
|
+
loop {
|
|
82
|
+
if (offset >= wg) { break; }
|
|
83
|
+
var v = 0u;
|
|
84
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
85
|
+
workgroupBarrier();
|
|
86
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
87
|
+
workgroupBarrier();
|
|
88
|
+
offset = offset << 1u;
|
|
89
|
+
}
|
|
90
|
+
// 排他:start = carry + 块内前缀(不含自身);fill 是 scatter 的原子填充
|
|
91
|
+
// 游标,初始化为段起点(与通用包 NeighborGrid 同语义)——scatter 填完一格
|
|
92
|
+
// 后 fill 恰好 = start + count,力核读 [start, fill) 才不会多扫下一格的粒子。
|
|
93
|
+
if (inRange) {
|
|
94
|
+
let excl = partial[tid] - v0;
|
|
95
|
+
cellStart[idx] = carry + excl;
|
|
96
|
+
cellFill[idx] = carry + excl;
|
|
97
|
+
atomicStore(&cellCount[idx], 0u);
|
|
98
|
+
}
|
|
99
|
+
// 所有线程读完 partial/写完 carry 后才能进入下一块
|
|
104
100
|
workgroupBarrier();
|
|
105
|
-
if (tid
|
|
101
|
+
if (tid == wg - 1u) { carry = carry + partial[wg - 1u]; }
|
|
106
102
|
workgroupBarrier();
|
|
107
|
-
offset = offset << 1u;
|
|
108
103
|
}
|
|
109
|
-
atomicStore(&blockSums[tid], partial[tid] - v0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Pass C:加块基址 → 最终 start/fill;counts 归零供下一帧
|
|
113
|
-
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
114
|
-
fn main_scan_apply(@builtin(global_invocation_id) gid: vec3u) {
|
|
115
|
-
let i = gid.x;
|
|
116
|
-
if (i >= params.cells) { return; }
|
|
117
|
-
let block = i / ${SCAN_WORKGROUP}u;
|
|
118
|
-
let base = atomicLoad(&blockSums[block]);
|
|
119
|
-
let excl = atomicLoad(&cellFill[i]);
|
|
120
|
-
cellStart[i] = excl + base;
|
|
121
|
-
atomicExchange(&cellFill[i], excl + base);
|
|
122
|
-
atomicStore(&cellCount[i], 0u);
|
|
123
104
|
}
|
|
124
105
|
`;
|
|
125
106
|
}
|
|
@@ -2,6 +2,7 @@ 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, createComputePipelineChecked } from "../../core/errors.js";
|
|
5
|
+
import { createShaderModuleChecked } from "../../core/shader.js";
|
|
5
6
|
import { resolveConfig } from "./config.js";
|
|
6
7
|
import { mulberry32, resolveMatrix, hashSeed } from "./presets.js";
|
|
7
8
|
import { simWgsl, WORKGROUP } from "./wgsl.js";
|
|
@@ -37,8 +38,9 @@ export async function particles(config = {}) {
|
|
|
37
38
|
}
|
|
38
39
|
const phys = { rMax: cfg.rMax, beta: cfg.beta, forceFactor: cfg.forceFactor, frictionHalfLife: cfg.frictionHalfLife, dt: cfg.dt };
|
|
39
40
|
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: 'particles-params' });
|
|
40
|
-
// grid 尺寸由 rMax 决定(
|
|
41
|
-
|
|
41
|
+
// grid 尺寸由 rMax 决定(floor 确保格宽 ≥ rMax → 3×3 邻域完整覆盖交互半径;
|
|
42
|
+
// ceil 会导致格宽 < rMax,3×3 不够覆盖,边界粒子漏配邻居)
|
|
43
|
+
const gridSizeOf = (rMax, half = worldHalf) => Math.max(4, Math.floor((2 * half) / Math.max(rMax, 1e-3)));
|
|
42
44
|
let gridSize = gridSizeOf(phys.rMax, worldHalf);
|
|
43
45
|
const writeUniform = (dt) => {
|
|
44
46
|
const buf = new ArrayBuffer(USIZE);
|
|
@@ -60,9 +62,8 @@ export async function particles(config = {}) {
|
|
|
60
62
|
writeUniform(cfg.dt);
|
|
61
63
|
// —— 着色器模块(编译错误 → 行号映射) ——
|
|
62
64
|
const compile = async (code, label) => {
|
|
63
|
-
const module = device
|
|
64
|
-
const
|
|
65
|
-
const errors = info.messages.filter((m) => m.type === 'error');
|
|
65
|
+
const { module, messages } = await createShaderModuleChecked(device, code, label);
|
|
66
|
+
const errors = messages.filter((m) => m.type === 'error');
|
|
66
67
|
if (errors.length > 0)
|
|
67
68
|
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
68
69
|
return module;
|
|
@@ -83,7 +84,6 @@ export async function particles(config = {}) {
|
|
|
83
84
|
g.partial.destroy();
|
|
84
85
|
g.sortedPos.destroy();
|
|
85
86
|
g.sortedSp.destroy();
|
|
86
|
-
g.blockSums.destroy();
|
|
87
87
|
};
|
|
88
88
|
const buildGrid = async (size) => {
|
|
89
89
|
const cells = size * size;
|
|
@@ -97,14 +97,11 @@ export async function particles(config = {}) {
|
|
|
97
97
|
const mScatter = await compile(gridScatterWgsl(), 'grid-scatter');
|
|
98
98
|
const mForce = await compile(gridForceWgsl(4), 'grid-force');
|
|
99
99
|
const pCounts = await makePipeline(mCounts, 'main', 'grid-counts');
|
|
100
|
-
const
|
|
101
|
-
const pScanBases = await makePipeline(mScan, 'main_scan_bases', 'grid-scan-bases');
|
|
102
|
-
const pScanApply = await makePipeline(mScan, 'main_scan_apply', 'grid-scan-apply');
|
|
100
|
+
const pScan = await makePipeline(mScan, 'main', 'grid-scan');
|
|
103
101
|
const pScatter = await makePipeline(mScatter, 'main', 'grid-scatter');
|
|
104
102
|
const pForceCell = await makePipeline(mForce, 'main_force_cell', 'grid-force-cell');
|
|
105
103
|
const pForceInt = await makePipeline(mForce, 'main_force_integrate', 'grid-force-integrate');
|
|
106
104
|
const partial = await Buffer.create('vec2f', cfg.count * 9); // (粒子 × 3×3 格) 部分力
|
|
107
|
-
const blockSums = await Buffer.create('u32', Math.ceil(cells / 256)); // 二级扫描块和
|
|
108
105
|
const sortedPos = await Buffer.create('vec2f', cfg.count); // 按格子序重排的副本(合并访问)
|
|
109
106
|
const sortedSp = await Buffer.create('u32', cfg.count);
|
|
110
107
|
const bgCounts = (readPos) => device.createBindGroup({
|
|
@@ -115,34 +112,13 @@ export async function particles(config = {}) {
|
|
|
115
112
|
{ binding: 2, resource: { buffer: count.gpuBuffer } },
|
|
116
113
|
],
|
|
117
114
|
});
|
|
118
|
-
const
|
|
119
|
-
layout:
|
|
115
|
+
const bgScan = device.createBindGroup({
|
|
116
|
+
layout: pScan.getBindGroupLayout(0),
|
|
120
117
|
entries: [
|
|
121
118
|
{ binding: 0, resource: { buffer: uniform } },
|
|
122
119
|
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
123
120
|
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
124
121
|
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
125
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
const bgScanBases = device.createBindGroup({
|
|
129
|
-
layout: pScanBases.getBindGroupLayout(0),
|
|
130
|
-
entries: [
|
|
131
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
132
|
-
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
133
|
-
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
134
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
135
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
136
|
-
],
|
|
137
|
-
});
|
|
138
|
-
const bgScanApply = device.createBindGroup({
|
|
139
|
-
layout: pScanApply.getBindGroupLayout(0),
|
|
140
|
-
entries: [
|
|
141
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
142
|
-
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
143
|
-
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
144
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
145
|
-
{ binding: 4, resource: { buffer: blockSums.gpuBuffer } },
|
|
146
122
|
],
|
|
147
123
|
});
|
|
148
124
|
const bgScatter = (readPos) => device.createBindGroup({
|
|
@@ -192,11 +168,10 @@ export async function particles(config = {}) {
|
|
|
192
168
|
};
|
|
193
169
|
const state = {
|
|
194
170
|
size,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
pCounts, pScanBlocks, pScanBases, pScanApply, pScatter, pForceCell, pForceInt,
|
|
171
|
+
count, start, fill, order, partial, sortedPos, sortedSp,
|
|
172
|
+
pCounts, pScan, pScatter, pForceCell, pForceInt,
|
|
198
173
|
bgCountsA: bgCounts(sideA.pos), bgCountsB: bgCounts(sideB.pos),
|
|
199
|
-
|
|
174
|
+
bgScan,
|
|
200
175
|
bgScatterA: bgScatter(sideA.pos), bgScatterB: bgScatter(sideB.pos),
|
|
201
176
|
bgForceCellAB: bgForceCell(sideA.pos), bgForceCellBA: bgForceCell(sideB.pos),
|
|
202
177
|
bgIntegrateAB: bgIntegrate(sideA, sideB), bgIntegrateBA: bgIntegrate(sideB, sideA),
|
|
@@ -259,28 +234,39 @@ export async function particles(config = {}) {
|
|
|
259
234
|
grid.bgForceCellRebuild(sideA.pos);
|
|
260
235
|
gridBindGroupsDirty = false;
|
|
261
236
|
}
|
|
237
|
+
const enc = device.createCommandEncoder();
|
|
262
238
|
if (grid) {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
239
|
+
// 每个 build 阶段独立 pass:同 pass 内跨 dispatch 的存储可见性规范不保证
|
|
240
|
+
// (Dawn/Windows 实测出现过旧数据),counts→scan→scatter 是严格串行依赖。
|
|
241
|
+
// 单 encoder 内 pass 按序执行且天然可见,不增加 submit 次数。
|
|
242
|
+
const passCounts = enc.beginComputePass();
|
|
243
|
+
passCounts.setPipeline(grid.pCounts);
|
|
244
|
+
passCounts.setBindGroup(0, useAB ? grid.bgCountsA : grid.bgCountsB);
|
|
245
|
+
passCounts.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
246
|
+
passCounts.end();
|
|
247
|
+
const passScan = enc.beginComputePass();
|
|
248
|
+
passScan.setPipeline(grid.pScan);
|
|
249
|
+
passScan.setBindGroup(0, grid.bgScan);
|
|
250
|
+
passScan.dispatchWorkgroups(1);
|
|
251
|
+
passScan.end();
|
|
252
|
+
const passScatter = enc.beginComputePass();
|
|
253
|
+
passScatter.setPipeline(grid.pScatter);
|
|
254
|
+
passScatter.setBindGroup(0, useAB ? grid.bgScatterA : grid.bgScatterB);
|
|
255
|
+
passScatter.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
256
|
+
passScatter.end();
|
|
257
|
+
const passB = enc.beginComputePass();
|
|
258
|
+
passB.setPipeline(grid.pForceCell);
|
|
259
|
+
passB.setBindGroup(0, useAB ? grid.bgForceCellAB : grid.bgForceCellBA);
|
|
260
|
+
passB.dispatchWorkgroups(Math.ceil((cfg.count * 9) / WORKGROUP));
|
|
261
|
+
passB.end();
|
|
262
|
+
const passC = enc.beginComputePass();
|
|
263
|
+
passC.setPipeline(grid.pForceInt);
|
|
264
|
+
passC.setBindGroup(0, useAB ? grid.bgIntegrateAB : grid.bgIntegrateBA);
|
|
265
|
+
passC.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
266
|
+
passC.end();
|
|
267
|
+
device.queue.submit([enc.finish()]);
|
|
281
268
|
}
|
|
282
269
|
else {
|
|
283
|
-
const enc = device.createCommandEncoder();
|
|
284
270
|
const pass = enc.beginComputePass();
|
|
285
271
|
pass.setPipeline(simPipeline);
|
|
286
272
|
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
@@ -67,7 +67,7 @@ export function resolveMatrix(forces, seed) {
|
|
|
67
67
|
if (typeof forces === 'string') {
|
|
68
68
|
const preset = FORCE_PRESETS[forces];
|
|
69
69
|
if (!preset) {
|
|
70
|
-
throw new Error(
|
|
70
|
+
throw new Error(`Unknown force preset "${forces}". Available: ${Object.keys(FORCE_PRESETS).join(", ")}, random`);
|
|
71
71
|
}
|
|
72
72
|
return preset;
|
|
73
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wgpu-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Creative-coding GPU toolkit for the browser. 200k-particle physics at 120fps in 5 lines of code. WebGPU compute without the boilerplate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -103,5 +103,10 @@
|
|
|
103
103
|
"bugs": {
|
|
104
104
|
"url": "https://github.com/nanfengw0w/wgpu-kit/issues"
|
|
105
105
|
},
|
|
106
|
-
"homepage": "https://github.com/nanfengw0w/wgpu-kit#readme"
|
|
106
|
+
"homepage": "https://github.com/nanfengw0w/wgpu-kit#readme",
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"react": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
107
112
|
}
|