@aethervtt/shard-particles 0.0.1 → 0.2.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/dist/plugin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Last } from "@aethervtt/shard-core";
2
- import { Graph, RenderDescribers, RenderSet } from "@aethervtt/shard-render";
2
+ import { addRenderFeatures, Graph, RenderDescribers, RenderSet } from "@aethervtt/shard-render";
3
3
  import { definePlugin } from "@aethervtt/shard-runtime";
4
4
  import { OriginShift } from "@aethervtt/shard-transform";
5
5
  import * as componentsModule from "./components.js";
@@ -29,7 +29,15 @@ const particlesPlugin = definePlugin({
29
29
  },
30
30
  ready(app) {
31
31
  const graph = app.world.resource(Graph);
32
- graph.addNode("particles/simulate", simulateNode());
32
+ addRenderFeatures(app.world, {
33
+ name: "particles",
34
+ description: "Particle simulation, depth sort and drawing.",
35
+ nodes: ["particles/simulate", "particles"],
36
+ baseline: {
37
+ strategy: "CPU simulation and depth sort for every system; particles in data textures"
38
+ }
39
+ });
40
+ graph.addNode("particles/simulate", simulateNode(app.world));
33
41
  graph.addNode("particles", drawNode());
34
42
  app.world.initResource(RenderDescribers).set("particles", (w) => describeParticles(w));
35
43
  }
@@ -60,6 +60,7 @@ async function renderEffectPreview(world, path, width, height) {
60
60
  const image = await shot;
61
61
  return { width: image.width, height: image.height, data: image.data };
62
62
  } finally {
63
+ await app.dispose();
63
64
  target.destroy();
64
65
  }
65
66
  }
package/dist/shaders.js CHANGED
@@ -208,9 +208,9 @@ struct Draw {
208
208
  mode: vec4u,
209
209
  }
210
210
 
211
- @group(1) @binding(0) var<storage, read> particles: array<Particle>;
211
+ @data @group(1) @binding(0) var<storage, read> particles: array<Particle>;
212
212
  @group(1) @binding(1) var<uniform> draw: Draw;
213
- @group(1) @binding(2) var<storage, read> order: array<u32>;
213
+ @data @group(1) @binding(2) var<storage, read> order: array<u32>;
214
214
  @group(2) @binding(0) var sprite: texture_2d<f32>;
215
215
  @group(2) @binding(1) var sprite_sampler: sampler;
216
216
  @group(2) @binding(2) var scene_depth: texture_depth_2d;
package/dist/sim.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Entity, type World } from '@aethervtt/shard-core';
2
2
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
- import { type CameraData, type NodeDescriptor } from '@aethervtt/shard-render';
3
+ import { type CameraData, DataStore, type NodeDescriptor } from '@aethervtt/shard-render';
4
4
  import { type EmitterDef, type ParticleEffect } from './effect.js';
5
5
  /** Alpha emitters up to this capacity sort by depth; larger ones draw unsorted. */
6
6
  export declare const MAX_SORTED = 65536;
@@ -9,9 +9,14 @@ export interface EmitterState {
9
9
  def: EmitterDef;
10
10
  key: string;
11
11
  capacity: number;
12
- particles: GpuBuffer;
12
+ /** Storage on the full tier, a data texture on baseline (0064). */
13
+ particles: DataStore;
13
14
  /** CPU backend: the particles, uploaded whole each frame. */
14
15
  cpu: Float32Array | undefined;
16
+ /** `cpu` as u32 (seeds). */
17
+ cpuBits: Uint32Array | undefined;
18
+ /** CPU backend, sorted emitters: order-preserving key bits and slot, packed per particle. */
19
+ cpuSort: Float64Array | undefined;
15
20
  sim: GpuBuffer;
16
21
  draw: GpuBuffer;
17
22
  counter: GPUBuffer;
@@ -29,10 +34,12 @@ export interface EmitterState {
29
34
  backlog: number;
30
35
  /** Alive particles from the last readback (a frame or two late). */
31
36
  alive: number;
37
+ /** System time of the last spawn: its particles live until lifetime max after it. */
38
+ lastSpawn: number;
32
39
  /** Sorting (alpha, small enough): keys, order, and per-step uniforms. */
33
40
  sort: {
34
41
  keys: GPUBuffer;
35
- order: GPUBuffer;
42
+ order: DataStore;
36
43
  steps: GPUBuffer;
37
44
  count: number;
38
45
  n: number;
@@ -56,6 +63,8 @@ export interface SystemState {
56
63
  seed: number;
57
64
  local: boolean;
58
65
  cpu: boolean;
66
+ /** The CPU backend because the device runs the baseline tier (0064), not by choice. */
67
+ forced: boolean;
59
68
  model: Float32Array;
60
69
  /** Bounding sphere (world) of the system, for culling. */
61
70
  center: Float32Array;
@@ -103,12 +112,12 @@ interface Caches {
103
112
  }>;
104
113
  white?: GPUTexture;
105
114
  noDepth?: GPUTexture;
106
- noOrder?: GPUBuffer;
115
+ noOrder?: DataStore;
107
116
  sampler?: GPUSampler;
108
117
  }
109
118
  export declare const ParticleCaches: import("@aethervtt/shard-core").ResourceDef<Caches>;
110
119
  /** Spawn, update, and (alpha emitters) depth sort, for the primary camera's view. */
111
- export declare function simulateNode(): NodeDescriptor;
120
+ export declare function simulateNode(world: World): NodeDescriptor;
112
121
  /** Particles over the resolved HDR image, depth-tested against the scene without writing depth. */
113
122
  export declare function drawNode(): NodeDescriptor;
114
123
  /** Reads an emitter's particle buffer (tests: determinism, CPU vs GPU). */
@@ -125,6 +134,7 @@ export declare function describeParticles(world: World): {
125
134
  systems: {
126
135
  entity: number;
127
136
  backend: string;
137
+ backendReason?: string | undefined;
128
138
  emitters: {
129
139
  name: string;
130
140
  alive: number;
package/dist/sim.js CHANGED
@@ -8,15 +8,20 @@ import { GpuBuffer } from "@aethervtt/shard-gpu";
8
8
  import {
9
9
  ComputedVisibility,
10
10
  cameraOf,
11
+ clearUnsupported,
12
+ DataStore,
13
+ dataEntry,
14
+ depthReadEntry,
11
15
  ForwardStateResource,
12
16
  Gpu,
13
17
  GpuAssetsResource,
14
18
  Meshes,
15
19
  RenderPhase,
20
+ reportUnsupported,
16
21
  Shaders,
17
22
  Views
18
23
  } from "@aethervtt/shard-render";
19
- import { Time } from "@aethervtt/shard-runtime";
24
+ import { FrameDemand, Time } from "@aethervtt/shard-runtime";
20
25
  import { Textures } from "@aethervtt/shard-texture";
21
26
  import { GlobalTransform } from "@aethervtt/shard-transform";
22
27
  import { ParticleEmitterOverrides, ParticleSystem } from "./components.js";
@@ -69,8 +74,24 @@ const cpuParticle = {
69
74
  rotSpeed: 0,
70
75
  seed: 0
71
76
  };
77
+ function randomDirection(seed, k0, out) {
78
+ const z = rand(seed, k0) * 2 - 1;
79
+ const phi = 6.2831853 * rand(seed, k0 + 1);
80
+ const r = Math.sqrt(Math.max(1 - z * z, 0));
81
+ out[0] = r * Math.cos(phi);
82
+ out[1] = z;
83
+ out[2] = r * Math.sin(phi);
84
+ }
85
+ function mixRange(r, t) {
86
+ return r[0] + (r[1] - r[0]) * t;
87
+ }
88
+ const spawnDirection = new Float32Array(3);
89
+ const cpuSteps = [];
90
+ const cpuStepParams = [];
72
91
  function simulateCpu(e, sys, dt) {
73
92
  const d = e.cpu;
93
+ if (!e.cpuBits || e.cpuBits.buffer !== d.buffer) e.cpuBits = new Uint32Array(d.buffer);
94
+ const u = e.cpuBits;
74
95
  const def = e.def;
75
96
  const s = def.shape;
76
97
  const init = def.init;
@@ -84,17 +105,14 @@ function simulateCpu(e, sys, dt) {
84
105
  let dx = 0;
85
106
  let dy = 1;
86
107
  let dz = 0;
87
- const dir = (k0) => {
88
- const z = rand(seed, k0) * 2 - 1;
89
- const phi = 6.2831853 * rand(seed, k0 + 1);
90
- const r = Math.sqrt(Math.max(1 - z * z, 0));
91
- dx = r * Math.cos(phi);
92
- dy = z;
93
- dz = r * Math.sin(phi);
94
- };
95
- if (s.type === "point") dir(6);
96
- else if (s.type === "sphere") {
97
- dir(6);
108
+ if (s.type === "point" || s.type === "sphere") {
109
+ randomDirection(seed, 6, spawnDirection);
110
+ dx = spawnDirection[0];
111
+ dy = spawnDirection[1];
112
+ dz = spawnDirection[2];
113
+ }
114
+ if (s.type === "point") {
115
+ } else if (s.type === "sphere") {
98
116
  const r = s.radius * rand(seed, 9) ** 0.3333333;
99
117
  px = dx * r;
100
118
  py = dy * r;
@@ -113,8 +131,7 @@ function simulateCpu(e, sys, dt) {
113
131
  py = (rand(seed, 7) - 0.5) * s.size[1];
114
132
  pz = (rand(seed, 8) - 0.5) * s.size[2];
115
133
  }
116
- const mix = (r, t) => r[0] + (r[1] - r[0]) * t;
117
- const speed = mix(init.speed, rand(seed, 1));
134
+ const speed = mixRange(init.speed, rand(seed, 1));
118
135
  const p = cpuParticle;
119
136
  p.pos[0] = px;
120
137
  p.pos[1] = py;
@@ -122,7 +139,7 @@ function simulateCpu(e, sys, dt) {
122
139
  p.vel[0] = dx * speed;
123
140
  p.vel[1] = dy * speed;
124
141
  p.vel[2] = dz * speed;
125
- p.life = Math.max(mix(init.lifetime, rand(seed, 2)), 1e-3);
142
+ p.life = Math.max(mixRange(init.lifetime, rand(seed, 2)), 1e-3);
126
143
  p.seed = seed;
127
144
  p.rotSpeed = 0;
128
145
  for (const m of def.update) MODULES[m.module].cpuInit?.(m, p);
@@ -144,15 +161,23 @@ function simulateCpu(e, sys, dt) {
144
161
  d[o + 3] = 0;
145
162
  d.set(p.vel, o + 4);
146
163
  d[o + 7] = p.life;
147
- d[o + 8] = mix(init.size, rand(seed, 3));
148
- d[o + 9] = mix(init.rotation, rand(seed, 4)) * Math.PI / 180;
164
+ d[o + 8] = mixRange(init.size, rand(seed, 3));
165
+ d[o + 9] = mixRange(init.rotation, rand(seed, 4)) * Math.PI / 180;
149
166
  d[o + 10] = p.rotSpeed;
150
- new Uint32Array(d.buffer, (o + 11) * 4, 1)[0] = seed;
167
+ u[o + 11] = seed;
151
168
  const c = rand(seed, 5);
152
169
  for (let a = 0; a < 4; a++)
153
170
  d[o + 12 + a] = init.color[0][a] + (init.color[1][a] - init.color[0][a]) * c;
154
171
  }
155
- const u = new Uint32Array(d.buffer);
172
+ let steps = 0;
173
+ for (const m of def.update) {
174
+ const step = MODULES[m.module].cpu;
175
+ if (!step) continue;
176
+ cpuSteps[steps] = step;
177
+ cpuStepParams[steps] = m;
178
+ steps++;
179
+ }
180
+ const time = sys.time;
156
181
  let alive = 0;
157
182
  for (let i = 0; i < e.capacity; i++) {
158
183
  const o = i * PARTICLE_FLOATS;
@@ -172,10 +197,16 @@ function simulateCpu(e, sys, dt) {
172
197
  p.rot = d[o + 9];
173
198
  p.rotSpeed = d[o + 10];
174
199
  p.seed = u[o + 11];
175
- for (const m of def.update) MODULES[m.module].cpu?.(m, p, dt, sys.time);
176
- for (let a = 0; a < 3; a++) d[o + a] = p.pos[a] + p.vel[a] * dt;
200
+ for (let k = 0; k < steps; k++) cpuSteps[k](cpuStepParams[k], p, dt, time);
201
+ const vel = p.vel;
202
+ const pos = p.pos;
203
+ d[o] = pos[0] + vel[0] * dt;
204
+ d[o + 1] = pos[1] + vel[1] * dt;
205
+ d[o + 2] = pos[2] + vel[2] * dt;
177
206
  d[o + 3] = p.age;
178
- d.set(p.vel, o + 4);
207
+ d[o + 4] = vel[0];
208
+ d[o + 5] = vel[1];
209
+ d[o + 6] = vel[2];
179
210
  d[o + 9] = p.rot;
180
211
  if (p.age < life) alive++;
181
212
  }
@@ -198,12 +229,14 @@ function createEmitter(gpu, entity, def, cpu, previous) {
198
229
  def,
199
230
  key: emitterKey(def),
200
231
  capacity: def.capacity,
201
- particles: keep ? previous.particles : new GpuBuffer(gpu, {
232
+ particles: keep ? previous.particles : new DataStore(gpu, {
202
233
  label,
203
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
234
+ usage: GPUBufferUsage.COPY_SRC,
204
235
  size: def.capacity * PARTICLE_BYTES
205
236
  }),
206
237
  cpu: cpu ? keep && previous.cpu ? previous.cpu : new Float32Array(def.capacity * PARTICLE_FLOATS) : void 0,
238
+ cpuBits: void 0,
239
+ cpuSort: cpu && sortable ? new Float64Array(def.capacity) : void 0,
207
240
  sim: previous?.sim ?? new GpuBuffer(gpu, { label: `${label}/sim`, usage: GPUBufferUsage.UNIFORM, size: SIM_BYTES }),
208
241
  draw: previous?.draw ?? new GpuBuffer(gpu, {
209
242
  label: `${label}/draw`,
@@ -229,17 +262,14 @@ function createEmitter(gpu, entity, def, cpu, previous) {
229
262
  spawnCount: 0,
230
263
  backlog: 0,
231
264
  alive: keep ? previous.alive : 0,
265
+ lastSpawn: keep ? previous.lastSpawn : Number.NEGATIVE_INFINITY,
232
266
  sort: sortable ? {
233
267
  keys: gpu.device.createBuffer({
234
268
  label: `${label}/keys`,
235
269
  size: n * 4,
236
270
  usage: GPUBufferUsage.STORAGE
237
271
  }),
238
- order: gpu.device.createBuffer({
239
- label: `${label}/order`,
240
- size: n * 4,
241
- usage: GPUBufferUsage.STORAGE
242
- }),
272
+ order: new DataStore(gpu, { label: `${label}/order`, size: n * 4 }),
243
273
  steps: gpu.device.createBuffer({
244
274
  label: `${label}/steps`,
245
275
  size: (steps + 1) * 256,
@@ -306,6 +336,7 @@ const prepareParticles = defineSystem({
306
336
  store.primary = primary?.name;
307
337
  store.primaryCamera = primary ? cameraOf(primary) : void 0;
308
338
  const cam = store.primaryCamera;
339
+ let live = false;
309
340
  for (const table of q.tables) {
310
341
  const effectRefs = table.column(ParticleSystem, "effect");
311
342
  const playing = table.column(ParticleSystem, "playing");
@@ -320,7 +351,8 @@ const prepareParticles = defineSystem({
320
351
  const entity = table.entities[i];
321
352
  const effect = effects.get(effectRefs[i]);
322
353
  if (!effect || !vis[i]) continue;
323
- const cpu = backends[i] === 1;
354
+ const forced = gpu.tier === "baseline" && backends[i] !== 1;
355
+ const cpu = backends[i] === 1 || forced;
324
356
  let sys = store.systems.get(entity);
325
357
  if (!sys || sys.effect !== effect || sys.version !== effect.version || sys.cpu !== cpu) {
326
358
  const old = sys;
@@ -342,6 +374,7 @@ const prepareParticles = defineSystem({
342
374
  seed: seeds[i],
343
375
  local: spaces[i] === 1,
344
376
  cpu,
377
+ forced,
345
378
  model: new Float32Array(16),
346
379
  center: new Float32Array(3),
347
380
  radius: reach(effect),
@@ -398,12 +431,16 @@ const prepareParticles = defineSystem({
398
431
  e.backlog + (e.dt > 0 ? spawnCount(e, sys.time, e.dt, rate) : 0)
399
432
  );
400
433
  e.backlog = e.spawnCount;
434
+ if (e.spawnCount > 0) e.lastSpawn = sys.time;
435
+ if (dt > 0 && (rate > 0 || burstPending(e) || sys.time - e.lastSpawn <= e.def.init.lifetime[1]))
436
+ live = true;
401
437
  writeUniforms(e, sys, cam);
402
438
  if (sys.cpu && e.cpu) {
403
439
  simulateCpu(e, sys, e.dt);
404
440
  e.particles.write(e.cpu);
405
441
  e.spawned += e.spawnCount;
406
442
  e.backlog = 0;
443
+ if (e.sort && e.cpuSort && cam) sortCpu(e, sys, cam.view);
407
444
  }
408
445
  }
409
446
  }
@@ -411,8 +448,90 @@ const prepareParticles = defineSystem({
411
448
  for (const [entity, sys] of store.systems) {
412
449
  if (sys.seen !== store.frame) store.systems.delete(entity);
413
450
  }
451
+ world.tryResource(FrameDemand)?.set("particles", live);
452
+ if (gpu.tier === "baseline") reportCpuOnly(world, store);
414
453
  }
415
454
  });
455
+ const keyFloat = new Float32Array(1);
456
+ const keyBits = new Uint32Array(keyFloat.buffer);
457
+ function sortCpu(e, sys, view) {
458
+ const d = e.cpu;
459
+ const packed = e.cpuSort;
460
+ const m = sys.model;
461
+ for (let i = 0; i < e.capacity; i++) {
462
+ const o = i * PARTICLE_FLOATS;
463
+ let key = 3e38;
464
+ if (d[o + 7] > 0 && d[o + 3] < d[o + 7]) {
465
+ let x = d[o];
466
+ let y = d[o + 1];
467
+ let z = d[o + 2];
468
+ if (sys.local) {
469
+ const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
470
+ const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
471
+ const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
472
+ x = wx;
473
+ y = wy;
474
+ z = wz;
475
+ }
476
+ key = view[2] * x + view[6] * y + view[10] * z + view[14];
477
+ }
478
+ keyFloat[0] = key;
479
+ const bits = keyBits[0];
480
+ const ordered = bits & 2147483648 ? ~bits >>> 0 : (bits | 2147483648) >>> 0;
481
+ packed[i] = ordered * 65536 + i;
482
+ }
483
+ packed.sort();
484
+ const order = sortScratch(e.capacity);
485
+ for (let i = 0; i < e.capacity; i++) order[i] = packed[i] % 65536;
486
+ e.sort.order.write(order, 0, 0, e.capacity);
487
+ }
488
+ let orderScratch = new Uint32Array(1024);
489
+ function sortScratch(n) {
490
+ if (orderScratch.length < n) orderScratch = new Uint32Array(n);
491
+ return orderScratch;
492
+ }
493
+ const reportedModules = /* @__PURE__ */ new WeakMap();
494
+ const usedModules = /* @__PURE__ */ new Set();
495
+ function reportCpuOnly(world, store) {
496
+ let reported = reportedModules.get(world);
497
+ if (!reported) {
498
+ reported = /* @__PURE__ */ new Set();
499
+ reportedModules.set(world, reported);
500
+ }
501
+ usedModules.clear();
502
+ for (const sys of store.systems.values()) {
503
+ if (!sys.forced) continue;
504
+ for (const e of sys.emitters) {
505
+ for (const m of e.def.update) {
506
+ const def = MODULES[m.module];
507
+ if (def.update && !def.cpu) usedModules.add(m.module);
508
+ }
509
+ }
510
+ }
511
+ for (const name of usedModules) {
512
+ if (reported.has(name)) continue;
513
+ reported.add(name);
514
+ reportUnsupported(
515
+ world,
516
+ `particles/${name}`,
517
+ `The particle module "${name}" runs on the GPU backend only; the baseline tier runs particles on the CPU, without it`
518
+ );
519
+ }
520
+ for (const name of reported) {
521
+ if (usedModules.has(name)) continue;
522
+ reported.delete(name);
523
+ clearUnsupported(world, `particles/${name}`);
524
+ }
525
+ }
526
+ function burstPending(e) {
527
+ const bursts = e.def.spawn.bursts;
528
+ for (let i = 0; i < bursts.length; i++) {
529
+ const b = bursts[i];
530
+ const fired = e.fired[i] ?? 0;
531
+ if ((fired === 0 || b.interval > 0) && (b.cycles === 0 || fired < b.cycles)) return true;
532
+ }
533
+ return false;
534
+ }
416
535
  function sphereVisible(planes, c, r) {
417
536
  for (let p = 0; p < 6; p++) {
418
537
  const d = planes[p * 4] * c[0] + planes[p * 4 + 1] * c[1] + planes[p * 4 + 2] * c[2] + planes[p * 4 + 3];
@@ -511,9 +630,9 @@ function caches(world, gpu) {
511
630
  data: gpu.layouts.bindGroupLayout({
512
631
  label: "particles/data",
513
632
  entries: [
514
- { binding: 0, visibility: V, buffer: { type: "read-only-storage" } },
633
+ dataEntry(gpu, 0, V),
515
634
  { binding: 1, visibility: V | F, buffer: { type: "uniform" } },
516
- { binding: 2, visibility: V, buffer: { type: "read-only-storage" } }
635
+ dataEntry(gpu, 2, V)
517
636
  ]
518
637
  }),
519
638
  texture: gpu.layouts.bindGroupLayout({
@@ -521,7 +640,7 @@ function caches(world, gpu) {
521
640
  entries: [
522
641
  { binding: 0, visibility: F, texture: { sampleType: "float" } },
523
642
  { binding: 1, visibility: F, sampler: { type: "filtering" } },
524
- { binding: 2, visibility: F, texture: { sampleType: "depth" } }
643
+ depthReadEntry(gpu, 2, F)
525
644
  ]
526
645
  }),
527
646
  view: gpu.layouts.bindGroupLayout({
@@ -549,11 +668,7 @@ function caches(world, gpu) {
549
668
  format: "depth32float",
550
669
  usage: GPUTextureUsage.TEXTURE_BINDING
551
670
  });
552
- c.noOrder = gpu.device.createBuffer({
553
- label: "particles/no-order",
554
- size: 16,
555
- usage: GPUBufferUsage.STORAGE
556
- });
671
+ c.noOrder = new DataStore(gpu, { label: "particles/no-order", size: 16 });
557
672
  c.sampler = gpu.device.createSampler({
558
673
  label: "particles",
559
674
  magFilter: "linear",
@@ -628,11 +743,12 @@ function sortPipelines(ctx, c) {
628
743
  c.sort = { keys, sort };
629
744
  return c.sort;
630
745
  }
631
- function simulateNode() {
746
+ function simulateNode(world) {
632
747
  return {
633
748
  kind: "raw",
634
749
  phase: RenderPhase.Resolve + 10,
635
- enabled: (view) => cameraOf(view) !== void 0,
750
+ // Compute is the full tier's: on baseline every system runs the CPU backend in prepare.
751
+ enabled: (view) => cameraOf(view) !== void 0 && world.resource(Gpu).tier === "full",
636
752
  reads: ["depth"],
637
753
  sideEffects: true,
638
754
  run: (ctx) => {
@@ -656,10 +772,10 @@ function simulateNode() {
656
772
  gpu,
657
773
  c,
658
774
  `sim/${sys.entity}/${e.def.name}`,
659
- `${idOf(e.particles.buffer)}/${idOf(e.sim.buffer)}/${idOf(depth)}`,
775
+ `${idOf(e.particles.gpuBuffer)}/${idOf(e.sim.buffer)}/${idOf(depth)}`,
660
776
  c.layouts.sim,
661
777
  () => [
662
- { binding: 0, resource: { buffer: e.particles.buffer } },
778
+ { binding: 0, resource: { buffer: e.particles.gpuBuffer } },
663
779
  { binding: 1, resource: { buffer: e.sim.buffer } },
664
780
  { binding: 2, resource: depth.createView() },
665
781
  { binding: 3, resource: { buffer: e.counter } }
@@ -714,13 +830,13 @@ function sortEmitter(ctx, c, pass, sys, e, viewBuffer) {
714
830
  gpu,
715
831
  c,
716
832
  `sort/${sys.entity}/${e.def.name}/${i}`,
717
- `${idOf(e.particles.buffer)}/${idOf(viewBuffer)}/${idOf(s.keys)}`,
833
+ `${idOf(e.particles.gpuBuffer)}/${idOf(viewBuffer)}/${idOf(s.keys)}`,
718
834
  c.layouts.sort,
719
835
  () => [
720
836
  { binding: 0, resource: { buffer: viewBuffer } },
721
- { binding: 10, resource: { buffer: e.particles.buffer } },
837
+ { binding: 10, resource: { buffer: e.particles.gpuBuffer } },
722
838
  { binding: 11, resource: { buffer: s.keys } },
723
- { binding: 12, resource: { buffer: s.order } },
839
+ { binding: 12, resource: { buffer: s.order.gpuBuffer } },
724
840
  { binding: 13, resource: { buffer: s.steps, offset: i * 256, size: 16 } },
725
841
  // The draw uniform starts with the model matrix: particles in local space sort in world.
726
842
  {
@@ -738,18 +854,16 @@ function sortEmitter(ctx, c, pass, sys, e, viewBuffer) {
738
854
  pass.dispatchWorkgroups(Math.ceil(s.n / 64));
739
855
  }
740
856
  }
741
- let identityBuf;
742
857
  function identityBuffer(gpu, _c) {
743
- if (!identityBuf || identityBuf.generation !== gpu.generation) {
744
- const buffer = gpu.device.createBuffer({
858
+ return gpu.shared("particles/identity", (device) => {
859
+ const buffer = device.createBuffer({
745
860
  label: "particles/identity",
746
861
  size: 64,
747
862
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
748
863
  });
749
- gpu.device.queue.writeBuffer(buffer, 0, identity);
750
- identityBuf = { buffer, generation: gpu.generation };
751
- }
752
- return identityBuf.buffer;
864
+ device.queue.writeBuffer(buffer, 0, identity);
865
+ return buffer;
866
+ });
753
867
  }
754
868
  const ADDITIVE = {
755
869
  color: { srcFactor: "one", dstFactor: "one", operation: "add" },
@@ -852,12 +966,12 @@ function drawNode() {
852
966
  gpu,
853
967
  c,
854
968
  `data/${sys.entity}/${e.def.name}`,
855
- `${idOf(e.particles.buffer)}/${idOf(e.draw.buffer)}/${idOf(order)}`,
969
+ `${idOf(e.particles)}:${e.particles.version}/${idOf(e.draw.buffer)}/${idOf(order)}:${order.version}`,
856
970
  c.layouts.data,
857
971
  () => [
858
- { binding: 0, resource: { buffer: e.particles.buffer } },
972
+ { binding: 0, resource: e.particles.resource() },
859
973
  { binding: 1, resource: { buffer: e.draw.buffer } },
860
- { binding: 2, resource: { buffer: order } }
974
+ { binding: 2, resource: order.resource() }
861
975
  ]
862
976
  )
863
977
  );
@@ -894,13 +1008,14 @@ function drawNode() {
894
1008
  };
895
1009
  }
896
1010
  async function readParticles(gpu, e) {
1011
+ if (e.particles.textured) return e.cpu.slice(0, e.capacity * PARTICLE_FLOATS);
897
1012
  const size = e.capacity * PARTICLE_BYTES;
898
1013
  const staging = gpu.device.createBuffer({
899
1014
  size,
900
1015
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
901
1016
  });
902
1017
  const encoder = gpu.device.createCommandEncoder();
903
- encoder.copyBufferToBuffer(e.particles.buffer, 0, staging, 0, size);
1018
+ encoder.copyBufferToBuffer(e.particles.gpuBuffer, 0, staging, 0, size);
904
1019
  gpu.device.queue.submit([encoder.finish()]);
905
1020
  await staging.mapAsync(GPUMapMode.READ);
906
1021
  const out = new Float32Array(staging.getMappedRange().slice(0));
@@ -936,6 +1051,7 @@ function describeParticles(world) {
936
1051
  systems: [...store.systems.values()].map((sys) => ({
937
1052
  entity: sys.entity,
938
1053
  backend: sys.cpu ? "cpu" : "gpu",
1054
+ ...sys.forced ? { backendReason: "the baseline tier has no compute" } : {},
939
1055
  emitters: sys.emitters.map((e) => ({
940
1056
  name: e.def.name,
941
1057
  alive: e.alive,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-particles",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -16,14 +16,14 @@
16
16
  "README.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@aethervtt/shard-assets": "0.0.1",
20
- "@aethervtt/shard-core": "0.0.1",
21
- "@aethervtt/shard-gpu": "0.0.1",
22
- "@aethervtt/shard-mesh": "0.0.1",
23
- "@aethervtt/shard-render": "0.0.1",
24
- "@aethervtt/shard-runtime": "0.0.1",
25
- "@aethervtt/shard-texture": "0.0.1",
26
- "@aethervtt/shard-transform": "0.0.1"
19
+ "@aethervtt/shard-assets": "0.2.0",
20
+ "@aethervtt/shard-core": "0.2.0",
21
+ "@aethervtt/shard-gpu": "0.2.0",
22
+ "@aethervtt/shard-mesh": "0.2.0",
23
+ "@aethervtt/shard-render": "0.2.0",
24
+ "@aethervtt/shard-runtime": "0.2.0",
25
+ "@aethervtt/shard-texture": "0.2.0",
26
+ "@aethervtt/shard-transform": "0.2.0"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",
@@ -35,6 +35,6 @@
35
35
  "provenance": true
36
36
  },
37
37
  "shard": {
38
- "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
38
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
39
39
  }
40
40
  }