@woosh/meep-engine 3.60.9 → 3.61.1

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +1 -1
  3. package/src/engine/ecs/EntityManager.d.ts +3 -3
  4. package/src/engine/ecs/EntityManager.js +3 -3
  5. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  6. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  7. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts +46 -2
  8. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  9. package/src/engine/graphics/ecs/decal/v2/Decal.js +74 -5
  10. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.d.ts.map +1 -1
  11. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.js +13 -1
  12. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts +9 -0
  13. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts.map +1 -0
  14. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.js +37 -0
  15. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  16. package/src/engine/graphics3/DecalSystem.js +462 -447
  17. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts +14 -0
  18. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts.map +1 -1
  19. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.js +53 -13
  20. package/src/engine/graphics3/decal/chunk_decal_record.d.ts +6 -1
  21. package/src/engine/graphics3/decal/chunk_decal_record.d.ts.map +1 -1
  22. package/src/engine/graphics3/decal/chunk_decal_record.js +28 -9
  23. package/src/engine/graphics3/decal/decal_gpu_records.d.ts +2 -1
  24. package/src/engine/graphics3/decal/decal_gpu_records.d.ts.map +1 -1
  25. package/src/engine/graphics3/decal/decal_gpu_records.js +26 -5
  26. package/src/engine/graphics3/decal/decal_material_samplers.d.ts +6 -0
  27. package/src/engine/graphics3/decal/decal_material_samplers.d.ts.map +1 -1
  28. package/src/engine/graphics3/decal/decal_material_samplers.js +13 -0
  29. package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +1 -1
  30. package/src/engine/graphics3/decal/shader_cull_decals_frustum.js +1 -1
  31. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +5 -0
  32. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  33. package/src/engine/graphics3/decal/shader_decal_composite.js +69 -28
  34. package/src/engine/network/NetworkSession.d.ts +3 -0
  35. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  36. package/src/engine/network/NetworkSession.js +68 -10
  37. package/src/engine/network/README.md +6 -3
  38. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +51 -5
  39. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  40. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +308 -34
  41. package/src/engine/network/replication/Replicator.d.ts +8 -2
  42. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  43. package/src/engine/network/replication/Replicator.js +13 -4
  44. package/src/engine/network/sim/ActionLog.d.ts +16 -2
  45. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  46. package/src/engine/network/sim/ActionLog.js +33 -2
  47. package/src/engine/network/sim/RewindEngine.d.ts +9 -0
  48. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  49. package/src/engine/network/sim/RewindEngine.js +15 -1
  50. package/src/engine/network/state/FrameBufferRing.d.ts +12 -5
  51. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  52. package/src/engine/network/state/FrameBufferRing.js +32 -6
  53. package/src/engine/network/time/TimeDilation.d.ts +30 -14
  54. package/src/engine/network/time/TimeDilation.d.ts.map +1 -1
  55. package/src/engine/network/time/TimeDilation.js +47 -17
  56. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  57. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts +3 -2
  58. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts.map +1 -1
  59. package/src/generation/foliage/skeleton/ShootWoodHash.js +25 -2
  60. package/src/shade/renderer/buffer/graph_read_buffer.d.ts +3 -1
  61. package/src/shade/renderer/buffer/graph_read_buffer.d.ts.map +1 -1
  62. package/src/shade/renderer/buffer/graph_read_buffer.js +9 -6
  63. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts +5 -0
  64. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
  65. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +48 -43
  66. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  67. package/src/shade/renderer/material/GPUMaterialManager.js +117 -35
  68. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts +2 -2
  69. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  70. package/src/shade/renderer/material/standard/fragment_gbuffer.js +10 -3
  71. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts +15 -3
  72. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts.map +1 -1
  73. package/src/shade/renderer/material/standard/material_gbuffer_vertex.js +25 -4
  74. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +23 -5
  75. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  76. package/src/shade/renderer/particles/GPUParticleSystem.js +294 -20
  77. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts +21 -0
  78. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts.map +1 -0
  79. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.js +83 -0
  80. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts +21 -0
  81. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts.map +1 -0
  82. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.js +64 -0
  83. package/src/shade/renderer/restir/di/chunk_restir_resolve.d.ts.map +1 -1
  84. package/src/shade/renderer/restir/di/chunk_restir_resolve.js +91 -94
  85. package/src/shade/renderer/restir/reservoir/chunk_rtxdi_stream_sample.js +1 -1
  86. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts +14 -0
  87. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts.map +1 -1
  88. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.js +36 -21
  89. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts +22 -0
  90. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts.map +1 -0
  91. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.js +64 -0
  92. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.d.ts.map +1 -1
  93. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.js +13 -18
  94. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.d.ts.map +1 -1
  95. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.js +28 -30
  96. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.d.ts.map +1 -1
  97. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.js +15 -16
  98. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts +0 -1
  99. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts.map +1 -1
  100. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.js +15 -18
  101. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts +32 -0
  102. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts.map +1 -0
  103. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.js +73 -0
  104. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +8 -1
  105. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  106. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +7 -3
  107. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts +0 -2
  108. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts.map +0 -1
  109. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.js +0 -6
@@ -35,6 +35,17 @@ import { WARMUP_QUEUE } from "./warmup/shader_particle_warmup.js";
35
35
  import { warmup_schedule } from "./warmup/warmup_schedule.js";
36
36
  import { GPUTextureContext } from "../texture/GPUTextureContext.js";
37
37
  import { inspect_gpu_buffer } from "../buffer/inspect_gpu_buffer.js";
38
+ import { shader_buffer_copy_storage_to_storage } from "../buffer/util/shader_buffer_copy_storage_to_storage.js";
39
+ import { COMPUTE_SHADER_DEFAULT_GROUP_SIZE } from "../shader/COMPUTE_SHADER_DEFAULT_GROUP_SIZE.js";
40
+ import {
41
+ SLOT_STACK_PUSH_RANGE_WORKGROUP_SIZE,
42
+ shader_slot_stack_push_range,
43
+ } from "../gpu_primitive/slot_stack/shader_slot_stack_push_range.js";
44
+ import {
45
+ PARTICLE_SHRINK_WORKGROUP_SIZE,
46
+ shader_particle_shrink_free_list,
47
+ } from "./resize/shader_particle_shrink_free_list.js";
48
+ import { shader_particle_shrink_alive_list } from "./resize/shader_particle_shrink_alive_list.js";
38
49
 
39
50
  const WORD = Uint32Array.BYTES_PER_ELEMENT;
40
51
 
@@ -162,6 +173,17 @@ const DEFAULT_CAPACITY = 65536;
162
173
  * and a set of accumulators that are sums — so the particles need no depth ordering at all, and
163
174
  * none is ever built.
164
175
  *
176
+ * ## Capacity
177
+ *
178
+ * {@link capacity} may be set at any time and takes effect on the next {@link execute}, which is
179
+ * where every buffer it sizes is replaced — never while a recorded graph may still read the old
180
+ * ones. Growing keeps every live particle: the pool and the free list are copied into the larger
181
+ * buffers and the new slots pushed onto the free list. Shrinking keeps every live particle the
182
+ * smaller pool can hold: a particle in a slot past the new capacity moves into a free slot below
183
+ * it, and when there are more particles than slots, the excess is dropped. A particle that moves
184
+ * reads a new `PARTICLE_INDEX`, and the frame that applies a shrink rebuilds the simulation order
185
+ * from the moved list before it simulates.
186
+ *
165
187
  * ## Allocation
166
188
  *
167
189
  * The per-frame path allocates the frame graph's records and nothing else: emitter rows are
@@ -185,10 +207,31 @@ export class GPUParticleSystem {
185
207
  scene_context;
186
208
 
187
209
  /**
188
- * Maximum number of simultaneously live particles.
210
+ * Maximum number of simultaneously live particles, as last set. See {@link capacity}.
211
+ * @type {number}
212
+ */
213
+ #capacity;
214
+
215
+ /**
216
+ * The capacity the pool's buffers are built for: {@link #capacity} as of the last
217
+ * {@link execute}, which is where a new one is applied.
189
218
  * @type {number}
190
219
  */
191
- capacity;
220
+ #pool_capacity;
221
+
222
+ /**
223
+ * Set by a shrink, which moves particles and so leaves the simulation order naming slots that
224
+ * are gone: the frame it is applied in rebuilds the order from the moved list, as for a program
225
+ * edit.
226
+ * @type {boolean}
227
+ */
228
+ #rebucket_pending = false;
229
+
230
+ /**
231
+ * The new slots a grow pushes onto the free list. Reused.
232
+ * @type {{first: number, count: number, counter: number}}
233
+ */
234
+ #push_settings = { first: 0, count: 0, counter: PARTICLE_COUNTER.DEAD };
192
235
 
193
236
  /**
194
237
  * @type {EmitterRegistry}
@@ -367,7 +410,8 @@ export class GPUParticleSystem {
367
410
 
368
411
  this.graphics = graphics;
369
412
  this.scene_context = scene_context;
370
- this.capacity = capacity;
413
+ this.#capacity = capacity;
414
+ this.#pool_capacity = capacity;
371
415
 
372
416
  this.registry = new EmitterRegistry(graphics.device);
373
417
 
@@ -378,6 +422,26 @@ export class GPUParticleSystem {
378
422
  this.#create_default_atlas();
379
423
  }
380
424
 
425
+ /**
426
+ * Maximum number of simultaneously live particles.
427
+ * @returns {number}
428
+ */
429
+ get capacity() {
430
+ return this.#capacity;
431
+ }
432
+
433
+ /**
434
+ * Raise or lower the number of particles the pool holds. Applied by the next {@link execute};
435
+ * see the class doc for what happens to the particles already live.
436
+ *
437
+ * @param {number} value
438
+ */
439
+ set capacity(value) {
440
+ assert.isPositiveInteger(value, "capacity");
441
+
442
+ this.#capacity = value;
443
+ }
444
+
381
445
  /**
382
446
  * The buffer the emitter table lives in — what every emitter-reading pass binds.
383
447
  * @returns {GPUBuffer}
@@ -493,8 +557,10 @@ export class GPUParticleSystem {
493
557
  frame.index = this.frame;
494
558
  frame.max_catchup = this.max_catchup_seconds;
495
559
  frame.spawn_command_count = this.#spawn_command_count;
496
- frame.rebucket = this.#program_assignment_version !== this.registry.program_assignment_version;
560
+ frame.rebucket = this.#rebucket_pending
561
+ || this.#program_assignment_version !== this.registry.program_assignment_version;
497
562
  this.#program_assignment_version = this.registry.program_assignment_version;
563
+ this.#rebucket_pending = false;
498
564
 
499
565
  const table = this.registry.table;
500
566
  const emitters = this.#emitters;
@@ -524,7 +590,7 @@ export class GPUParticleSystem {
524
590
  wind: this.scene_context.wind,
525
591
  gpu: buffers,
526
592
  emitters,
527
- capacity: this.capacity,
593
+ capacity: this.#pool_capacity,
528
594
  bucket_count: this.bucketCount(),
529
595
  // Whose register file the VM runs on is decided here, once, for the whole system: the
530
596
  // registry reports what the hungriest registered effect needs and this compares it
@@ -709,14 +775,17 @@ export class GPUParticleSystem {
709
775
  }
710
776
 
711
777
  /**
712
- * Push everything the CPU changed to the GPU: dirty emitter and binding rows, the state buffer's
713
- * growth, the program buffer rebuilt or its changed field span, the spawn-command ring, and the
714
- * coherence scratch's growth. A command context is opened only if something needs one.
778
+ * Push everything the CPU changed to the GPU: a new pool capacity, dirty emitter and binding
779
+ * rows, the state buffer's growth, the program buffer rebuilt or its changed field span, the
780
+ * spawn-command ring, and the coherence scratch's growth. A command context is opened only if
781
+ * something needs one.
715
782
  */
716
783
  #upload() {
717
784
  const registry = this.registry;
718
785
  const device = this.graphics.device;
719
786
 
787
+ this.#apply_capacity();
788
+
720
789
  registry.flush();
721
790
 
722
791
  // All three tables stage into the one database: the emitter rows the author changed, the
@@ -850,7 +919,7 @@ export class GPUParticleSystem {
850
919
  device.queue.writeBuffer(this.buffers.warmup_queue, 0, words.buffer, words.byteOffset, bytes);
851
920
 
852
921
  frame.warmup_count = count;
853
- frame.warmup_capacity = Math.max(1, Math.min(this.capacity, spawned));
922
+ frame.warmup_capacity = Math.max(1, Math.min(this.#pool_capacity, spawned));
854
923
  frame.warmup_schedule = warmup_schedule(max_prewarm);
855
924
  }
856
925
 
@@ -974,6 +1043,18 @@ export class GPUParticleSystem {
974
1043
  return;
975
1044
  }
976
1045
 
1046
+ this.#replace_sim_list(buckets, true);
1047
+ }
1048
+
1049
+ /**
1050
+ * A coherence list for `buckets` programs over the pool as it now stands, in place of the one
1051
+ * there is.
1052
+ *
1053
+ * @param {number} buckets
1054
+ * @param {boolean} carry whether the entries are copied forward — they must be whenever the
1055
+ * next simulate reads the list, and need not be when that frame rebuilds it first
1056
+ */
1057
+ #replace_sim_list(buckets, carry) {
977
1058
  const device = this.graphics.device;
978
1059
  const buffers = this.buffers;
979
1060
 
@@ -982,20 +1063,216 @@ export class GPUParticleSystem {
982
1063
  const previous = buffers.sim_list;
983
1064
  buffers.sim_list = device.createBuffer({
984
1065
  label: "particles/sim_list",
985
- size: (this.capacity + buckets * PARTICLE_SIMULATE_WORKGROUP_SIZE) * WORD,
1066
+ size: (this.#pool_capacity + buckets * PARTICLE_SIMULATE_WORKGROUP_SIZE) * WORD,
986
1067
  usage: STORAGE_GROWABLE,
987
1068
  });
988
1069
 
989
1070
  if (previous !== undefined) {
990
- const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/coherence grow");
991
- cmd.gpu_encoder.copyBufferToBuffer(previous, 0, buffers.sim_list, 0, previous.size);
992
- cmd.finish();
1071
+ if (carry) {
1072
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/coherence grow");
1073
+ cmd.gpu_encoder.copyBufferToBuffer(previous, 0, buffers.sim_list, 0, previous.size);
1074
+ cmd.finish();
1075
+ }
1076
+
993
1077
  previous.destroy();
994
1078
  }
995
1079
 
996
1080
  this.#sim_bucket_capacity = buckets;
997
1081
  }
998
1082
 
1083
+ /**
1084
+ * Rebuild the pool at {@link #capacity} if it was set to something else since the last frame.
1085
+ * Here, at the top of {@link execute}, because the last frame's graph has been submitted and
1086
+ * nothing recorded reads the buffers being replaced.
1087
+ */
1088
+ #apply_capacity() {
1089
+ const capacity = this.#capacity;
1090
+ const previous_capacity = this.#pool_capacity;
1091
+
1092
+ if (capacity === previous_capacity) {
1093
+ return;
1094
+ }
1095
+
1096
+ this.#pool_capacity = capacity;
1097
+ this.#alive_descriptor = BufferResourceDescriptor.from(capacity * WORD, STORAGE);
1098
+
1099
+ if (capacity > previous_capacity) {
1100
+ this.#grow_pool(previous_capacity, capacity);
1101
+ } else {
1102
+ this.#shrink_pool(previous_capacity, capacity);
1103
+ }
1104
+ }
1105
+
1106
+ /**
1107
+ * Every live particle keeps its slot, and the free list keeps its order: both are copied into
1108
+ * buffers of the larger size, and the slots the pool gains are pushed on top of the free list.
1109
+ * The coherence list is carried forward too, so the next simulate reads the list and the count
1110
+ * the last frame left, exactly as it would have.
1111
+ *
1112
+ * The alive list needs nothing: the last frame's is read, if at all, only up to its count, and
1113
+ * the next one is asked for at the new size.
1114
+ *
1115
+ * @param {number} previous_capacity
1116
+ * @param {number} capacity
1117
+ */
1118
+ #grow_pool(previous_capacity, capacity) {
1119
+ const device = this.graphics.device;
1120
+ const buffers = this.buffers;
1121
+ const pool = buffers.pool;
1122
+ const dead = buffers.dead;
1123
+
1124
+ const grown_pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
1125
+ const grown_dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE });
1126
+
1127
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/pool grow");
1128
+
1129
+ this.#copy_words(cmd, pool, grown_pool, previous_capacity * PARTICLE_RECORD_WORD_COUNT);
1130
+ this.#copy_words(cmd, dead, grown_dead, previous_capacity);
1131
+
1132
+ const settings = this.#push_settings;
1133
+ settings.first = previous_capacity;
1134
+ settings.count = capacity - previous_capacity;
1135
+
1136
+ shader_slot_stack_push_range.dispatch({
1137
+ encoder: cmd,
1138
+ group_count_x: this.#launch(settings.count, SLOT_STACK_PUSH_RANGE_WORKGROUP_SIZE),
1139
+ bindings: { settings, counts: buffers.counters, slots: grown_dead },
1140
+ });
1141
+
1142
+ cmd.finish();
1143
+
1144
+ pool.destroy();
1145
+ dead.destroy();
1146
+
1147
+ buffers.pool = grown_pool;
1148
+ buffers.dead = grown_dead;
1149
+
1150
+ if (buffers.sim_list !== undefined) {
1151
+ this.#replace_sim_list(this.#sim_bucket_capacity, true);
1152
+ }
1153
+ }
1154
+
1155
+ /**
1156
+ * The smaller pool is built beside the larger one and every list is rebuilt into it, on the GPU,
1157
+ * since only the GPU knows which slots are live: the records below the new capacity are copied
1158
+ * across, the free slots below it make the new free list, and the particles past it move into
1159
+ * free slots below it ({@link shader_particle_shrink_alive_list}). The counters move to a buffer
1160
+ * of their own for the same reason — the passes count the new lists while reading the old
1161
+ * lengths — carrying everything else in them across.
1162
+ *
1163
+ * The coherence list names slots that are gone, so it is replaced empty and the next frame
1164
+ * rebuilds it from the new alive list before it simulates.
1165
+ *
1166
+ * @param {number} previous_capacity
1167
+ * @param {number} capacity
1168
+ */
1169
+ #shrink_pool(previous_capacity, capacity) {
1170
+ const graphics = this.graphics;
1171
+ const device = graphics.device;
1172
+ const allocator = graphics.buffer_allocator_main;
1173
+ const buffers = this.buffers;
1174
+
1175
+ const pool = buffers.pool;
1176
+ const dead = buffers.dead;
1177
+ const counters = buffers.counters;
1178
+ const alive = this.#alive;
1179
+
1180
+ const shrunk_pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
1181
+ const shrunk_dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE });
1182
+ const shrunk_counters = device.createBuffer({ label: "particles/counters", size: counters.size, usage: STORAGE_GROWABLE });
1183
+ // Taken before the old list goes back, so the pool cannot answer with the list being read.
1184
+ const shrunk_alive = allocator.get(this.#alive_descriptor);
1185
+
1186
+ const cmd = ShadeGPUCommandContext.create(graphics, "particles/pool shrink");
1187
+ const encoder = cmd.gpu_encoder;
1188
+
1189
+ // The two lists' lengths start at zero and the passes count them up; every other word — the
1190
+ // counts the next frame resets or rebuilds, and the bounds accumulators — comes across.
1191
+ encoder.copyBufferToBuffer(counters, 0, shrunk_counters, 0, counters.size);
1192
+ encoder.clearBuffer(shrunk_counters, PARTICLE_COUNTER.ALIVE * WORD, WORD);
1193
+ encoder.clearBuffer(shrunk_counters, PARTICLE_COUNTER.DEAD * WORD, WORD);
1194
+
1195
+ this.#copy_words(cmd, pool, shrunk_pool, capacity * PARTICLE_RECORD_WORD_COUNT);
1196
+
1197
+ // The lists are as long as the old pool at most; the lanes that find nothing past the GPU's
1198
+ // count return at once.
1199
+ const group_count_x = this.#launch(previous_capacity, PARTICLE_SHRINK_WORKGROUP_SIZE);
1200
+
1201
+ shader_particle_shrink_free_list.dispatch({
1202
+ encoder: cmd,
1203
+ group_count_x,
1204
+ bindings: {
1205
+ capacity,
1206
+ previous_counters: counters, previous_dead: dead,
1207
+ counters: shrunk_counters, dead: shrunk_dead,
1208
+ },
1209
+ });
1210
+
1211
+ shader_particle_shrink_alive_list.dispatch({
1212
+ encoder: cmd,
1213
+ group_count_x,
1214
+ bindings: {
1215
+ capacity,
1216
+ previous_counters: counters, previous_alive: alive, previous_pool: pool,
1217
+ counters: shrunk_counters, dead: shrunk_dead, pool: shrunk_pool, alive: shrunk_alive,
1218
+ },
1219
+ });
1220
+
1221
+ cmd.finish();
1222
+
1223
+ pool.destroy();
1224
+ dead.destroy();
1225
+ counters.destroy();
1226
+ allocator.release(alive);
1227
+
1228
+ buffers.pool = shrunk_pool;
1229
+ buffers.dead = shrunk_dead;
1230
+ buffers.counters = shrunk_counters;
1231
+
1232
+ this.#alive = shrunk_alive;
1233
+ buffers.alive = shrunk_alive;
1234
+ buffers.alive_write = shrunk_alive;
1235
+
1236
+ if (buffers.sim_list !== undefined) {
1237
+ this.#replace_sim_list(this.#sim_bucket_capacity, false);
1238
+ }
1239
+
1240
+ this.#rebucket_pending = true;
1241
+ }
1242
+
1243
+ /**
1244
+ * Copy the first `words` words of `source` into `destination`. On the GPU, because the pool and
1245
+ * the free list are written by shaders alone and carry no copy usage — see the usage masks at
1246
+ * the top of this file.
1247
+ *
1248
+ * @param {ShadeGPUCommandContext} cmd
1249
+ * @param {GPUBuffer} source
1250
+ * @param {GPUBuffer} destination
1251
+ * @param {number} words
1252
+ */
1253
+ #copy_words(cmd, source, destination, words) {
1254
+ shader_buffer_copy_storage_to_storage.dispatch({
1255
+ encoder: cmd,
1256
+ group_count_x: this.#launch(words, COMPUTE_SHADER_DEFAULT_GROUP_SIZE),
1257
+ bindings: { source, destination, size: words },
1258
+ });
1259
+ }
1260
+
1261
+ /**
1262
+ * Workgroups for `threads` lanes of a grid-striding pass, clamped to what a dispatch dimension
1263
+ * holds: past that the lanes stride through the rest.
1264
+ *
1265
+ * @param {number} threads
1266
+ * @param {number} workgroup_size
1267
+ * @returns {number}
1268
+ */
1269
+ #launch(threads, workgroup_size) {
1270
+ return Math.min(
1271
+ Math.ceil(threads / workgroup_size),
1272
+ this.graphics.device.limits.maxComputeWorkgroupsPerDimension,
1273
+ );
1274
+ }
1275
+
999
1276
  /**
1000
1277
  * Give this frame its alive list, and hand back the ones nothing reads any more.
1001
1278
  *
@@ -1081,17 +1358,14 @@ export class GPUParticleSystem {
1081
1358
  * the caller's to choose and a dispatch past `maxComputeWorkgroupsPerDimension` is refused.
1082
1359
  */
1083
1360
  #init_free_list() {
1084
- const device = this.graphics.device;
1361
+ const capacity = this.#pool_capacity;
1085
1362
 
1086
1363
  const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/free list init");
1087
1364
 
1088
1365
  shader_slot_stack_fill.dispatch({
1089
1366
  encoder: cmd,
1090
- group_count_x: Math.min(
1091
- Math.ceil(this.capacity / SLOT_STACK_FILL_WORKGROUP_SIZE),
1092
- device.limits.maxComputeWorkgroupsPerDimension,
1093
- ),
1094
- bindings: { slots: this.buffers.dead, capacity: this.capacity },
1367
+ group_count_x: this.#launch(capacity, SLOT_STACK_FILL_WORKGROUP_SIZE),
1368
+ bindings: { slots: this.buffers.dead, capacity },
1095
1369
  });
1096
1370
 
1097
1371
  cmd.finish();
@@ -1102,7 +1376,7 @@ export class GPUParticleSystem {
1102
1376
  */
1103
1377
  #create_buffers() {
1104
1378
  const device = this.graphics.device;
1105
- const capacity = this.capacity;
1379
+ const capacity = this.#pool_capacity;
1106
1380
  const buffers = this.buffers;
1107
1381
 
1108
1382
  buffers.pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The second half of shrinking the particle pool to `capacity` slots: the alive list of the smaller
3
+ * pool, out of the alive list of the larger one. A particle below `capacity` keeps its slot; one at
4
+ * or past it moves into a slot popped off the smaller pool's free list
5
+ * ({@link shader_particle_shrink_free_list}), its record copied across from `previous_pool`. A
6
+ * particle that finds the free list empty is dropped — the smaller pool cannot hold it.
7
+ *
8
+ * `pool` must already hold the larger pool's records below `capacity`: the ones that stay are not
9
+ * copied here. `counters` is the smaller pool's, its `ALIVE` count zero going in; the larger pool's
10
+ * alive count is read out of `previous_counters`.
11
+ *
12
+ * Every live slot below `capacity` is on neither free list, so a pop never hands out a slot a staying
13
+ * particle holds, and the two lists the pass leaves partition `[0, capacity)` again.
14
+ *
15
+ * The launch is the caller's to clamp; every lane strides through the list by the whole grid.
16
+ *
17
+ * @type {ComputeShader}
18
+ */
19
+ export const shader_particle_shrink_alive_list: ComputeShader;
20
+ import { ComputeShader } from "../../shader/ComputeShader.js";
21
+ //# sourceMappingURL=shader_particle_shrink_alive_list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shader_particle_shrink_alive_list.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.js"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;GAiBG;AACH,gDAFU,aAAa,CA0CpB;8BAhF2B,+BAA+B"}
@@ -0,0 +1,83 @@
1
+ import { atomic_u32, u32 } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
+ import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
3
+ import { ComputeShader } from "../../shader/ComputeShader.js";
4
+ import { CodeChunk } from "../../shader/compiler/CodeChunk.js";
5
+ import { ShaderResourceSetDescriptor } from "../../shader/resource/ShaderResourceSetDescriptor.js";
6
+ import { chunk_slot_stack } from "../../gpu_primitive/slot_stack/chunk_slot_stack.js";
7
+ import { PARTICLE_COUNTER } from "../data/PARTICLE_COUNTERS.js";
8
+ import { PARTICLE_RECORD_WORD_COUNT } from "../ParticleConstants.js";
9
+ import { PARTICLE_SHRINK_WORKGROUP_SIZE } from "./shader_particle_shrink_free_list.js";
10
+
11
+ const resources = new ShaderResourceSetDescriptor();
12
+
13
+ // per-dispatch, four bytes, read-only: immediate data rather than a uniform buffer holding one u32
14
+ resources.setImmediate("capacity", u32);
15
+
16
+ resources.createGroup()
17
+ .addStorageBuffer("previous_counters", WebGPUArray.u32, false)
18
+ .addStorageBuffer("previous_alive", WebGPUArray.u32, false)
19
+ .addStorageBuffer("previous_pool", WebGPUArray.u32, false)
20
+ .addStorageBuffer("counters", WebGPUArray.from(atomic_u32), true)
21
+ .addStorageBuffer("dead", WebGPUArray.u32, true)
22
+ .addStorageBuffer("pool", WebGPUArray.u32, true)
23
+ .addStorageBuffer("alive", WebGPUArray.u32, true);
24
+
25
+ /**
26
+ * The second half of shrinking the particle pool to `capacity` slots: the alive list of the smaller
27
+ * pool, out of the alive list of the larger one. A particle below `capacity` keeps its slot; one at
28
+ * or past it moves into a slot popped off the smaller pool's free list
29
+ * ({@link shader_particle_shrink_free_list}), its record copied across from `previous_pool`. A
30
+ * particle that finds the free list empty is dropped — the smaller pool cannot hold it.
31
+ *
32
+ * `pool` must already hold the larger pool's records below `capacity`: the ones that stay are not
33
+ * copied here. `counters` is the smaller pool's, its `ALIVE` count zero going in; the larger pool's
34
+ * alive count is read out of `previous_counters`.
35
+ *
36
+ * Every live slot below `capacity` is on neither free list, so a pop never hands out a slot a staying
37
+ * particle holds, and the two lists the pass leaves partition `[0, capacity)` again.
38
+ *
39
+ * The launch is the caller's to clamp; every lane strides through the list by the whole grid.
40
+ *
41
+ * @type {ComputeShader}
42
+ */
43
+ export const shader_particle_shrink_alive_list = ComputeShader.from({
44
+ label: "Particle Shrink Alive List",
45
+ resources,
46
+ body: CodeChunk.from(
47
+ //language=WGSL
48
+ `
49
+ const COUNTER_ALIVE : u32 = ${PARTICLE_COUNTER.ALIVE}u;
50
+ const COUNTER_DEAD : u32 = ${PARTICLE_COUNTER.DEAD}u;
51
+ const RECORD_WORDS : u32 = ${PARTICLE_RECORD_WORD_COUNT}u;
52
+
53
+ @compute @workgroup_size(${PARTICLE_SHRINK_WORKGROUP_SIZE}, 1, 1)
54
+ fn main(
55
+ @builtin(global_invocation_id) global_id: vec3<u32>,
56
+ @builtin(num_workgroups) group_count: vec3<u32>,
57
+ ) {
58
+ let stride = group_count.x * ${PARTICLE_SHRINK_WORKGROUP_SIZE}u;
59
+ let count = previous_counters[COUNTER_ALIVE];
60
+
61
+ for (var i = global_id.x; i < count; i += stride) {
62
+ var slot = previous_alive[i];
63
+
64
+ if (slot >= capacity) {
65
+ let moved = slot_stack_pop(&counters[COUNTER_DEAD], &dead, capacity);
66
+
67
+ if (moved == SLOT_STACK_NONE) {
68
+ continue;
69
+ }
70
+
71
+ for (var word = 0u; word < RECORD_WORDS; word++) {
72
+ pool[moved * RECORD_WORDS + word] = previous_pool[slot * RECORD_WORDS + word];
73
+ }
74
+
75
+ slot = moved;
76
+ }
77
+
78
+ let a = atomicAdd(&counters[COUNTER_ALIVE], 1u);
79
+ alive[a] = slot;
80
+ }
81
+ }
82
+ `, [chunk_slot_stack]),
83
+ });
@@ -0,0 +1,21 @@
1
+ /** Lanes per workgroup. One free slot each, so this is sized for occupancy and nothing else. */
2
+ export const PARTICLE_SHRINK_WORKGROUP_SIZE: 256;
3
+ /**
4
+ * The first half of shrinking the particle pool to `capacity` slots: the free list of the smaller
5
+ * pool, out of the free list of the larger one. Every free slot below `capacity` is pushed onto
6
+ * `dead`; the slots at or past it go with the old pool.
7
+ *
8
+ * `counters` is the smaller pool's counters buffer, its `DEAD` count zero going in. The larger
9
+ * pool's free-list length is read out of `previous_counters` — a plain view, since nothing writes
10
+ * that buffer here. The order the slots land in is arbitrary, as it is for every push.
11
+ *
12
+ * The launch is the caller's to clamp; every lane strides through the list by the whole grid.
13
+ *
14
+ * {@link shader_particle_shrink_alive_list} pops from the list this builds, so it is a separate
15
+ * dispatch, after this one: a pop can read a slot a concurrent push has counted but not yet written.
16
+ *
17
+ * @type {ComputeShader}
18
+ */
19
+ export const shader_particle_shrink_free_list: ComputeShader;
20
+ import { ComputeShader } from "../../shader/ComputeShader.js";
21
+ //# sourceMappingURL=shader_particle_shrink_free_list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shader_particle_shrink_free_list.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/resize/shader_particle_shrink_free_list.js"],"names":[],"mappings":"AAQA,gGAAgG;AAChG,iDAAkD;AAalD;;;;;;;;;;;;;;;GAeG;AACH,+CAFU,aAAa,CA2BpB;8BA7D2B,+BAA+B"}
@@ -0,0 +1,64 @@
1
+ import { atomic_u32, u32 } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
+ import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
3
+ import { ComputeShader } from "../../shader/ComputeShader.js";
4
+ import { CodeChunk } from "../../shader/compiler/CodeChunk.js";
5
+ import { ShaderResourceSetDescriptor } from "../../shader/resource/ShaderResourceSetDescriptor.js";
6
+ import { chunk_slot_stack } from "../../gpu_primitive/slot_stack/chunk_slot_stack.js";
7
+ import { PARTICLE_COUNTER } from "../data/PARTICLE_COUNTERS.js";
8
+
9
+ /** Lanes per workgroup. One free slot each, so this is sized for occupancy and nothing else. */
10
+ export const PARTICLE_SHRINK_WORKGROUP_SIZE = 256;
11
+
12
+ const resources = new ShaderResourceSetDescriptor();
13
+
14
+ // per-dispatch, four bytes, read-only: immediate data rather than a uniform buffer holding one u32
15
+ resources.setImmediate("capacity", u32);
16
+
17
+ resources.createGroup()
18
+ .addStorageBuffer("previous_counters", WebGPUArray.u32, false)
19
+ .addStorageBuffer("previous_dead", WebGPUArray.u32, false)
20
+ .addStorageBuffer("counters", WebGPUArray.from(atomic_u32), true)
21
+ .addStorageBuffer("dead", WebGPUArray.u32, true);
22
+
23
+ /**
24
+ * The first half of shrinking the particle pool to `capacity` slots: the free list of the smaller
25
+ * pool, out of the free list of the larger one. Every free slot below `capacity` is pushed onto
26
+ * `dead`; the slots at or past it go with the old pool.
27
+ *
28
+ * `counters` is the smaller pool's counters buffer, its `DEAD` count zero going in. The larger
29
+ * pool's free-list length is read out of `previous_counters` — a plain view, since nothing writes
30
+ * that buffer here. The order the slots land in is arbitrary, as it is for every push.
31
+ *
32
+ * The launch is the caller's to clamp; every lane strides through the list by the whole grid.
33
+ *
34
+ * {@link shader_particle_shrink_alive_list} pops from the list this builds, so it is a separate
35
+ * dispatch, after this one: a pop can read a slot a concurrent push has counted but not yet written.
36
+ *
37
+ * @type {ComputeShader}
38
+ */
39
+ export const shader_particle_shrink_free_list = ComputeShader.from({
40
+ label: "Particle Shrink Free List",
41
+ resources,
42
+ body: CodeChunk.from(
43
+ //language=WGSL
44
+ `
45
+ const COUNTER_DEAD : u32 = ${PARTICLE_COUNTER.DEAD}u;
46
+
47
+ @compute @workgroup_size(${PARTICLE_SHRINK_WORKGROUP_SIZE}, 1, 1)
48
+ fn main(
49
+ @builtin(global_invocation_id) global_id: vec3<u32>,
50
+ @builtin(num_workgroups) group_count: vec3<u32>,
51
+ ) {
52
+ let stride = group_count.x * ${PARTICLE_SHRINK_WORKGROUP_SIZE}u;
53
+ let count = previous_counters[COUNTER_DEAD];
54
+
55
+ for (var i = global_id.x; i < count; i += stride) {
56
+ let slot = previous_dead[i];
57
+
58
+ if (slot < capacity) {
59
+ slot_stack_push(&counters[COUNTER_DEAD], &dead, slot);
60
+ }
61
+ }
62
+ }
63
+ `, [chunk_slot_stack]),
64
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_restir_resolve.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/restir/di/chunk_restir_resolve.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,mCAFU,SAAS,CA+DjB;0BAvFwB,oCAAoC"}
1
+ {"version":3,"file":"chunk_restir_resolve.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/restir/di/chunk_restir_resolve.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,mCAFU,SAAS,CA4DjB;0BApFwB,oCAAoC"}