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.
@@ -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.createShaderModule({ code, label });
83
- const info = await m.getCompilationInfo();
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 {
@@ -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 必须是正整数,收到 ${String(count)}`);
11
+ throw new Error(`count must be a positive integer, got ${String(count)}`);
11
12
  if (!(cellSize > 0))
12
- throw new Error(`cellSize 必须为正,收到 ${String(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.createShaderModule({ code: gridWgsl(), label: 'ngrid' });
38
- const info = await module.getCompilationInfo();
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.createShaderModule({ code: shader(), label: 'image-filters' });
38
- const info = await module.getCompilationInfo();
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({
@@ -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.createShaderModule({ code: boidsWgsl(size), label: 'boids' });
51
- const info = await module.getCompilationInfo();
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.createShaderModule({ code, label });
55
- const info = await m.getCompilationInfo();
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.createShaderModule({ code: updateWgsl(), label: 'turing-update' });
62
- const info = await module.getCompilationInfo();
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 必须是 1..1_000_000 的整数,收到: ${String(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 必须是 ${MODES.join(' | ')},收到: "${String(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' 建议 count 20000(当前 ${count});大规模请用 mode='tiled' 'grid'`);
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
- // 两级扫描,三 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 分块升级(路线图)。
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<atomic<u32>>;
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 main_scan_blocks(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
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 v0 = select(0u, atomicLoad(&cellCount[base + tid]), base + tid < cells);
72
- partial[tid] = v0;
70
+ let numChunks = (cells + wg - 1u) / wg;
71
+ if (tid == 0u) { carry = 0u; }
73
72
  workgroupBarrier();
74
- var offset = 1u;
75
- loop {
76
- if (offset >= wg) { break; }
77
- var v = 0u;
78
- if (tid >= offset) { v = partial[tid - offset]; }
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
- offset = offset << 1u;
83
- }
84
- // 含前缀 → 排他:excl = incl - own
85
- if (base + tid < cells) {
86
- atomicStore(&cellFill[base + tid], partial[tid] - v0);
87
- }
88
- if (tid == 0u) { atomicStore(&blockSums[wid.x], partial[wg - 1u]); }
89
- }
90
-
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;
98
- workgroupBarrier();
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]; }
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 >= offset) { partial[tid] = partial[tid] + v; }
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 决定(格宽 rMax → 邻域恰好 3×3 格)
41
- const gridSizeOf = (rMax, half = worldHalf) => Math.max(4, Math.ceil((2 * half) / Math.max(rMax, 1e-3)));
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.createShaderModule({ code, label });
64
- const info = await module.getCompilationInfo();
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 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');
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 bgScanBlocks = device.createBindGroup({
119
- layout: pScanBlocks.getBindGroupLayout(0),
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
- cells,
196
- count, start, fill, order, partial, sortedPos, sortedSp, blockSums,
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
- bgScanBlocks, bgScanBases, bgScanApply,
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
- // 五段各自独立 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));
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(`未知力矩阵预设 "${forces}",可用: ${Object.keys(FORCE_PRESETS).join(', ')}, random`);
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.0",
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
  }