Unravel Engine C++ Reference
Loading...
Searching...
No Matches
particle_system_soa.cpp
Go to the documentation of this file.
2
3#include <bgfx/bgfx.h>
4#include <bx/allocator.h>
5#include <bx/bx.h>
6#include <bx/handlealloc.h>
7#include <bx/rng.h>
12#include <graphics/graphics.h>
14
15#include <algorithm>
16#include <cmath>
17#include <cstring>
18#include <cstdint>
19#include <memory>
20#include <vector>
21
22#define POOLSTL_STD_SUPPLEMENT 1
23#include <poolstl/poolstl.hpp>
24
25namespace unravel
26{
27namespace ps_soa
28{
29namespace
30{
31
32constexpr float k_min_particle_lifespan = 1.0e-4f;
33constexpr float k_emit_dir_zero_len_sq = 1.0e-12f;
34// pos+pivotX | rotation | scale3d+pivotY | uv | color | renderMode
35constexpr uint16_t k_instance_stride = 96;
36// Emitter updates already run under poolstl::par in particle_system.
37// Do not nest poolstl::par inside update — it oversubscribes the pool and
38// inflates both wall time and summed "Update Emitter" thread time.
39constexpr uint32_t k_parallel_particle_threshold = 512;
40constexpr uint32_t k_min_rows_per_job = 256;
41
43bool g_gpu_sim_available = false;
44std::shared_ptr<gpu_program> g_compact_pack_program;
45std::shared_ptr<gpu_program> g_spawn_scatter_program;
46std::shared_ptr<gpu_program> g_indirect_args_program;
47std::shared_ptr<gpu_program> g_sort_program;
48bgfx::UniformHandle g_u_pack0 = BGFX_INVALID_HANDLE;
49bgfx::UniformHandle g_u_pack1 = BGFX_INVALID_HANDLE;
50bgfx::UniformHandle g_u_pack2 = BGFX_INVALID_HANDLE;
51bgfx::UniformHandle g_u_pack3 = BGFX_INVALID_HANDLE;
52bgfx::UniformHandle g_u_pack4 = BGFX_INVALID_HANDLE;
53bgfx::UniformHandle g_u_pack5 = BGFX_INVALID_HANDLE;
54bgfx::UniformHandle g_u_local_to_world = BGFX_INVALID_HANDLE;
55bgfx::UniformHandle g_u_args0 = BGFX_INVALID_HANDLE;
56bgfx::UniformHandle g_u_sort0 = BGFX_INVALID_HANDLE;
57bgfx::UniformHandle g_u_sort1 = BGFX_INVALID_HANDLE;
58bgfx::UniformHandle g_u_spawn0 = BGFX_INVALID_HANDLE;
59bgfx::VertexLayout g_gpu_vec4_layout;
60bgfx::VertexLayout g_gpu_instance_layout;
61bool g_gpu_layouts_ready = false;
62constexpr uint32_t k_quad_index_count = 6;
63
64auto next_pow2_u32(uint32_t value) -> uint32_t
65{
66 if(value <= 1u)
67 {
68 return 1u;
69 }
70 uint32_t v = value - 1u;
71 v |= v >> 1u;
72 v |= v >> 2u;
73 v |= v >> 4u;
74 v |= v >> 8u;
75 v |= v >> 16u;
76 return v + 1u;
77}
78
79void ensure_gpu_layouts()
80{
81 if(g_gpu_layouts_ready)
82 {
83 return;
84 }
85 g_gpu_vec4_layout.begin().add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float).end();
86 g_gpu_instance_layout.begin()
87 .add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float)
88 .add(bgfx::Attrib::TexCoord1, 4, bgfx::AttribType::Float)
89 .add(bgfx::Attrib::TexCoord2, 4, bgfx::AttribType::Float)
90 .add(bgfx::Attrib::TexCoord3, 4, bgfx::AttribType::Float)
91 .add(bgfx::Attrib::TexCoord4, 4, bgfx::AttribType::Float)
92 .add(bgfx::Attrib::TexCoord5, 4, bgfx::AttribType::Float)
93 .end();
94 g_gpu_layouts_ready = true;
95}
96
97struct emitter_gpu_resources
98{
99 bool pending_pack = false;
102 bool luts_valid = false;
103 bool luts_gpu_dirty = true;
105 bool cached_need_ease = false;
107 bx::EaseFn cached_ease_pos = nullptr;
108 uint32_t gpu_capacity = 0;
110 uint32_t high_water = 0;
111 float sim_dt = 0.0f;
112 bgfx::DynamicVertexBufferHandle sim_vb = BGFX_INVALID_HANDLE;
113 bgfx::DynamicVertexBufferHandle instance_vb = BGFX_INVALID_HANDLE;
114 bgfx::DynamicIndexBufferHandle counter_ib = BGFX_INVALID_HANDLE;
115 bgfx::IndirectBufferHandle indirect_buf = BGFX_INVALID_HANDLE;
116 bgfx::DynamicVertexBufferHandle color_lut_vb = BGFX_INVALID_HANDLE;
117 bgfx::DynamicVertexBufferHandle color_speed_lut_vb = BGFX_INVALID_HANDLE;
118 bgfx::DynamicVertexBufferHandle ease_lut_vb = BGFX_INVALID_HANDLE;
119 bgfx::DynamicVertexBufferHandle spawn_vb = BGFX_INVALID_HANDLE;
120 bgfx::DynamicIndexBufferHandle spawn_slots_ib = BGFX_INVALID_HANDLE;
122 std::vector<uint32_t> free_list;
123 std::vector<uint32_t> active_slots;
124 std::vector<gpu_sim_particle> spawn_particles;
125 std::vector<uint32_t> spawn_slots;
126 std::vector<math::vec4> color_lut;
127 std::vector<math::vec4> color_speed_lut;
128 std::vector<math::vec4> ease_lut;
129 emitter_sim_constants constants{};
132 bool trail_bounds_valid = false;
133
134 void clear_trail_bounds()
135 {
137 trail_bounds_valid = false;
138 }
139
140 void destroy_buffers()
141 {
142 if(bgfx::isValid(sim_vb))
143 {
144 bgfx::destroy(sim_vb);
145 sim_vb = BGFX_INVALID_HANDLE;
146 }
147 if(bgfx::isValid(instance_vb))
148 {
149 bgfx::destroy(instance_vb);
150 instance_vb = BGFX_INVALID_HANDLE;
151 }
152 if(bgfx::isValid(counter_ib))
153 {
154 bgfx::destroy(counter_ib);
155 counter_ib = BGFX_INVALID_HANDLE;
156 }
157 if(bgfx::isValid(indirect_buf))
158 {
159 bgfx::destroy(indirect_buf);
160 indirect_buf = BGFX_INVALID_HANDLE;
161 }
162 if(bgfx::isValid(color_lut_vb))
163 {
164 bgfx::destroy(color_lut_vb);
165 color_lut_vb = BGFX_INVALID_HANDLE;
166 }
167 if(bgfx::isValid(color_speed_lut_vb))
168 {
169 bgfx::destroy(color_speed_lut_vb);
170 color_speed_lut_vb = BGFX_INVALID_HANDLE;
171 }
172 if(bgfx::isValid(ease_lut_vb))
173 {
174 bgfx::destroy(ease_lut_vb);
175 ease_lut_vb = BGFX_INVALID_HANDLE;
176 }
177 if(bgfx::isValid(spawn_vb))
178 {
179 bgfx::destroy(spawn_vb);
180 spawn_vb = BGFX_INVALID_HANDLE;
181 }
182 if(bgfx::isValid(spawn_slots_ib))
183 {
184 bgfx::destroy(spawn_slots_ib);
185 spawn_slots_ib = BGFX_INVALID_HANDLE;
186 }
187 gpu_capacity = 0;
188 high_water = 0;
190 pending_pack = false;
191 luts_gpu_dirty = true;
192 free_list.clear();
193 active_slots.clear();
194 spawn_particles.clear();
195 spawn_slots.clear();
196 clear_trail_bounds();
197 }
198
199 void ensure_spawn_upload_capacity(uint32_t spawn_count)
200 {
201 if(spawn_count == 0)
202 {
203 return;
204 }
205 if(spawn_upload_capacity >= spawn_count && bgfx::isValid(spawn_vb) && bgfx::isValid(spawn_slots_ib))
206 {
207 return;
208 }
209 if(bgfx::isValid(spawn_vb))
210 {
211 bgfx::destroy(spawn_vb);
212 spawn_vb = BGFX_INVALID_HANDLE;
213 }
214 if(bgfx::isValid(spawn_slots_ib))
215 {
216 bgfx::destroy(spawn_slots_ib);
217 spawn_slots_ib = BGFX_INVALID_HANDLE;
218 }
219 spawn_upload_capacity = math::max(spawn_count, 64u);
220 const uint16_t spawn_flags = BGFX_BUFFER_COMPUTE_READ | BGFX_BUFFER_ALLOW_RESIZE |
221 BGFX_BUFFER_COMPUTE_FORMAT_32X4 | BGFX_BUFFER_COMPUTE_TYPE_FLOAT;
222 const uint16_t slot_flags = BGFX_BUFFER_COMPUTE_READ | BGFX_BUFFER_ALLOW_RESIZE | BGFX_BUFFER_INDEX32 |
223 BGFX_BUFFER_COMPUTE_FORMAT_32X1 | BGFX_BUFFER_COMPUTE_TYPE_UINT;
224 spawn_vb = bgfx::createDynamicVertexBuffer(spawn_upload_capacity * k_gpu_sim_vec4s_per_particle,
225 g_gpu_vec4_layout,
226 spawn_flags);
227 spawn_slots_ib = bgfx::createDynamicIndexBuffer(spawn_upload_capacity, slot_flags);
228 }
229
230 void reset_freelist(uint32_t max_particles)
231 {
232 free_list.resize(max_particles);
233 for(uint32_t i = 0; i < max_particles; ++i)
234 {
235 free_list[i] = max_particles - 1u - i;
236 }
237 }
238
240 auto ensure_capacity(uint32_t max_particles) -> bool
241 {
242 ensure_gpu_layouts();
243 if(gpu_capacity >= max_particles && bgfx::isValid(sim_vb) && bgfx::isValid(counter_ib) &&
244 bgfx::isValid(indirect_buf))
245 {
246 return false;
247 }
248 const bool keep_pending = pending_pack;
249 destroy_buffers();
250 pending_pack = keep_pending;
251 luts_gpu_dirty = true;
252 gpu_capacity = max_particles;
253 const uint16_t sim_flags = BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_ALLOW_RESIZE |
254 BGFX_BUFFER_COMPUTE_FORMAT_32X4 | BGFX_BUFFER_COMPUTE_TYPE_FLOAT;
255 const uint16_t instance_flags = BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_ALLOW_RESIZE |
256 BGFX_BUFFER_COMPUTE_FORMAT_32X4 | BGFX_BUFFER_COMPUTE_TYPE_FLOAT;
257 const uint16_t counter_flags = BGFX_BUFFER_COMPUTE_READ_WRITE | BGFX_BUFFER_INDEX32 |
258 BGFX_BUFFER_COMPUTE_FORMAT_32X1 | BGFX_BUFFER_COMPUTE_TYPE_UINT;
259 const uint16_t lut_flags = BGFX_BUFFER_COMPUTE_READ | BGFX_BUFFER_ALLOW_RESIZE |
260 BGFX_BUFFER_COMPUTE_FORMAT_32X4 | BGFX_BUFFER_COMPUTE_TYPE_FLOAT;
261 sim_vb = bgfx::createDynamicVertexBuffer(max_particles * k_gpu_sim_vec4s_per_particle,
262 g_gpu_vec4_layout,
263 sim_flags);
264 instance_vb = bgfx::createDynamicVertexBuffer(max_particles, g_gpu_instance_layout, instance_flags);
265 counter_ib = bgfx::createDynamicIndexBuffer(1, counter_flags);
266 indirect_buf = bgfx::createIndirectBuffer(1);
267 color_lut_vb = bgfx::createDynamicVertexBuffer(k_gpu_lut_size, g_gpu_vec4_layout, lut_flags);
268 color_speed_lut_vb = bgfx::createDynamicVertexBuffer(k_gpu_lut_size, g_gpu_vec4_layout, lut_flags);
269 ease_lut_vb = bgfx::createDynamicVertexBuffer(k_gpu_lut_size, g_gpu_vec4_layout, lut_flags);
270 reset_freelist(max_particles);
271 std::vector<gpu_sim_particle> zeros(max_particles);
272 std::memset(zeros.data(), 0, sizeof(gpu_sim_particle) * max_particles);
273 bgfx::update(sim_vb, 0, bgfx::copy(zeros.data(), uint32_t(sizeof(gpu_sim_particle) * max_particles)));
274 uint32_t zero_count = 0;
275 bgfx::update(counter_ib, 0, bgfx::copy(&zero_count, sizeof(uint32_t)));
276 return true;
277 }
278};
279
280auto thread_local_rng() -> bx::RngMwc&
281{
282 thread_local bx::RngMwc rng;
283 return rng;
284}
285
286auto frand01(bx::RngMwc& rng) -> float
287{
288 return bx::frnd(&rng);
289}
290
291auto frand_range(bx::RngMwc& rng, float lo, float hi) -> float
292{
293 return math::mix(lo, hi, frand01(rng));
294}
295
296auto random_unit_vector(bx::RngMwc& rng) -> math::vec3
297{
298 // Marsaglia method for uniform direction on sphere surface.
299 float x = 0.0f;
300 float y = 0.0f;
301 float s = 0.0f;
302 do
303 {
304 x = frand_range(rng, -1.0f, 1.0f);
305 y = frand_range(rng, -1.0f, 1.0f);
306 s = x * x + y * y;
307 } while(s >= 1.0f || s <= 0.0f);
308 const float f = 2.0f * std::sqrt(1.0f - s);
309 return math::vec3(x * f, y * f, 1.0f - 2.0f * s);
310}
311
312auto random_in_unit_ball(bx::RngMwc& rng) -> math::vec3
313{
314 math::vec3 p;
315 do
316 {
317 p = math::vec3(frand_range(rng, -1.0f, 1.0f), frand_range(rng, -1.0f, 1.0f), frand_range(rng, -1.0f, 1.0f));
318 } while(math::dot(p, p) > 1.0f);
319 return p;
320}
321
322auto random_in_unit_disk(bx::RngMwc& rng) -> math::vec2
323{
324 math::vec2 p;
325 do
326 {
327 p = math::vec2(frand_range(rng, -1.0f, 1.0f), frand_range(rng, -1.0f, 1.0f));
328 } while(math::dot(p, p) > 1.0f);
329 return p;
330}
331
332auto random_on_unit_circle(bx::RngMwc& rng) -> math::vec2
333{
334 const float a = frand_range(rng, 0.0f, 6.28318530718f);
335 return math::vec2(std::cos(a), std::sin(a));
336}
337
338struct particle_vertex
339{
340 float x, y, z, u, v;
341
342 static void init()
343 {
344 ms_layout.begin()
345 .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
346 .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
347 .end();
348 }
349
350 static bgfx::VertexLayout ms_layout;
351};
352
353bgfx::VertexLayout particle_vertex::ms_layout;
354
355static particle_vertex s_quad_vertices[4] = {
356 {-0.5f, -0.5f, 0.0f, 0.0f, 1.0f},
357 {0.5f, -0.5f, 0.0f, 1.0f, 1.0f},
358 {0.5f, 0.5f, 0.0f, 1.0f, 0.0f},
359 {-0.5f, 0.5f, 0.0f, 0.0f, 0.0f},
360};
361
362static const uint16_t s_quad_indices[6] = {0, 1, 2, 2, 3, 0};
363
364struct particle_soa
365{
366 std::vector<math::vec3> start;
367 std::vector<math::vec3> end0;
368 std::vector<math::vec3> end1;
369 std::vector<float> scale_start;
370 std::vector<float> scale_end;
371 std::vector<float> life;
372 std::vector<float> lifespan;
373 std::vector<float> texsheet_seed;
374 std::vector<math::vec3> position;
375 std::vector<math::color> color;
376 std::vector<float> scale;
377 std::vector<float> cached_speed;
378 std::vector<math::vec2> uv_offset;
379 std::vector<math::vec2> uv_scale;
380 std::vector<math::quat> rotation;
381 uint32_t count = 0;
382 uint32_t capacity = 0;
383
384 void resize(uint32_t max_particles)
385 {
386 capacity = max_particles;
387 count = 0;
388 start.resize(max_particles);
389 end0.resize(max_particles);
390 end1.resize(max_particles);
391 scale_start.resize(max_particles);
392 scale_end.resize(max_particles);
393 life.resize(max_particles);
394 lifespan.resize(max_particles);
395 texsheet_seed.resize(max_particles);
396 position.resize(max_particles);
397 color.resize(max_particles);
398 scale.resize(max_particles);
399 cached_speed.resize(max_particles);
400 uv_offset.resize(max_particles);
401 uv_scale.resize(max_particles);
402 rotation.resize(max_particles);
403 }
404
405 void clear_live()
406 {
407 count = 0;
408 }
409
410 // Compact only sim streams; render caches are rebuilt immediately after.
411 void move_sim_particle(uint32_t dst, uint32_t src)
412 {
413 start[dst] = start[src];
414 end0[dst] = end0[src];
415 end1[dst] = end1[src];
416 scale_start[dst] = scale_start[src];
417 scale_end[dst] = scale_end[src];
418 life[dst] = life[src];
419 lifespan[dst] = lifespan[src];
420 texsheet_seed[dst] = texsheet_seed[src];
421 }
422};
423
424void expand_aabb_sphere(math::bbox& aabb, const math::vec3& pos, float radius)
425{
426 const math::vec3 pad(radius);
427 aabb.add_point(pos - pad);
428 aabb.add_point(pos + pad);
429}
430
431void bake_constants(const emitter_desc& desc,
432 const emitter_transform_state& transform,
433 emitter_sim_constants& out_constants)
434{
435 const math::vec3 scale = transform.current.get_scale();
436 out_constants.features = desc.bake_features();
437 out_constants.space = desc.motion.space;
438 out_constants.opacity = desc.appearance.opacity;
439 out_constants.color_intensity = desc.appearance.color_intensity;
440 out_constants.avg_system_scale = (scale.x + scale.y + scale.z) / 3.0f;
441 out_constants.particle_scale_3d = desc.appearance.initial_scale_3d;
442 out_constants.pivot = desc.render.pivot;
443 out_constants.render_mode = desc.render.render_mode;
444 out_constants.blend_mode = desc.render.blend_mode;
445 out_constants.texture_mode = desc.render.texture_mode;
446 out_constants.tex_sheet_tiles = desc.render.tex_sheet_tiles;
447 out_constants.tex_sheet_cycles = desc.render.tex_sheet_cycles;
448 out_constants.tex_sheet_randomize = desc.render.tex_sheet_randomize;
449 out_constants.size_by_speed_range = desc.appearance.size_by_speed_range;
450 out_constants.size_by_speed_velocity_range = desc.appearance.size_by_speed_velocity_range;
451 out_constants.color_by_speed_velocity_range = desc.appearance.color_by_speed_velocity_range;
452 const float size_span = desc.appearance.size_by_speed_velocity_range.max - desc.appearance.size_by_speed_velocity_range.min;
453 const float color_span =
454 desc.appearance.color_by_speed_velocity_range.max - desc.appearance.color_by_speed_velocity_range.min;
455 out_constants.inv_size_by_speed_velocity_span = (size_span > 0.0f) ? (1.0f / size_span) : 0.0f;
456 out_constants.inv_color_by_speed_velocity_span = (color_span > 0.0f) ? (1.0f / color_span) : 0.0f;
457 out_constants.ease_pos = bx::getEaseFunc(desc.motion.position_easing);
458 out_constants.local_to_world = transform.current;
459}
460
461float calculate_particle_speed(const math::vec3& start,
462 const math::vec3& end0,
463 const math::vec3& end1,
464 float lifespan,
465 float tt_pos)
466{
467 const math::vec3 initial_velocity = end0 - start;
468 const math::vec3 final_velocity = end1 - end0;
469 const math::vec3 current_velocity = math::mix(initial_velocity, final_velocity, tt_pos);
470 const math::vec3 velocity_per_second = current_velocity * (1.0f / lifespan);
471 return math::length(velocity_per_second);
472}
473
474// Fast path: world space, linear ease, no align/texsheet/speed effects.
475void update_particle_basic(particle_soa& particles,
476 uint32_t index,
477 const emitter_desc& desc,
478 const emitter_sim_constants& constants)
479{
480 const float life = particles.life[index];
481 math::color sampled_color = desc.appearance.color_gradient.sample(life);
482 sampled_color.value.a *= constants.opacity;
483 sampled_color.value.r *= constants.color_intensity;
484 sampled_color.value.g *= constants.color_intensity;
485 sampled_color.value.b *= constants.color_intensity;
486 particles.color[index] = sampled_color;
487 particles.scale[index] =
488 math::mix(particles.scale_start[index], particles.scale_end[index], life) * constants.avg_system_scale;
489 const math::vec3 p0 = math::mix(particles.start[index], particles.end0[index], life);
490 const math::vec3 p1 = math::mix(particles.end0[index], particles.end1[index], life);
491 particles.position[index] = math::mix(p0, p1, life);
492 // Shader treats xyz~0 as "no rotation" (glm quat is w,x,y,z).
493 particles.rotation[index] = math::identity<math::quat>();
494 particles.uv_offset[index] = math::vec2(0.0f, 0.0f);
495 particles.uv_scale[index] = math::vec2(1.0f, 1.0f);
496}
497
498void update_particle_full(particle_soa& particles,
499 uint32_t index,
500 const emitter_desc& desc,
501 const emitter_sim_constants& constants)
502{
503 const float life = particles.life[index];
504 const float tt_pos = constants.ease_pos ? constants.ease_pos(life) : life;
508 float particle_speed = 0.0f;
509 if(need_speed)
510 {
511 particle_speed = calculate_particle_speed(particles.start[index],
512 particles.end0[index],
513 particles.end1[index],
514 particles.lifespan[index],
515 tt_pos);
516 particles.cached_speed[index] = particle_speed;
517 }
518 math::color sampled_color = desc.appearance.color_gradient.sample(life);
520 {
521 const float speed_factor = math::clamp(
523 0.0f,
524 1.0f);
525 const math::color speed_color = desc.appearance.color_by_speed_gradient.sample(speed_factor);
526 sampled_color.value *= speed_color.value;
527 }
528 sampled_color.value.a *= constants.opacity;
529 sampled_color.value.r *= constants.color_intensity;
530 sampled_color.value.g *= constants.color_intensity;
531 sampled_color.value.b *= constants.color_intensity;
532 particles.color[index] = sampled_color;
533 float scale = math::mix(particles.scale_start[index], particles.scale_end[index], life) * constants.avg_system_scale;
535 {
536 const float speed_factor = math::clamp(
538 0.0f,
539 1.0f);
541 }
542 particles.scale[index] = scale;
543 const math::vec3 p0 = math::mix(particles.start[index], particles.end0[index], tt_pos);
544 const math::vec3 p1 = math::mix(particles.end0[index], particles.end1[index], tt_pos);
545 const math::vec3 local_pos = math::mix(p0, p1, tt_pos);
547 {
548 const math::vec4 world_pos4 = constants.local_to_world * math::vec4(local_pos, 1.0f);
549 particles.position[index] = math::vec3(world_pos4.x, world_pos4.y, world_pos4.z);
550 }
551 else
552 {
553 particles.position[index] = local_pos;
554 }
556 {
557 const math::vec3 velocity0 = particles.end0[index] - particles.start[index];
558 const math::vec3 velocity1 = particles.end1[index] - particles.end0[index];
559 const math::vec3 current_velocity = math::mix(velocity0, velocity1, tt_pos);
560 const float velocity_len_sq = math::dot(current_velocity, current_velocity);
561 if(velocity_len_sq > 0.0001f)
562 {
563 const math::vec3 direction = math::normalize(current_velocity);
564 math::vec3 up_ref(0.0f, 1.0f, 0.0f);
565 if(math::abs(math::dot(direction, up_ref)) > 0.99f)
566 {
567 up_ref = math::vec3(1.0f, 0.0f, 0.0f);
568 }
569 particles.rotation[index] = math::look_rotation(direction, up_ref);
570 }
571 else
572 {
573 particles.rotation[index] = math::identity<math::quat>();
574 }
575 }
577 {
578 const float uv_scale_x = 1.0f / constants.tex_sheet_tiles.x;
579 const float uv_scale_y = 1.0f / constants.tex_sheet_tiles.y;
580 const uint32_t total_frames =
581 uint32_t(constants.tex_sheet_tiles.x) * uint32_t(constants.tex_sheet_tiles.y);
582 float anim_progress = life * constants.tex_sheet_cycles;
584 {
585 anim_progress += particles.texsheet_seed[index];
586 }
587 anim_progress = math::fmod(anim_progress, 1.0f);
588 const uint32_t current_frame = uint32_t(anim_progress * float(total_frames)) % total_frames;
589 const uint32_t tile_x = current_frame % uint32_t(constants.tex_sheet_tiles.x);
590 const uint32_t tile_y = current_frame / uint32_t(constants.tex_sheet_tiles.x);
591 particles.uv_offset[index] = math::vec2(float(tile_x) * uv_scale_x, float(tile_y) * uv_scale_y);
592 particles.uv_scale[index] = math::vec2(uv_scale_x, uv_scale_y);
593 }
594 else
595 {
596 particles.uv_offset[index] = math::vec2(0.0f, 0.0f);
597 particles.uv_scale[index] = math::vec2(1.0f, 1.0f);
598 }
599}
600
601auto has_heavy_features(emitter_feature features) -> bool
602{
606 return static_cast<uint32_t>(heavy) != 0u;
607}
608
609void update_particle_properties(particle_soa& particles,
610 uint32_t index,
611 const emitter_desc& desc,
612 const emitter_sim_constants& constants)
613{
614 if(!has_heavy_features(constants.features))
615 {
616 update_particle_basic(particles, index, desc, constants);
617 return;
618 }
619 update_particle_full(particles, index, desc, constants);
620}
621
622void update_particles_range(particle_soa& particles,
623 uint32_t begin,
624 uint32_t end,
625 const emitter_desc& desc,
626 const emitter_sim_constants& constants)
627{
628 if(!has_heavy_features(constants.features))
629 {
630 for(uint32_t i = begin; i < end; ++i)
631 {
632 update_particle_basic(particles, i, desc, constants);
633 }
634 return;
635 }
636 for(uint32_t i = begin; i < end; ++i)
637 {
638 update_particle_full(particles, i, desc, constants);
639 }
640}
641
642struct emitter
643{
644 void create(emitter_shape shape, emitter_direction direction, uint32_t max_particles)
645 {
646 sim.reset();
647 shape_ = shape;
648 direction_ = direction;
649 particles_.resize(max_particles);
650 rng_.reset();
651 }
652
653 void destroy()
654 {
655 gpu_.destroy_buffers();
656 particles_ = particle_soa{};
657 }
658
659 void reset()
660 {
661 sim.reset();
662 particles_.clear_live();
663 for(uint32_t i = 0; i < particles_.capacity; ++i)
664 {
665 particles_.lifespan[i] = 0.0f;
666 particles_.life[i] = 0.0f;
667 }
668 rng_.reset();
669 gpu_.pending_pack = false;
670 gpu_.luts_valid = false;
671 gpu_.active_slots.clear();
672 gpu_.high_water = 0;
673 gpu_.spawn_particles.clear();
674 gpu_.spawn_slots.clear();
675 gpu_.clear_trail_bounds();
676 if(gpu_.gpu_capacity > 0)
677 {
678 gpu_.reset_freelist(gpu_.gpu_capacity);
679 if(bgfx::isValid(gpu_.sim_vb))
680 {
681 std::vector<gpu_sim_particle> zeros(gpu_.gpu_capacity);
682 std::memset(zeros.data(), 0, sizeof(gpu_sim_particle) * gpu_.gpu_capacity);
683 bgfx::update(gpu_.sim_vb,
684 0,
685 bgfx::copy(zeros.data(), uint32_t(sizeof(gpu_sim_particle) * gpu_.gpu_capacity)));
686 }
687 }
688 }
689
690 auto resolve_backend() const -> particle_sim_backend
691 {
692 if(!g_gpu_sim_available)
693 {
695 }
696 if(gpu_.has_backend_override)
697 {
698 return gpu_.backend_override;
699 }
700 return g_default_sim_backend;
701 }
702
703 auto wants_gpu_pack() const -> bool
704 {
705 // Artist / component selects backend; no particle-count gate.
706 return g_gpu_sim_available && resolve_backend() == particle_sim_backend::gpu;
707 }
708
709 void fill_gradient_lut(const math::gradient<math::color>& gradient, std::vector<math::vec4>& out_lut)
710 {
711 out_lut.resize(k_gpu_lut_size);
712 for(uint32_t i = 0; i < k_gpu_lut_size; ++i)
713 {
714 const float t = float(i) / float(k_gpu_lut_size - 1);
715 const math::color c = gradient.sample(t);
716 out_lut[i] = math::vec4(c.value.r, c.value.g, c.value.b, c.value.a);
717 }
718 }
719
720 void ensure_gpu_luts(const emitter_desc& desc, const emitter_sim_constants& constants)
721 {
722 const bool need_color_speed = has_feature(constants.features, emitter_feature::color_by_speed);
723 const bool need_ease = has_feature(constants.features, emitter_feature::non_linear_ease);
724 if(gpu_.luts_valid && gpu_.cached_ease_pos == constants.ease_pos &&
725 gpu_.cached_need_color_speed == need_color_speed && gpu_.cached_need_ease == need_ease &&
726 gpu_.cached_features == constants.features)
727 {
728 return;
729 }
730 fill_gradient_lut(desc.appearance.color_gradient, gpu_.color_lut);
731 if(need_color_speed)
732 {
733 fill_gradient_lut(desc.appearance.color_by_speed_gradient, gpu_.color_speed_lut);
734 }
735 else if(gpu_.color_speed_lut.size() != k_gpu_lut_size)
736 {
737 gpu_.color_speed_lut.assign(k_gpu_lut_size, math::vec4(1.0f));
738 }
739 gpu_.ease_lut.resize(k_gpu_lut_size);
740 for(uint32_t i = 0; i < k_gpu_lut_size; ++i)
741 {
742 const float t = float(i) / float(k_gpu_lut_size - 1);
743 const float eased = (need_ease && constants.ease_pos) ? constants.ease_pos(t) : t;
744 gpu_.ease_lut[i] = math::vec4(eased, 0.0f, 0.0f, 0.0f);
745 }
746 gpu_.cached_ease_pos = constants.ease_pos;
747 gpu_.cached_need_color_speed = need_color_speed;
748 gpu_.cached_need_ease = need_ease;
749 gpu_.cached_features = constants.features;
750 gpu_.luts_valid = true;
751 gpu_.luts_gpu_dirty = true;
752 }
753
754 auto compute_gpu_particle_radius(const emitter_desc& desc, const emitter_sim_constants& constants) const -> float
755 {
756 const frange_t start_scale_range = desc.appearance.scale_gradient.sample(0.0f);
757 const frange_t end_scale_range = desc.appearance.scale_gradient.sample(1.0f);
758 const float max_author_scale = math::max(math::max(start_scale_range.min, start_scale_range.max),
759 math::max(end_scale_range.min, end_scale_range.max));
760 float size_speed_mul = 1.0f;
762 {
763 size_speed_mul = math::max(constants.size_by_speed_range.min, constants.size_by_speed_range.max);
764 }
765 const float base_extent = math::max(constants.particle_scale_3d.x,
766 math::max(constants.particle_scale_3d.y, constants.particle_scale_3d.z)) *
767 0.5f;
768 const math::vec2 pivot_offset = constants.pivot - math::vec2(0.5f, 0.5f);
769 const float pivot_pad_factor = math::max(math::abs(pivot_offset.x), math::abs(pivot_offset.y)) * 2.0f;
770 const float particle_scale = max_author_scale * constants.avg_system_scale * size_speed_mul;
771 const float max_extent = base_extent * particle_scale;
772 return max_extent + pivot_pad_factor * max_extent;
773 }
774
775 void accumulate_conservative_gpu_bounds(math::bbox& aabb,
776 const emitter_desc& desc,
777 const emitter_sim_constants& constants,
778 const math::vec3& emitter_pos)
779 {
780 const float particle_radius = compute_gpu_particle_radius(desc, constants);
781 const math::vec3 shape_pad = math::abs(desc.emission.shape_scale) + math::abs(desc.emission.shape_position);
782 const float shape_radius = math::length(shape_pad);
783 expand_aabb_sphere(aabb, emitter_pos, shape_radius + particle_radius);
784 }
785
786 void expand_gpu_trail_bounds_for_particle(const math::vec3& start,
787 const math::vec3& end0,
788 const math::vec3& end1,
789 float particle_radius)
790 {
791 // Path is a double-mix of start/end0/end1 — covered by the convex hull of those points.
792 if(!gpu_.trail_bounds_valid)
793 {
794 gpu_.trail_bounds.reset();
795 gpu_.trail_bounds_valid = true;
796 }
797 expand_aabb_sphere(gpu_.trail_bounds, start, particle_radius);
798 expand_aabb_sphere(gpu_.trail_bounds, end0, particle_radius);
799 expand_aabb_sphere(gpu_.trail_bounds, end1, particle_radius);
800 }
801
802 void rebuild_gpu_trail_bounds_from_live(float particle_radius)
803 {
804 gpu_.clear_trail_bounds();
805 for(uint32_t slot : gpu_.active_slots)
806 {
807 expand_gpu_trail_bounds_for_particle(particles_.start[slot],
808 particles_.end0[slot],
809 particles_.end1[slot],
810 particle_radius);
811 }
812 }
813
814 void rebuild_gpu_slot_lists()
815 {
816 gpu_.free_list.clear();
817 gpu_.active_slots.clear();
818 uint32_t high = 0;
819 for(uint32_t i = 0; i < particles_.capacity; ++i)
820 {
821 if(particles_.lifespan[i] <= 0.0f)
822 {
823 particles_.lifespan[i] = 0.0f;
824 particles_.life[i] = 0.0f;
825 gpu_.free_list.push_back(i);
826 continue;
827 }
828 gpu_.active_slots.push_back(i);
829 high = math::max(high, i + 1u);
830 }
831 particles_.count = uint32_t(gpu_.active_slots.size());
832 gpu_.high_water = high;
833 }
834
835 void reclaim_gpu_slots(float sim_dt)
836 {
837 if(gpu_.active_slots.empty())
838 {
839 particles_.count = 0;
840 gpu_.high_water = 0;
841 gpu_.clear_trail_bounds();
842 return;
843 }
844 APP_SCOPE_PERF("Particles/SOA GPU Reclaim");
845 uint32_t write = 0;
846 uint32_t high = 0;
847 for(uint32_t i = 0; i < gpu_.active_slots.size(); ++i)
848 {
849 const uint32_t slot = gpu_.active_slots[i];
850 particles_.life[slot] += sim_dt / math::max(particles_.lifespan[slot], k_min_particle_lifespan);
851 if(particles_.life[slot] > 1.0f)
852 {
853 particles_.lifespan[slot] = 0.0f;
854 particles_.life[slot] = 0.0f;
855 gpu_.free_list.push_back(slot);
856 continue;
857 }
858 gpu_.active_slots[write++] = slot;
859 high = math::max(high, slot + 1u);
860 }
861 gpu_.active_slots.resize(write);
862 particles_.count = write;
863 gpu_.high_water = high;
864 if(write == 0)
865 {
866 gpu_.clear_trail_bounds();
867 }
868 if(gpu_.free_list.empty() && write < particles_.capacity)
869 {
870 rebuild_gpu_slot_lists();
871 }
872 }
873
874 void fill_gpu_sim_particle(uint32_t slot, gpu_sim_particle& dst) const
875 {
876 dst.start_x = particles_.start[slot].x;
877 dst.start_y = particles_.start[slot].y;
878 dst.start_z = particles_.start[slot].z;
879 dst.life = particles_.life[slot];
880 dst.end0_x = particles_.end0[slot].x;
881 dst.end0_y = particles_.end0[slot].y;
882 dst.end0_z = particles_.end0[slot].z;
883 dst.lifespan = particles_.lifespan[slot];
884 dst.end1_x = particles_.end1[slot].x;
885 dst.end1_y = particles_.end1[slot].y;
886 dst.end1_z = particles_.end1[slot].z;
887 dst.scale_start = particles_.scale_start[slot];
888 dst.scale_end = particles_.scale_end[slot];
889 dst.texsheet_seed = particles_.texsheet_seed[slot];
890 dst.pad0 = 0.0f;
891 dst.pad1 = 0.0f;
892 const math::quat rot = math::identity<math::quat>();
893 dst.rot_x = rot.x;
894 dst.rot_y = rot.y;
895 dst.rot_z = rot.z;
896 dst.rot_w = rot.w;
897 }
898
899 void stage_gpu_spawn(uint32_t slot)
900 {
901 gpu_sim_particle dst{};
902 fill_gpu_sim_particle(slot, dst);
903 gpu_.spawn_particles.push_back(dst);
904 gpu_.spawn_slots.push_back(slot);
905 gpu_.active_slots.push_back(slot);
906 gpu_.high_water = math::max(gpu_.high_water, slot + 1u);
907 }
908
909 void upload_gpu_sim_slot(uint32_t slot)
910 {
911 if(!bgfx::isValid(gpu_.sim_vb) || slot >= gpu_.gpu_capacity)
912 {
913 return;
914 }
915 gpu_sim_particle dst{};
916 fill_gpu_sim_particle(slot, dst);
917 bgfx::update(gpu_.sim_vb,
919 bgfx::copy(&dst, sizeof(gpu_sim_particle)));
920 }
921
923 void flush_gpu_spawn_uploads_cpu()
924 {
925 const uint32_t spawn_count = uint32_t(gpu_.spawn_slots.size());
926 if(spawn_count == 0 || !bgfx::isValid(gpu_.sim_vb))
927 {
928 gpu_.spawn_particles.clear();
929 gpu_.spawn_slots.clear();
930 return;
931 }
932 std::vector<uint32_t> order(spawn_count);
933 for(uint32_t i = 0; i < spawn_count; ++i)
934 {
935 order[i] = i;
936 }
937 std::sort(order.begin(),
938 order.end(),
939 [&](uint32_t a, uint32_t b)
940 {
941 return gpu_.spawn_slots[a] < gpu_.spawn_slots[b];
942 });
943 std::vector<gpu_sim_particle> run;
944 run.reserve(64);
945 uint32_t run_start_slot = 0;
946 auto flush_run = [&]()
947 {
948 if(run.empty())
949 {
950 return;
951 }
952 bgfx::update(gpu_.sim_vb,
953 run_start_slot * k_gpu_sim_vec4s_per_particle,
954 bgfx::copy(run.data(), uint32_t(sizeof(gpu_sim_particle) * run.size())));
955 run.clear();
956 };
957 for(uint32_t oi = 0; oi < spawn_count; ++oi)
958 {
959 const uint32_t src = order[oi];
960 const uint32_t slot = gpu_.spawn_slots[src];
961 if(run.empty())
962 {
963 run_start_slot = slot;
964 run.push_back(gpu_.spawn_particles[src]);
965 continue;
966 }
967 const uint32_t expected = run_start_slot + uint32_t(run.size());
968 if(slot == expected)
969 {
970 run.push_back(gpu_.spawn_particles[src]);
971 continue;
972 }
973 flush_run();
974 run_start_slot = slot;
975 run.push_back(gpu_.spawn_particles[src]);
976 }
977 flush_run();
978 gpu_.spawn_particles.clear();
979 gpu_.spawn_slots.clear();
980 }
981
982 void resync_gpu_slots_from_cpu()
983 {
984 // Dense CPU layout only guarantees live data in [0, count). Clear the rest.
985 for(uint32_t i = particles_.count; i < particles_.capacity; ++i)
986 {
987 particles_.lifespan[i] = 0.0f;
988 particles_.life[i] = 0.0f;
989 }
990 rebuild_gpu_slot_lists();
991 for(uint32_t slot : gpu_.active_slots)
992 {
993 upload_gpu_sim_slot(slot);
994 }
995 }
996
997 void prepare_gpu_resident(const emitter_desc& desc,
998 const emitter_sim_constants& constants,
999 math::bbox& aabb,
1000 const math::vec3& emitter_pos,
1001 float sim_dt)
1002 {
1003 APP_SCOPE_PERF("Particles/SOA Prepare GPU Resident");
1004 gpu_.constants = constants;
1005 gpu_.sim_dt = sim_dt;
1006 ensure_gpu_luts(desc, constants);
1007 accumulate_conservative_gpu_bounds(aabb, desc, constants, emitter_pos);
1008 // World trails stay where spawned; rebuild hull from live control points so bounds shrink on death.
1009 if(desc.motion.space == simulation_space::world)
1010 {
1011 if(particles_.count == 0)
1012 {
1013 gpu_.clear_trail_bounds();
1014 }
1015 else
1016 {
1017 rebuild_gpu_trail_bounds_from_live(compute_gpu_particle_radius(desc, constants));
1018 }
1019 if(gpu_.trail_bounds_valid)
1020 {
1021 aabb.add_point(gpu_.trail_bounds.min);
1022 aabb.add_point(gpu_.trail_bounds.max);
1023 }
1024 }
1025 gpu_.pending_pack = particles_.count > 0;
1026 if(!gpu_.pending_pack)
1027 {
1028 gpu_.spawn_particles.clear();
1029 gpu_.spawn_slots.clear();
1030 }
1031 }
1032
1033 void update(float dt,
1034 const emitter_desc& desc,
1035 emitter_transform_state& transform,
1036 emitter_playback_desc& playback)
1037 {
1038 if(sim.first_update)
1039 {
1040 transform.previous = transform.current;
1041 }
1042 const bool was_playing = sim.playing;
1043 const bool was_loop = sim.loop;
1044 sim.playing = playback.playing;
1045 sim.loop = desc.emission.loop;
1046 if(was_playing != sim.playing || was_loop != sim.loop)
1047 {
1048 sim.total_particles_spawned = 0;
1049 if(sim.playing && !was_playing)
1050 {
1051 sim.start_delay_elapsed = 0.0f;
1052 }
1053 }
1054 float sim_dt = dt;
1055 if(playback.paused)
1056 {
1057 sim_dt = 0.0f;
1058 }
1059 else if(sim.playing)
1060 {
1061 sim.start_delay_elapsed += dt;
1062 }
1063 const math::vec3 current_pos = transform.current.get_position();
1064 if(!playback.paused)
1065 {
1066 sim.push_temporal_sample(current_pos, sim_dt);
1067 }
1068 if(!desc.emission.loop && sim.total_particles_spawned >= particles_.capacity)
1069 {
1070 playback.playing = false;
1071 sim.playing = false;
1072 sim.total_particles_spawned = 0;
1073 }
1074 emitter_sim_constants constants{};
1075 bake_constants(desc, transform, constants);
1076 sim.features = constants.features;
1077 math::bbox aabb;
1078 aabb.reset();
1079 aabb.add_point(current_pos - math::vec3(0.5f));
1080 aabb.add_point(current_pos + math::vec3(0.5f));
1081 const bool use_gpu = wants_gpu_pack();
1082 if(use_gpu)
1083 {
1084 if(gpu_.ensure_capacity(particles_.capacity))
1085 {
1086 resync_gpu_slots_from_cpu();
1087 }
1088 }
1089 else
1090 {
1091 compact_alive(sim_dt);
1092 APP_SCOPE_PERF("Particles/SOA Update Properties");
1093 update_particles_range(particles_, 0, particles_.count, desc, constants);
1094 }
1095 if(desc.emission.emission_lifetime > 0.0f && playback.playing)
1096 {
1097 const bool start_delay_elapsed = sim.start_delay_elapsed >= desc.emission.start_delay;
1098 const bool initial_emission_complete = sim.total_particles_spawned >= particles_.capacity;
1099 if(start_delay_elapsed && (desc.emission.loop || !initial_emission_complete))
1100 {
1101 spawn(desc, constants, transform, sim_dt, use_gpu);
1102 }
1103 }
1104 particles_.count = math::min(particles_.count, particles_.capacity);
1105 if(use_gpu)
1106 {
1107 // Shadow life matches the upcoming GPU compact-pack advance; frees slots for next frame.
1108 reclaim_gpu_slots(sim_dt);
1109 prepare_gpu_resident(desc, constants, aabb, current_pos, sim_dt);
1110 }
1111 else
1112 {
1113 accumulate_world_bounds(aabb, constants);
1114 }
1115 if(sim.first_update)
1116 {
1117 sim.first_update = false;
1118 }
1119 sim.world_bounds = aabb;
1120 transform.previous = transform.current;
1121 }
1122
1123 void update_bounds_only(const emitter_desc& desc, emitter_transform_state& transform)
1124 {
1125 // Frozen: keep last trail/particle AABB and union a cheap emitter region so the
1126 // emitter can re-enter any drawing camera without advancing life/emission.
1127 const math::vec3 current_pos = transform.current.get_position();
1128 math::bbox aabb = sim.world_bounds;
1129 if(!aabb.is_populated())
1130 {
1131 aabb.reset();
1132 aabb.add_point(current_pos - math::vec3(0.5f));
1133 aabb.add_point(current_pos + math::vec3(0.5f));
1134 }
1135 emitter_sim_constants constants{};
1136 bake_constants(desc, transform, constants);
1137 accumulate_conservative_gpu_bounds(aabb, desc, constants, current_pos);
1138 gpu_.pending_pack = false;
1139 gpu_.spawn_particles.clear();
1140 gpu_.spawn_slots.clear();
1141 sim.world_bounds = aabb;
1142 transform.previous = transform.current;
1143 }
1144
1145 void compact_alive(float sim_dt)
1146 {
1147 const uint32_t old_count = particles_.count;
1148 if(old_count == 0)
1149 {
1150 return;
1151 }
1152 APP_SCOPE_PERF("Particles/SOA Compact");
1153 uint32_t write = 0;
1154 for(uint32_t i = 0; i < old_count; ++i)
1155 {
1156 if(particles_.lifespan[i] <= 0.0f)
1157 {
1158 continue;
1159 }
1160 particles_.life[i] += sim_dt / particles_.lifespan[i];
1161 if(particles_.life[i] > 1.0f)
1162 {
1163 continue;
1164 }
1165 if(write != i)
1166 {
1167 particles_.move_sim_particle(write, i);
1168 }
1169 ++write;
1170 }
1171 particles_.count = write;
1172 }
1173
1174 void accumulate_world_bounds(math::bbox& aabb, const emitter_sim_constants& constants)
1175 {
1176 if(particles_.count == 0)
1177 {
1178 return;
1179 }
1180 APP_SCOPE_PERF("Particles/SOA Bounds");
1181 const float base_extent = math::max(constants.particle_scale_3d.x,
1182 math::max(constants.particle_scale_3d.y, constants.particle_scale_3d.z)) *
1183 0.5f;
1184 const math::vec2 pivot_offset = constants.pivot - math::vec2(0.5f, 0.5f);
1185 const float pivot_pad_factor = math::max(math::abs(pivot_offset.x), math::abs(pivot_offset.y)) * 2.0f;
1186 for(uint32_t i = 0; i < particles_.count; ++i)
1187 {
1188 const float max_extent = base_extent * particles_.scale[i];
1189 const float radius = max_extent + pivot_pad_factor * max_extent;
1190 expand_aabb_sphere(aabb, particles_.position[i], radius);
1191 }
1192 }
1193
1194 void spawn(const emitter_desc& desc,
1195 const emitter_sim_constants& constants,
1196 const emitter_transform_state& transform,
1197 float dt,
1198 bool skip_cpu_properties)
1199 {
1200 if(desc.emission.particles_per_second <= 0.0f)
1201 {
1202 return;
1203 }
1204 const float time_per_particle = 1.0f / desc.emission.particles_per_second;
1205 sim.emission_time_accum += dt;
1206 const uint32_t num_to_emit = uint32_t(sim.emission_time_accum / time_per_particle);
1207 sim.emission_time_accum -= float(num_to_emit) * time_per_particle;
1208 const uint32_t max_emittable =
1209 skip_cpu_properties ? uint32_t(gpu_.free_list.size()) : (particles_.capacity - particles_.count);
1210 const uint32_t actual_emit_count = math::min(num_to_emit, max_emittable);
1211 if(actual_emit_count == 0)
1212 {
1213 return;
1214 }
1215 if(skip_cpu_properties && gpu_.ensure_capacity(particles_.capacity))
1216 {
1217 resync_gpu_slots_from_cpu();
1218 }
1219 const math::vec3 effective_position = transform.current.get_position();
1220 const math::vec3 system_scale = transform.current.get_scale();
1221 const math::vec3 emission_shape_scale = desc.emission.shape_scale;
1222 const math::mat4 effective_transform = transform.current;
1223 const math::mat3 rotation_matrix = math::mat3(effective_transform);
1224 float lifetime_multiplier = 1.0f;
1226 {
1227 const float emitter_speed =
1228 sim.calculate_smoothed_emitter_speed(desc.motion.lifetime_by_emitter_speed_range.max);
1229 const float speed_factor = math::clamp(
1230 (emitter_speed - desc.motion.lifetime_by_emitter_speed_range.min) /
1231 (desc.motion.lifetime_by_emitter_speed_range.max - desc.motion.lifetime_by_emitter_speed_range.min),
1232 0.0f,
1233 1.0f);
1234 lifetime_multiplier = desc.motion.lifetime_by_emitter_speed_gradient.sample(speed_factor);
1235 }
1236 const float life_span = math::max(desc.motion.lifetime * lifetime_multiplier, k_min_particle_lifespan);
1237 const float life_span_squared = life_span * life_span;
1238 math::vec3 gravity_vector(0.0f, -9.81f * desc.motion.gravity_scale * life_span_squared, 0.0f);
1239 math::vec3 force_vector = desc.motion.force_over_lifetime * life_span_squared;
1240 if(desc.motion.space == simulation_space::world)
1241 {
1242 gravity_vector.y *= system_scale.y;
1243 force_vector *= system_scale;
1244 }
1245 const float velocity_damping_factor = (1.0f - desc.motion.velocity_damping);
1246 const bool expand_world_trail =
1247 skip_cpu_properties && desc.motion.space == simulation_space::world;
1248 const float gpu_trail_particle_radius =
1249 expand_world_trail ? compute_gpu_particle_radius(desc, constants) : 0.0f;
1250 math::vec3 prev_pos = transform.previous.get_position();
1251 if(sim.temporal_count >= 2)
1252 {
1253 prev_pos = sim.temporal_positions[sim.temporal_count - 2];
1254 }
1255 const uint32_t base_index = skip_cpu_properties ? 0u : particles_.count;
1256 if(!skip_cpu_properties)
1257 {
1258 particles_.count += actual_emit_count;
1259 }
1260 sim.total_particles_spawned += actual_emit_count;
1261 const auto emit_one = [&](uint32_t ii, bx::RngMwc& rng)
1262 {
1263 const float base_emission_phase = float(ii) / float(actual_emit_count);
1264 const float emission_phase = base_emission_phase * desc.motion.temporal_motion;
1265 uint32_t index = base_index + ii;
1266 if(skip_cpu_properties)
1267 {
1268 index = gpu_.free_list.back();
1269 gpu_.free_list.pop_back();
1270 }
1271 const math::vec3 up(0.0f, 1.0f, 0.0f);
1272 math::vec3 pos;
1273 if(desc.emission.spawn_location == spawn_location::surface)
1274 {
1275 switch(shape_)
1276 {
1277 default:
1279 pos = random_unit_vector(rng);
1280 break;
1282 {
1283 math::vec3 sphere_pos = random_unit_vector(rng);
1284 if(sphere_pos.y < 0.0f)
1285 {
1286 sphere_pos.y = -sphere_pos.y;
1287 }
1288 pos = sphere_pos;
1289 }
1290 break;
1292 {
1293 const math::vec2 circle_pos = random_on_unit_circle(rng);
1294 pos = math::vec3(circle_pos.x, 0.0f, circle_pos.y);
1295 }
1296 break;
1297 case emitter_shape::box:
1298 {
1299 const int face_index = int(frand01(rng) * 6.0f);
1300 const float u = frand_range(rng, -1.0f, 1.0f);
1301 const float v = frand_range(rng, -1.0f, 1.0f);
1302 switch(face_index)
1303 {
1304 case 0: pos = math::vec3(1.0f, u, v); break;
1305 case 1: pos = math::vec3(-1.0f, u, v); break;
1306 case 2: pos = math::vec3(u, 1.0f, v); break;
1307 case 3: pos = math::vec3(u, -1.0f, v); break;
1308 case 4: pos = math::vec3(u, v, 1.0f); break;
1309 default: pos = math::vec3(u, v, -1.0f); break;
1310 }
1311 }
1312 break;
1314 {
1315 const int edge_index = int(frand01(rng) * 4.0f);
1316 const float t = frand_range(rng, -1.0f, 1.0f);
1317 switch(edge_index)
1318 {
1319 case 0: pos = math::vec3(t, 0.0f, 1.0f); break;
1320 case 1: pos = math::vec3(1.0f, 0.0f, t); break;
1321 case 2: pos = math::vec3(t, 0.0f, -1.0f); break;
1322 default: pos = math::vec3(-1.0f, 0.0f, t); break;
1323 }
1324 }
1325 break;
1326 }
1327 }
1328 else
1329 {
1330 switch(shape_)
1331 {
1332 default:
1334 pos = random_in_unit_ball(rng);
1335 break;
1337 {
1338 math::vec3 sphere_pos = random_in_unit_ball(rng);
1339 if(math::dot(sphere_pos, up) < 0.0f)
1340 {
1341 sphere_pos = -sphere_pos;
1342 }
1343 pos = sphere_pos;
1344 }
1345 break;
1347 {
1348 const math::vec2 circle_pos = random_in_unit_disk(rng);
1349 pos = math::vec3(circle_pos.x, 0.0f, circle_pos.y);
1350 }
1351 break;
1352 case emitter_shape::box:
1353 pos = math::vec3(frand_range(rng, -1.0f, 1.0f),
1354 frand_range(rng, -1.0f, 1.0f),
1355 frand_range(rng, -1.0f, 1.0f));
1356 break;
1358 pos = math::vec3(frand_range(rng, -1.0f, 1.0f), 0.0f, frand_range(rng, -1.0f, 1.0f));
1359 break;
1360 }
1361 }
1362 pos = (desc.emission.shape_position + pos) * emission_shape_scale;
1363 math::vec3 dir;
1364 switch(direction_)
1365 {
1366 default:
1368 dir = up;
1369 break;
1371 {
1372 const float len_sq = math::dot(pos, pos);
1373 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) : up;
1374 }
1375 break;
1377 {
1378 const float len_sq = math::dot(pos, pos);
1379 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) : up;
1380 }
1381 break;
1382 }
1383 math::vec3 start = pos;
1384 const frange_t end_velocity_range = desc.motion.velocity_gradient.sample(1.0f);
1385 const float end_velocity = math::mix(end_velocity_range.min, end_velocity_range.max, frand01(rng));
1386 math::vec3 end = dir * end_velocity + start;
1387 if(direction_ == emitter_direction::inward)
1388 {
1389 std::swap(start, end);
1390 dir *= -1.0f;
1391 }
1392 particles_.lifespan[index] = life_span;
1393 particles_.life[index] = 0.0f;
1394 const math::vec3 interpolated_emitter_pos = math::mix(prev_pos, effective_position, emission_phase);
1395 if(desc.motion.space == simulation_space::local)
1396 {
1397 particles_.start[index] = start;
1398 particles_.end0[index] = end;
1399 }
1400 else
1401 {
1402 particles_.start[index] = rotation_matrix * start + interpolated_emitter_pos;
1403 particles_.end0[index] = rotation_matrix * end + interpolated_emitter_pos;
1404 }
1405 if(desc.motion.velocity_damping > 0.0f)
1406 {
1407 const math::vec3 velocity = particles_.end0[index] - particles_.start[index];
1408 particles_.end0[index] = particles_.start[index] + velocity * velocity_damping_factor;
1409 }
1410 particles_.end1[index] = particles_.end0[index] + gravity_vector + force_vector;
1411 if(expand_world_trail)
1412 {
1413 expand_gpu_trail_bounds_for_particle(particles_.start[index],
1414 particles_.end0[index],
1415 particles_.end1[index],
1416 gpu_trail_particle_radius);
1417 }
1418 const frange_t start_scale_range = desc.appearance.scale_gradient.sample(0.0f);
1419 const frange_t end_scale_range = desc.appearance.scale_gradient.sample(1.0f);
1420 particles_.scale_start[index] = math::mix(start_scale_range.min, start_scale_range.max, frand01(rng));
1421 particles_.scale_end[index] = math::mix(end_scale_range.min, end_scale_range.max, frand01(rng));
1422 particles_.texsheet_seed[index] = frand01(rng);
1423 if(!skip_cpu_properties)
1424 {
1425 update_particle_properties(particles_, index, desc, constants);
1426 }
1427 else
1428 {
1429 stage_gpu_spawn(index);
1430 }
1431 };
1432 APP_SCOPE_PERF("Particles/SOA Spawn");
1433 if(skip_cpu_properties)
1434 {
1435 gpu_.spawn_particles.reserve(gpu_.spawn_particles.size() + actual_emit_count);
1436 gpu_.spawn_slots.reserve(gpu_.spawn_slots.size() + actual_emit_count);
1437 }
1438 for(uint32_t ii = 0; ii < actual_emit_count; ++ii)
1439 {
1440 emit_one(ii, rng_);
1441 }
1442 if(skip_cpu_properties)
1443 {
1444 particles_.count += actual_emit_count;
1445 }
1446 }
1447
1450 particle_soa particles_;
1451 emitter_sim_state sim;
1452 bx::RngMwc rng_;
1453 emitter_sim_constants cached_constants_{};
1454 emitter_gpu_resources gpu_;
1455};
1456
1457struct batched_particle
1458{
1459 float dist = 0.0f;
1460 uint32_t emitter_idx = 0;
1461 uint32_t particle_idx = 0;
1462};
1463
1464auto float_to_sortable_uint(float value) -> uint32_t
1465{
1466 uint32_t bits = 0;
1467 static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
1468 std::memcpy(&bits, &value, sizeof(uint32_t));
1469 const uint32_t mask = static_cast<uint32_t>(-static_cast<int32_t>(bits >> 31)) | 0x80000000u;
1470 return bits ^ mask;
1471}
1472
1473void radix_sort_desc_distances(std::vector<batched_particle>& items, std::vector<batched_particle>& scratch)
1474{
1475 const uint32_t n = static_cast<uint32_t>(items.size());
1476 if(n < 2)
1477 {
1478 return;
1479 }
1480 scratch.resize(n);
1481 constexpr uint32_t k_bits = 8;
1482 constexpr uint32_t k_bins = 1u << k_bits;
1483 constexpr uint32_t k_passes = 4;
1484 uint32_t counts[k_bins];
1485 for(uint32_t pass = 0; pass < k_passes; ++pass)
1486 {
1487 const uint32_t shift = pass * k_bits;
1488 std::memset(counts, 0, sizeof(counts));
1489 for(uint32_t i = 0; i < n; ++i)
1490 {
1491 const uint32_t key = ~float_to_sortable_uint(items[i].dist);
1492 ++counts[(key >> shift) & (k_bins - 1u)];
1493 }
1494 uint32_t sum = 0;
1495 for(uint32_t b = 0; b < k_bins; ++b)
1496 {
1497 const uint32_t c = counts[b];
1498 counts[b] = sum;
1499 sum += c;
1500 }
1501 for(uint32_t i = 0; i < n; ++i)
1502 {
1503 const uint32_t key = ~float_to_sortable_uint(items[i].dist);
1504 const uint32_t bin = (key >> shift) & (k_bins - 1u);
1505 scratch[counts[bin]++] = items[i];
1506 }
1507 items.swap(scratch);
1508 }
1509}
1510
1511struct particle_system_soa
1512{
1513 void init(uint16_t max_emitters)
1514 {
1515 static bx::DefaultAllocator allocator;
1516 allocator_ = &allocator;
1517 emitter_alloc_ = bx::createHandleAlloc(allocator_, max_emitters);
1518 emitters_.resize(max_emitters);
1519 particle_vertex::init();
1520 quad_vbh_ = bgfx::createVertexBuffer(bgfx::makeRef(s_quad_vertices, sizeof(s_quad_vertices)),
1521 particle_vertex::ms_layout);
1522 quad_ibh_ = bgfx::createIndexBuffer(bgfx::makeRef(s_quad_indices, sizeof(s_quad_indices)));
1523 tex_color_ = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler);
1524 view_camera_ = bgfx::createUniform("u_viewCamera", bgfx::UniformType::Mat4);
1525 eye_pos_ = bgfx::createUniform("u_eyePos", bgfx::UniformType::Vec4);
1526 }
1527
1528 void shutdown()
1529 {
1530 for(auto& em : emitters_)
1531 {
1532 em.gpu_.destroy_buffers();
1533 }
1534 if(bgfx::isValid(g_u_pack0))
1535 {
1536 bgfx::destroy(g_u_pack0);
1537 g_u_pack0 = BGFX_INVALID_HANDLE;
1538 }
1539 if(bgfx::isValid(g_u_pack1))
1540 {
1541 bgfx::destroy(g_u_pack1);
1542 g_u_pack1 = BGFX_INVALID_HANDLE;
1543 }
1544 if(bgfx::isValid(g_u_pack2))
1545 {
1546 bgfx::destroy(g_u_pack2);
1547 g_u_pack2 = BGFX_INVALID_HANDLE;
1548 }
1549 if(bgfx::isValid(g_u_pack3))
1550 {
1551 bgfx::destroy(g_u_pack3);
1552 g_u_pack3 = BGFX_INVALID_HANDLE;
1553 }
1554 if(bgfx::isValid(g_u_pack4))
1555 {
1556 bgfx::destroy(g_u_pack4);
1557 g_u_pack4 = BGFX_INVALID_HANDLE;
1558 }
1559 if(bgfx::isValid(g_u_pack5))
1560 {
1561 bgfx::destroy(g_u_pack5);
1562 g_u_pack5 = BGFX_INVALID_HANDLE;
1563 }
1564 if(bgfx::isValid(g_u_local_to_world))
1565 {
1566 bgfx::destroy(g_u_local_to_world);
1567 g_u_local_to_world = BGFX_INVALID_HANDLE;
1568 }
1569 if(bgfx::isValid(g_u_args0))
1570 {
1571 bgfx::destroy(g_u_args0);
1572 g_u_args0 = BGFX_INVALID_HANDLE;
1573 }
1574 if(bgfx::isValid(g_u_sort0))
1575 {
1576 bgfx::destroy(g_u_sort0);
1577 g_u_sort0 = BGFX_INVALID_HANDLE;
1578 }
1579 if(bgfx::isValid(g_u_sort1))
1580 {
1581 bgfx::destroy(g_u_sort1);
1582 g_u_sort1 = BGFX_INVALID_HANDLE;
1583 }
1584 if(bgfx::isValid(g_u_spawn0))
1585 {
1586 bgfx::destroy(g_u_spawn0);
1587 g_u_spawn0 = BGFX_INVALID_HANDLE;
1588 }
1589 g_compact_pack_program.reset();
1590 g_spawn_scatter_program.reset();
1591 g_indirect_args_program.reset();
1592 g_sort_program.reset();
1593 g_gpu_sim_available = false;
1594 g_default_sim_backend = particle_sim_backend::cpu;
1595 bgfx::destroy(tex_color_);
1596 bgfx::destroy(view_camera_);
1597 bgfx::destroy(eye_pos_);
1598 bgfx::destroy(quad_vbh_);
1599 bgfx::destroy(quad_ibh_);
1600 bx::destroyHandleAlloc(allocator_, emitter_alloc_);
1601 allocator_ = nullptr;
1602 }
1603
1604
1605 void init_gpu(rtti::context& ctx)
1606 {
1607 auto& am = ctx.get_cached<asset_manager>();
1608 auto cs_compact = am.get_asset<gfx::shader>("engine:/data/shaders/particles/cs_particle_compact_pack.sc");
1609 auto cs_spawn = am.get_asset<gfx::shader>("engine:/data/shaders/particles/cs_particle_spawn_scatter.sc");
1610 auto cs_args = am.get_asset<gfx::shader>("engine:/data/shaders/particles/cs_particle_indirect_args.sc");
1611 auto cs_sort = am.get_asset<gfx::shader>("engine:/data/shaders/particles/cs_particle_sort_bitonic.sc");
1612 if(!cs_compact)
1613 {
1614 APPLOG_WARNING("Particles: GPU resident compact shader missing; CPU backend only");
1615 g_gpu_sim_available = false;
1616 return;
1617 }
1618 g_compact_pack_program = std::make_shared<gpu_program>(cs_compact);
1619 if(!g_compact_pack_program || !g_compact_pack_program->is_valid())
1620 {
1621 APPLOG_WARNING("Particles: GPU resident compact program invalid; CPU backend only");
1622 g_compact_pack_program.reset();
1623 g_gpu_sim_available = false;
1624 return;
1625 }
1626 if(cs_spawn)
1627 {
1628 g_spawn_scatter_program = std::make_shared<gpu_program>(cs_spawn);
1629 if(!g_spawn_scatter_program || !g_spawn_scatter_program->is_valid())
1630 {
1631 APPLOG_WARNING("Particles: GPU spawn scatter program invalid; using CPU coalesce uploads");
1632 g_spawn_scatter_program.reset();
1633 }
1634 }
1635 if(cs_args)
1636 {
1637 g_indirect_args_program = std::make_shared<gpu_program>(cs_args);
1638 if(!g_indirect_args_program || !g_indirect_args_program->is_valid())
1639 {
1640 g_indirect_args_program.reset();
1641 }
1642 }
1643 if(cs_sort)
1644 {
1645 g_sort_program = std::make_shared<gpu_program>(cs_sort);
1646 if(!g_sort_program || !g_sort_program->is_valid())
1647 {
1648 APPLOG_WARNING("Particles: GPU sort program invalid; Normal blend will be unsorted on GPU path");
1649 g_sort_program.reset();
1650 }
1651 }
1652 g_u_pack0 = bgfx::createUniform("u_pack0", bgfx::UniformType::Vec4);
1653 g_u_pack1 = bgfx::createUniform("u_pack1", bgfx::UniformType::Vec4);
1654 g_u_pack2 = bgfx::createUniform("u_pack2", bgfx::UniformType::Vec4);
1655 g_u_pack3 = bgfx::createUniform("u_pack3", bgfx::UniformType::Vec4);
1656 g_u_pack4 = bgfx::createUniform("u_pack4", bgfx::UniformType::Vec4);
1657 g_u_pack5 = bgfx::createUniform("u_pack5", bgfx::UniformType::Vec4);
1658 g_u_local_to_world = bgfx::createUniform("u_localToWorld", bgfx::UniformType::Mat4);
1659 g_u_args0 = bgfx::createUniform("u_args0", bgfx::UniformType::Vec4);
1660 g_u_sort0 = bgfx::createUniform("u_sort0", bgfx::UniformType::Vec4);
1661 g_u_sort1 = bgfx::createUniform("u_sort1", bgfx::UniformType::Vec4);
1662 g_u_spawn0 = bgfx::createUniform("u_spawn0", bgfx::UniformType::Vec4);
1663 ensure_gpu_layouts();
1664 g_gpu_sim_available = true;
1665 APPLOG_INFO("Particles: GPU resident sim available (per-emitter Simulation Backend)");
1666 }
1667
1668 void flush_emitter_gpu_spawns(emitter& em, bgfx::ViewId view)
1669 {
1670 const uint32_t spawn_count = uint32_t(em.gpu_.spawn_slots.size());
1671 if(spawn_count == 0)
1672 {
1673 return;
1674 }
1675 APP_SCOPE_PERF("Particles/SOA GPU Spawn Upload");
1676 // Scatter CS: two contiguous uploads + one dispatch beats sorting/coalescing many
1677 // sparse bgfx::update calls when freelist slots are fragmented.
1678 if(g_spawn_scatter_program && g_spawn_scatter_program->begin() && bgfx::isValid(em.gpu_.sim_vb))
1679 {
1680 em.gpu_.ensure_spawn_upload_capacity(spawn_count);
1681 if(bgfx::isValid(em.gpu_.spawn_vb) && bgfx::isValid(em.gpu_.spawn_slots_ib))
1682 {
1683 bgfx::update(em.gpu_.spawn_vb,
1684 0,
1685 bgfx::copy(em.gpu_.spawn_particles.data(),
1686 uint32_t(sizeof(gpu_sim_particle) * spawn_count)));
1687 bgfx::update(em.gpu_.spawn_slots_ib,
1688 0,
1689 bgfx::copy(em.gpu_.spawn_slots.data(), uint32_t(sizeof(uint32_t) * spawn_count)));
1690 float spawn0[4] = {float(spawn_count), 0.0f, 0.0f, 0.0f};
1691 bgfx::setBuffer(0, em.gpu_.spawn_vb, bgfx::Access::Read);
1692 bgfx::setBuffer(1, em.gpu_.spawn_slots_ib, bgfx::Access::Read);
1693 bgfx::setBuffer(2, em.gpu_.sim_vb, bgfx::Access::ReadWrite);
1694 bgfx::setUniform(g_u_spawn0, spawn0);
1695 const uint32_t groups = (spawn_count + k_gpu_cs_threads - 1) / k_gpu_cs_threads;
1696 bgfx::dispatch(view, g_spawn_scatter_program->native_handle(), groups, 1, 1);
1697 g_spawn_scatter_program->end();
1698 em.gpu_.spawn_particles.clear();
1699 em.gpu_.spawn_slots.clear();
1700 return;
1701 }
1702 g_spawn_scatter_program->end();
1703 }
1704 em.flush_gpu_spawn_uploads_cpu();
1705 }
1706
1707 auto dispatch_gpu_resident(emitter& em, bool sort_by_depth, const math::vec3& eye, bgfx::ViewId pack_view) -> bool
1708 {
1709 if(!em.gpu_.pending_pack || em.particles_.count == 0 || !g_compact_pack_program)
1710 {
1711 return false;
1712 }
1713 APP_SCOPE_PERF("Particles/SOA GPU Resident Dispatch");
1714 em.gpu_.ensure_capacity(em.particles_.capacity);
1715 if(!bgfx::isValid(em.gpu_.sim_vb) || !bgfx::isValid(em.gpu_.instance_vb) || !bgfx::isValid(em.gpu_.counter_ib))
1716 {
1717 return false;
1718 }
1719 flush_emitter_gpu_spawns(em, pack_view);
1720 if(em.gpu_.luts_gpu_dirty)
1721 {
1722 bgfx::update(em.gpu_.color_lut_vb,
1723 0,
1724 bgfx::copy(em.gpu_.color_lut.data(), uint32_t(sizeof(math::vec4) * k_gpu_lut_size)));
1725 bgfx::update(em.gpu_.color_speed_lut_vb,
1726 0,
1727 bgfx::copy(em.gpu_.color_speed_lut.data(), uint32_t(sizeof(math::vec4) * k_gpu_lut_size)));
1728 bgfx::update(em.gpu_.ease_lut_vb,
1729 0,
1730 bgfx::copy(em.gpu_.ease_lut.data(), uint32_t(sizeof(math::vec4) * k_gpu_lut_size)));
1731 em.gpu_.luts_gpu_dirty = false;
1732 }
1733 uint32_t zero_count = 0;
1734 bgfx::update(em.gpu_.counter_ib, 0, bgfx::copy(&zero_count, sizeof(uint32_t)));
1735 const auto& c = em.gpu_.constants;
1736 const uint32_t dispatch_count = math::max(em.gpu_.high_water, 1u);
1737 float pack0[4] = {c.opacity,
1738 c.color_intensity,
1739 c.avg_system_scale,
1740 float(static_cast<int>(c.render_mode))};
1741 float pack1[4] = {c.pivot.x, c.pivot.y, float(dispatch_count), float(gpu_feature_mask(c.features))};
1742 float pack2[4] = {c.particle_scale_3d.x, c.particle_scale_3d.y, c.particle_scale_3d.z, c.tex_sheet_cycles};
1743 float pack3[4] = {c.tex_sheet_tiles.x,
1744 c.tex_sheet_tiles.y,
1745 c.tex_sheet_randomize ? 1.0f : 0.0f,
1746 float(k_quad_index_count)};
1747 float pack4[4] = {c.size_by_speed_range.min,
1748 c.size_by_speed_range.max,
1749 c.inv_size_by_speed_velocity_span,
1750 c.size_by_speed_velocity_range.min};
1751 float pack5[4] = {c.inv_color_by_speed_velocity_span,
1752 c.color_by_speed_velocity_range.min,
1753 em.gpu_.sim_dt,
1754 0.0f};
1755 if(!g_compact_pack_program->begin())
1756 {
1757 return false;
1758 }
1759 bgfx::setBuffer(0, em.gpu_.sim_vb, bgfx::Access::ReadWrite);
1760 bgfx::setBuffer(1, em.gpu_.instance_vb, bgfx::Access::Write);
1761 bgfx::setBuffer(2, em.gpu_.counter_ib, bgfx::Access::ReadWrite);
1762 bgfx::setBuffer(3, em.gpu_.color_lut_vb, bgfx::Access::Read);
1763 bgfx::setBuffer(4, em.gpu_.color_speed_lut_vb, bgfx::Access::Read);
1764 bgfx::setBuffer(5, em.gpu_.ease_lut_vb, bgfx::Access::Read);
1765 bgfx::setUniform(g_u_pack0, pack0);
1766 bgfx::setUniform(g_u_pack1, pack1);
1767 bgfx::setUniform(g_u_pack2, pack2);
1768 bgfx::setUniform(g_u_pack3, pack3);
1769 bgfx::setUniform(g_u_pack4, pack4);
1770 bgfx::setUniform(g_u_pack5, pack5);
1771 bgfx::setUniform(g_u_local_to_world, &c.local_to_world[0][0]);
1772 const uint32_t groups = (dispatch_count + k_gpu_cs_threads - 1) / k_gpu_cs_threads;
1773 bgfx::dispatch(pack_view, g_compact_pack_program->native_handle(), groups, 1, 1);
1774 g_compact_pack_program->end();
1775 (void)sort_by_depth;
1776 (void)eye;
1777 return true;
1778 }
1779
1780 auto draw_gpu_emitter(emitter& em,
1781 uint8_t view,
1782 bgfx::ProgramHandle program,
1783 const float* view_camera,
1784 const float* eye_pos_vec4,
1785 bgfx::TextureHandle texture,
1786 uint64_t blend_state) -> uint32_t
1787 {
1788 APP_SCOPE_PERF("Particles/SOA GPU Draw");
1789 const uint32_t draw_count = em.particles_.count;
1790 if(draw_count == 0)
1791 {
1792 return 0;
1793 }
1794 bgfx::setVertexBuffer(0, quad_vbh_);
1795 bgfx::setIndexBuffer(quad_ibh_);
1796 bgfx::setState(0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW |
1797 blend_state);
1798 bgfx::setTexture(0, tex_color_, texture);
1799 bgfx::setUniform(view_camera_, view_camera);
1800 bgfx::setUniform(eye_pos_, eye_pos_vec4);
1801 // Compact-pack densifies into [0, alive). CPU shadow count matches after reclaim.
1802 // Prefer explicit instance count over indirect — writing IndirectBuffer from CS is
1803 // not reliable on all backends and previously left instanceCount at 0.
1804 bgfx::setInstanceDataBuffer(em.gpu_.instance_vb, 0, draw_count);
1805 bgfx::submit(view, program);
1806 return draw_count;
1807 }
1808
1809 auto create_emitter(emitter_shape shape, emitter_direction direction, uint32_t max_particles) -> emitter_handle
1810 {
1811 emitter_handle handle{emitter_alloc_->alloc()};
1812 if(is_valid(handle))
1813 {
1814 emitters_[handle.idx].create(shape, direction, max_particles);
1815 }
1816 return handle;
1817 }
1818
1819 void destroy_emitter(emitter_handle handle)
1820 {
1821 BX_ASSERT(is_valid(handle), "destroy_emitter invalid handle");
1822 emitters_[handle.idx].destroy();
1823 emitter_alloc_->free(handle.idx);
1824 }
1825
1826 void reset_emitter(emitter_handle handle)
1827 {
1828 BX_ASSERT(is_valid(handle), "reset_emitter invalid handle");
1829 emitters_[handle.idx].reset();
1830 }
1831
1832 void update_emitter(emitter_handle handle,
1833 float dt,
1834 const emitter_desc& desc,
1835 emitter_transform_state& transform,
1836 emitter_playback_desc& playback)
1837 {
1838 BX_ASSERT(is_valid(handle), "update_emitter invalid handle");
1839 emitters_[handle.idx].update(dt, desc, transform, playback);
1840 bake_constants(desc, transform, emitters_[handle.idx].cached_constants_);
1841 }
1842
1843 void update_emitter_bounds_only(emitter_handle handle,
1844 const emitter_desc& desc,
1845 emitter_transform_state& transform)
1846 {
1847 BX_ASSERT(is_valid(handle), "update_emitter_bounds_only invalid handle");
1848 emitters_[handle.idx].update_bounds_only(desc, transform);
1849 bake_constants(desc, transform, emitters_[handle.idx].cached_constants_);
1850 }
1851
1852 void sync_gpu_simulation()
1853 {
1854 if(!g_gpu_sim_available || !emitter_alloc_)
1855 {
1856 return;
1857 }
1858 const uint16_t num_handles = emitter_alloc_->getNumHandles();
1859 if(num_handles == 0)
1860 {
1861 return;
1862 }
1863 bool any_work = false;
1864 const uint16_t* handles = emitter_alloc_->getHandles();
1865 for(uint16_t i = 0; i < num_handles; ++i)
1866 {
1867 emitter& em = emitters_[handles[i]];
1868 if(!em.wants_gpu_pack())
1869 {
1870 continue;
1871 }
1872 if(em.gpu_.pending_pack || !em.gpu_.spawn_slots.empty())
1873 {
1874 any_work = true;
1875 break;
1876 }
1877 }
1878 if(!any_work)
1879 {
1880 return;
1881 }
1882 APP_SCOPE_PERF("Particles/SOA GPU Sync");
1883 // Before particle draw so this view id sorts earlier. Renderer-based freeze
1884 // clears pending_pack so undrawn emitters are skipped here.
1885 gfx::render_pass pass("Particles/GPU Sim");
1886 pass.touch();
1887 const math::vec3 eye(0.0f);
1888 for(uint16_t i = 0; i < num_handles; ++i)
1889 {
1890 emitter& em = emitters_[handles[i]];
1891 if(!em.wants_gpu_pack())
1892 {
1893 continue;
1894 }
1895 if(!em.gpu_.pending_pack && em.gpu_.spawn_slots.empty())
1896 {
1897 continue;
1898 }
1899 dispatch_gpu_resident(em, false, eye, pass.id);
1900 }
1901 }
1902
1903 auto has_updated(emitter_handle handle) -> bool
1904 {
1905 BX_ASSERT(is_valid(handle), "has_updated invalid handle");
1906 return !emitters_[handle.idx].sim.first_update;
1907 }
1908
1909 void get_aabb(emitter_handle handle, math::bbox& out_aabb)
1910 {
1911 BX_ASSERT(is_valid(handle), "get_aabb invalid handle");
1912 out_aabb = emitters_[handle.idx].sim.world_bounds;
1913 }
1914
1915 auto get_num_particles(emitter_handle handle) -> uint32_t
1916 {
1917 BX_ASSERT(is_valid(handle), "get_num_particles invalid handle");
1918 return emitters_[handle.idx].particles_.count;
1919 }
1920
1921 void set_emitter_sim_backend(emitter_handle handle, particle_sim_backend backend)
1922 {
1923 BX_ASSERT(is_valid(handle), "set_emitter_sim_backend invalid handle");
1924 emitters_[handle.idx].gpu_.backend_override = backend;
1925 emitters_[handle.idx].gpu_.has_backend_override = true;
1926 }
1927
1929 {
1930 BX_ASSERT(is_valid(handle), "get_emitter_sim_backend invalid handle");
1931 return emitters_[handle.idx].resolve_backend();
1932 }
1933
1934 static void write_instance_row(uint8_t* row, const emitter& em, uint32_t particle_idx)
1935 {
1936 const auto& p = em.particles_;
1937 float* pos = reinterpret_cast<float*>(row);
1938 pos[0] = p.position[particle_idx].x;
1939 pos[1] = p.position[particle_idx].y;
1940 pos[2] = p.position[particle_idx].z;
1941 pos[3] = em.cached_constants_.pivot.x;
1942 float* rot = reinterpret_cast<float*>(row + 16);
1943 rot[0] = p.rotation[particle_idx].x;
1944 rot[1] = p.rotation[particle_idx].y;
1945 rot[2] = p.rotation[particle_idx].z;
1946 rot[3] = p.rotation[particle_idx].w;
1947 float* scale3d = reinterpret_cast<float*>(row + 32);
1948 scale3d[0] = p.scale[particle_idx] * em.cached_constants_.particle_scale_3d.x;
1949 scale3d[1] = p.scale[particle_idx] * em.cached_constants_.particle_scale_3d.y;
1950 scale3d[2] = p.scale[particle_idx] * em.cached_constants_.particle_scale_3d.z;
1951 scale3d[3] = em.cached_constants_.pivot.y;
1952 float* uv = reinterpret_cast<float*>(row + 48);
1953 uv[0] = p.uv_offset[particle_idx].x;
1954 uv[1] = p.uv_offset[particle_idx].y;
1955 uv[2] = p.uv_scale[particle_idx].x;
1956 uv[3] = p.uv_scale[particle_idx].y;
1957 float* color = reinterpret_cast<float*>(row + 64);
1958 color[0] = p.color[particle_idx].value.r;
1959 color[1] = p.color[particle_idx].value.g;
1960 color[2] = p.color[particle_idx].value.b;
1961 color[3] = p.color[particle_idx].value.a;
1962 float* facing = reinterpret_cast<float*>(row + 80);
1963 facing[0] = static_cast<float>(static_cast<int>(em.cached_constants_.render_mode));
1964 facing[1] = 0.0f;
1965 facing[2] = 0.0f;
1966 facing[3] = 0.0f;
1967 }
1968
1969 auto build_prefixes(const emitter_handle* handles, uint32_t count) -> uint32_t
1970 {
1971 prefix_scratch_.resize(count + 1);
1972 prefix_scratch_[0] = 0;
1973 for(uint32_t i = 0; i < count; ++i)
1974 {
1975 uint32_t n = 0;
1976 if(is_valid(handles[i]))
1977 {
1978 n = emitters_[handles[i].idx].particles_.count;
1979 }
1981 }
1982 return prefix_scratch_[count];
1983 }
1984
1985 void build_sorted(const emitter_handle* handles, uint32_t count, const math::vec3& eye, uint32_t total)
1986 {
1987 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Build Sorted");
1988 batched_scratch_.resize(total);
1989 {
1990 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Sort Keys");
1991 const auto fill_emitter_keys = [&](uint32_t emitter_idx)
1992 {
1993 const uint32_t start = prefix_scratch_[emitter_idx];
1994 const uint32_t end = prefix_scratch_[emitter_idx + 1];
1995 if(start == end)
1996 {
1997 return;
1998 }
1999 const auto& em = emitters_[handles[emitter_idx].idx];
2000 for(uint32_t p = 0; p < end - start; ++p)
2001 {
2002 const math::vec3 delta = eye - em.particles_.position[p];
2003 batched_scratch_[start + p] = batched_particle{math::dot(delta, delta), emitter_idx, p};
2004 }
2005 };
2006 constexpr uint32_t k_parallel_particle_threshold = 2048;
2007 constexpr uint32_t k_min_rows_per_job = 512;
2008 constexpr uint32_t k_parallel_emitter_threshold = 16;
2009 constexpr uint32_t k_min_emitters_per_job = 16;
2010 if(total >= k_parallel_particle_threshold && count <= 4)
2011 {
2012 // Few large emitters: parallelize by particle rows across the batch.
2013 const uint32_t num_jobs = (total + k_min_rows_per_job - 1) / k_min_rows_per_job;
2014 std::for_each(poolstl::par,
2015 poolstl::iota_iter<uint32_t>(0),
2016 poolstl::iota_iter<uint32_t>(num_jobs),
2017 [&](uint32_t job)
2018 {
2019 const uint32_t global_begin = job * k_min_rows_per_job;
2020 const uint32_t global_end = math::min(global_begin + k_min_rows_per_job, total);
2021 for(uint32_t emitter_idx = 0; emitter_idx < count; ++emitter_idx)
2022 {
2023 const uint32_t emit_begin = prefix_scratch_[emitter_idx];
2024 const uint32_t emit_end = prefix_scratch_[emitter_idx + 1];
2025 const uint32_t range_begin = math::max(emit_begin, global_begin);
2026 const uint32_t range_end = math::min(emit_end, global_end);
2027 if(range_begin >= range_end)
2028 {
2029 continue;
2030 }
2031 const auto& em = emitters_[handles[emitter_idx].idx];
2032 for(uint32_t g = range_begin; g < range_end; ++g)
2033 {
2034 const uint32_t p = g - emit_begin;
2035 const math::vec3 delta = eye - em.particles_.position[p];
2036 batched_scratch_[g] = batched_particle{math::dot(delta, delta), emitter_idx, p};
2037 }
2038 }
2039 });
2040 }
2041 else if(count >= k_parallel_emitter_threshold)
2042 {
2043 const uint32_t num_jobs = (count + k_min_emitters_per_job - 1) / k_min_emitters_per_job;
2044 std::for_each(poolstl::par,
2045 poolstl::iota_iter<uint32_t>(0),
2046 poolstl::iota_iter<uint32_t>(num_jobs),
2047 [&](uint32_t job)
2048 {
2049 const uint32_t begin = job * k_min_emitters_per_job;
2050 const uint32_t end = math::min(begin + k_min_emitters_per_job, count);
2051 for(uint32_t emitter_idx = begin; emitter_idx < end; ++emitter_idx)
2052 {
2053 fill_emitter_keys(emitter_idx);
2054 }
2055 });
2056 }
2057 else
2058 {
2059 for(uint32_t emitter_idx = 0; emitter_idx < count; ++emitter_idx)
2060 {
2061 fill_emitter_keys(emitter_idx);
2062 }
2063 }
2064 }
2065 constexpr uint32_t k_radix_sort_threshold = 512;
2066 if(total < k_radix_sort_threshold)
2067 {
2068 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Sort");
2069 std::sort(batched_scratch_.begin(),
2070 batched_scratch_.end(),
2071 [](const batched_particle& a, const batched_particle& b)
2072 {
2073 return a.dist > b.dist;
2074 });
2075 }
2076 else
2077 {
2078 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Sort Radix");
2079 radix_sort_desc_distances(batched_scratch_, radix_scratch_);
2080 }
2081 }
2082
2083 void write_sorted_chunk(uint8_t* data, uint32_t start, uint32_t count, const emitter_handle* handles)
2084 {
2085 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Write Sorted Chunk");
2086 constexpr uint32_t k_parallel_write_threshold = 128;
2087 constexpr uint32_t k_min_rows_per_job = 128;
2088 const auto write_row = [&](uint32_t i)
2089 {
2090 const auto& key = batched_scratch_[start + i];
2091 const auto& em = emitters_[handles[key.emitter_idx].idx];
2092 write_instance_row(data + size_t(i) * k_instance_stride, em, key.particle_idx);
2093 };
2094 if(count < k_parallel_write_threshold)
2095 {
2096 for(uint32_t i = 0; i < count; ++i)
2097 {
2098 write_row(i);
2099 }
2100 return;
2101 }
2102 const uint32_t num_jobs = (count + k_min_rows_per_job - 1) / k_min_rows_per_job;
2103 std::for_each(poolstl::par,
2104 poolstl::iota_iter<uint32_t>(0),
2105 poolstl::iota_iter<uint32_t>(num_jobs),
2106 [&](uint32_t job)
2107 {
2108 const uint32_t begin = job * k_min_rows_per_job;
2109 const uint32_t end = math::min(begin + k_min_rows_per_job, count);
2110 for(uint32_t i = begin; i < end; ++i)
2111 {
2112 write_row(i);
2113 }
2114 });
2115 }
2116
2117 void write_direct_chunk(uint8_t* data,
2118 uint32_t global_start,
2119 uint32_t count,
2120 const emitter_handle* handles,
2121 uint32_t emitter_count)
2122 {
2123 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Write Direct Chunk");
2124 constexpr uint32_t k_parallel_emitter_threshold = 16;
2125 constexpr uint32_t k_min_emitters_per_job = 16;
2126 const auto write_emitter_range = [&](uint32_t emit_begin, uint32_t emit_end)
2127 {
2128 for(uint32_t emitter_idx = emit_begin; emitter_idx < emit_end; ++emitter_idx)
2129 {
2130 const uint32_t range_begin_global = prefix_scratch_[emitter_idx];
2131 const uint32_t range_end_global = prefix_scratch_[emitter_idx + 1];
2132 const uint32_t range_begin = math::max(range_begin_global, global_start);
2133 const uint32_t range_end = math::min(range_end_global, global_start + count);
2134 if(range_begin >= range_end)
2135 {
2136 continue;
2137 }
2138 const auto& em = emitters_[handles[emitter_idx].idx];
2139 for(uint32_t g = range_begin; g < range_end; ++g)
2140 {
2141 const uint32_t particle_idx = g - range_begin_global;
2142 const uint32_t out_idx = g - global_start;
2143 write_instance_row(data + size_t(out_idx) * k_instance_stride, em, particle_idx);
2144 }
2145 }
2146 };
2147 if(emitter_count < k_parallel_emitter_threshold)
2148 {
2149 write_emitter_range(0, emitter_count);
2150 return;
2151 }
2152 const uint32_t num_jobs = (emitter_count + k_min_emitters_per_job - 1) / k_min_emitters_per_job;
2153 std::for_each(poolstl::par,
2154 poolstl::iota_iter<uint32_t>(0),
2155 poolstl::iota_iter<uint32_t>(num_jobs),
2156 [&](uint32_t job)
2157 {
2158 const uint32_t begin = job * k_min_emitters_per_job;
2159 const uint32_t end = math::min(begin + k_min_emitters_per_job, emitter_count);
2160 write_emitter_range(begin, end);
2161 });
2162 }
2163
2164 auto render_cpu_batch(const emitter_handle* handles,
2165 uint32_t count,
2166 uint8_t view,
2167 bgfx::ProgramHandle program,
2168 const float* view_camera,
2169 const float* eye_pos_vec4,
2170 const math::vec3& eye,
2171 bgfx::TextureHandle texture,
2172 uint64_t blend_state,
2173 bool sort_by_depth) -> uint32_t
2174 {
2175 const uint32_t total = build_prefixes(handles, count);
2176 if(total == 0)
2177 {
2178 return 0;
2179 }
2180 if(sort_by_depth)
2181 {
2182 build_sorted(handles, count, eye, total);
2183 }
2184 bgfx::setVertexBuffer(0, quad_vbh_);
2185 bgfx::setIndexBuffer(quad_ibh_);
2186 bgfx::setState(0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW |
2187 blend_state);
2188 bgfx::setTexture(0, tex_color_, texture);
2189 bgfx::setUniform(view_camera_, view_camera);
2190 bgfx::setUniform(eye_pos_, eye_pos_vec4);
2191 const auto write_chunk = [&](uint8_t* data, uint32_t start, uint32_t chunk_count)
2192 {
2193 if(sort_by_depth)
2194 {
2195 write_sorted_chunk(data, start, chunk_count, handles);
2196 }
2197 else
2198 {
2199 write_direct_chunk(data, start, chunk_count, handles, count);
2200 }
2201 };
2202 const uint32_t avail_all = bgfx::getAvailInstanceDataBuffer(total, k_instance_stride);
2203 if(avail_all >= total)
2204 {
2205 bgfx::InstanceDataBuffer idb{};
2206 bgfx::allocInstanceDataBuffer(&idb, total, k_instance_stride);
2207 write_chunk(idb.data, 0, total);
2208 bgfx::setInstanceDataBuffer(&idb);
2209 bgfx::submit(view, program);
2210 return total;
2211 }
2212 uint32_t rendered = 0;
2213 uint32_t offset = 0;
2214 while(offset < total)
2215 {
2216 const uint32_t remaining = total - offset;
2217 uint32_t chunk = bgfx::getAvailInstanceDataBuffer(remaining, k_instance_stride);
2218 if(chunk == 0)
2219 {
2220 break;
2221 }
2222 chunk = math::min(chunk, remaining);
2223 bgfx::InstanceDataBuffer idb{};
2224 bgfx::allocInstanceDataBuffer(&idb, chunk, k_instance_stride);
2225 write_chunk(idb.data, offset, chunk);
2226 bgfx::setInstanceDataBuffer(&idb);
2227 bgfx::submit(view, program);
2228 rendered += chunk;
2229 offset += chunk;
2230 }
2231 return rendered;
2232 }
2233
2234 auto render_batch(const emitter_handle* handles,
2235 uint32_t count,
2236 uint8_t view,
2237 bgfx::ProgramHandle program,
2238 const float* mtx_view,
2239 const math::vec3& eye,
2240 bgfx::TextureHandle texture,
2241 uint64_t blend_state,
2242 bool sort_by_depth) -> uint32_t
2243 {
2244 if(count == 0 || !bgfx::isValid(texture))
2245 {
2246 return 0;
2247 }
2248 APP_SCOPE_PERF("Rendering/Particle Pass SOA/Render Batched Emitters");
2249 float view_camera[16];
2250 view_camera[0] = mtx_view[0];
2251 view_camera[1] = mtx_view[4];
2252 view_camera[2] = mtx_view[8];
2253 view_camera[3] = 0.0f;
2254 view_camera[4] = mtx_view[1];
2255 view_camera[5] = mtx_view[5];
2256 view_camera[6] = mtx_view[9];
2257 view_camera[7] = 0.0f;
2258 view_camera[8] = mtx_view[2];
2259 view_camera[9] = mtx_view[6];
2260 view_camera[10] = mtx_view[10];
2261 view_camera[11] = 0.0f;
2262 view_camera[12] = 0.0f;
2263 view_camera[13] = 0.0f;
2264 view_camera[14] = 0.0f;
2265 view_camera[15] = 1.0f;
2266 float eye_pos_vec4[4] = {eye.x, eye.y, eye.z, 0.0f};
2267 gpu_emitters_scratch_.clear();
2268 cpu_handles_scratch_.clear();
2269 for(uint32_t i = 0; i < count; ++i)
2270 {
2271 if(!is_valid(handles[i]))
2272 {
2273 continue;
2274 }
2275 emitter& em = emitters_[handles[i].idx];
2276 // GPU-backend emitters must never use the CPU instance path — their SoA
2277 // render caches are not maintained and produce garbage quads.
2278 if(em.wants_gpu_pack())
2279 {
2280 if(em.gpu_.pending_pack)
2281 {
2282 gpu_emitters_scratch_.push_back(handles[i]);
2283 }
2284 }
2285 else
2286 {
2287 cpu_handles_scratch_.push_back(handles[i]);
2288 }
2289 }
2290 uint32_t rendered = 0;
2291 if(!cpu_handles_scratch_.empty())
2292 {
2293 rendered += render_cpu_batch(cpu_handles_scratch_.data(),
2294 uint32_t(cpu_handles_scratch_.size()),
2295 view,
2296 program,
2297 view_camera,
2298 eye_pos_vec4,
2299 eye,
2300 texture,
2301 blend_state,
2302 sort_by_depth);
2303 }
2304 if(!gpu_emitters_scratch_.empty())
2305 {
2306 // Pack/spawn flush already ran in sync_gpu_simulation (before cull).
2307 // Draw only — re-packing here would double-advance GPU life.
2308 for(emitter_handle handle : gpu_emitters_scratch_)
2309 {
2310 emitter& em = emitters_[handle.idx];
2311 rendered += draw_gpu_emitter(em, view, program, view_camera, eye_pos_vec4, texture, blend_state);
2312 }
2313 }
2314 (void)sort_by_depth;
2315 return rendered;
2316 }
2317
2318 bx::AllocatorI* allocator_ = nullptr;
2319 bx::HandleAlloc* emitter_alloc_ = nullptr;
2320 std::vector<emitter> emitters_;
2321 std::vector<batched_particle> batched_scratch_;
2322 std::vector<batched_particle> radix_scratch_;
2323 std::vector<uint32_t> prefix_scratch_;
2324 std::vector<emitter_handle> gpu_emitters_scratch_;
2325 std::vector<emitter_handle> cpu_handles_scratch_;
2326 bgfx::VertexBufferHandle quad_vbh_ = BGFX_INVALID_HANDLE;
2327 bgfx::IndexBufferHandle quad_ibh_ = BGFX_INVALID_HANDLE;
2328 bgfx::UniformHandle tex_color_ = BGFX_INVALID_HANDLE;
2329 bgfx::UniformHandle view_camera_ = BGFX_INVALID_HANDLE;
2330 bgfx::UniformHandle eye_pos_ = BGFX_INVALID_HANDLE;
2331};
2332
2333particle_system_soa g_system;
2334
2335} // namespace
2336
2337void init(uint16_t max_emitters)
2338{
2339 g_system.init(max_emitters);
2340}
2341
2343{
2344 g_system.init_gpu(ctx);
2345}
2346
2348{
2349 g_system.shutdown();
2350}
2351
2353{
2354 g_default_sim_backend = backend;
2355}
2356
2358{
2359 return g_default_sim_backend;
2360}
2361
2363{
2364 g_system.set_emitter_sim_backend(handle, backend);
2365}
2366
2368{
2369 return g_system.get_emitter_sim_backend(handle);
2370}
2371
2373{
2374 return g_gpu_sim_available;
2375}
2376
2377auto create_emitter(emitter_shape shape, emitter_direction direction, uint32_t max_particles) -> emitter_handle
2378{
2379 return g_system.create_emitter(shape, direction, max_particles);
2380}
2381
2383{
2384 g_system.destroy_emitter(handle);
2385}
2386
2388{
2389 g_system.reset_emitter(handle);
2390}
2391
2393 float dt,
2394 const emitter_desc& desc,
2395 emitter_transform_state& transform,
2396 emitter_playback_desc& playback)
2397{
2398 g_system.update_emitter(handle, dt, desc, transform, playback);
2399}
2400
2402 const emitter_desc& desc,
2403 emitter_transform_state& transform)
2404{
2405 g_system.update_emitter_bounds_only(handle, desc, transform);
2406}
2407
2409{
2410 g_system.sync_gpu_simulation();
2411}
2412
2414{
2415 return g_system.has_updated(handle);
2416}
2417
2419{
2420 g_system.get_aabb(handle, out_aabb);
2421}
2422
2424{
2425 return g_system.get_num_particles(handle);
2426}
2427
2429 uint32_t count,
2430 uint8_t view,
2431 bgfx::ProgramHandle program,
2432 const float* mtx_view,
2433 const math::vec3& eye,
2434 bgfx::TextureHandle texture,
2435 uint64_t blend_state,
2436 bool sort_by_depth) -> uint32_t
2437{
2438 return g_system.render_batch(handles, count, view, program, mtx_view, eye, texture, blend_state, sort_by_depth);
2439}
2440
2441} // namespace ps_soa
2442} // namespace unravel
entt::handle b
entt::handle a
auto sample(float progress) const -> T
Definition gradient.hpp:115
float y
float x
float z
math::vec3 position
Definition defaults.cpp:52
uint16_t view
std::vector< render_pass_node_item > items
uint16_t index
#define APPLOG_WARNING(...)
Definition logging.h:19
#define APPLOG_INFO(...)
Definition logging.h:18
imported_texture desc
encoder * begin()
Definition graphics.cpp:422
void reset(uint32_t _width, uint32_t _height, uint32_t _flags)
Definition graphics.cpp:417
void destroy(index_buffer_handle _handle)
Definition graphics.cpp:505
void update(dynamic_index_buffer_handle _handle, uint32_t _startIndex, const memory_view *_mem)
Definition graphics.cpp:535
bgfx::Transform transform
Definition graphics.h:42
void end(encoder *_encoder)
Definition graphics.cpp:427
auto look_rotation(const glm::vec3 &forward, const glm::vec3 &upwards) -> glm::quat
void run(bool use_random_inputs)
Definition tests.cpp:1093
constexpr uint32_t k_gpu_sim_vec4s_per_particle
void update_emitter(emitter_handle handle, float dt, const emitter_desc &desc, emitter_transform_state &transform, emitter_playback_desc &playback)
Advance simulation for one emitter.
void update_emitter_bounds_only(emitter_handle handle, const emitter_desc &desc, emitter_transform_state &transform)
Refresh transform-driven world bounds without advancing sim (renderer-based freeze).
constexpr uint32_t k_gpu_cs_threads
void destroy_emitter(emitter_handle handle)
Destroy an emitter and free its particle storage.
emitter_direction
Initial emission direction. Ordinals match legacy EmitterDirection.
void init(uint16_t max_emitters)
Initialize the soa particle system.
particle_sim_backend
Selects CPU vs resident-GPU simulation backend.
constexpr auto has_feature(emitter_feature mask, emitter_feature bit) -> bool
void shutdown()
Shutdown and free all emitters / GPU resources.
emitter_shape
Emission volume shape. Ordinals match legacy EmitterShape for content compatibility.
auto create_emitter(emitter_shape shape, emitter_direction direction, uint32_t max_particles) -> emitter_handle
Create an emitter with shape/direction and capacity.
void get_aabb(emitter_handle handle, math::bbox &out_aabb)
World-space AABB used for culling.
auto get_emitter_sim_backend(emitter_handle handle) -> particle_sim_backend
Effective backend for an emitter (after availability checks).
auto is_valid(emitter_handle handle) -> bool
void set_emitter_sim_backend(emitter_handle handle, particle_sim_backend backend)
Override backend for one emitter.
auto get_num_particles(emitter_handle handle) -> uint32_t
Live particle count.
auto render_emitter_batch(const emitter_handle *handles, uint32_t count, uint8_t view, bgfx::ProgramHandle program, const float *mtx_view, const math::vec3 &eye, bgfx::TextureHandle texture, uint64_t blend_state, bool sort_by_depth) -> uint32_t
Submit a homogeneous material batch (same texture / texture_mode / blend).
auto is_gpu_sim_available() -> bool
True when compute pack program loaded successfully.
constexpr uint32_t k_gpu_lut_size
void sync_gpu_simulation()
Flush staged GPU spawns and advance resident GPU sim for awake emitters.
auto gpu_feature_mask(emitter_feature features) -> uint32_t
void init_gpu(rtti::context &ctx)
Load compute pack program and enable GPU backend when available.
void reset_emitter(emitter_handle handle)
Clear particles and reset sim bookkeeping.
emitter_feature
Feature bits baked from authoring desc for specialized update/render paths.
auto get_default_sim_backend() -> particle_sim_backend
Current default sim backend.
void set_default_sim_backend(particle_sim_backend backend)
Set default sim backend for new / unset emitters (cpu remains fallback).
auto has_updated(emitter_handle handle) -> bool
True after the first successful update.
std::vector< float > cached_speed
float sim_dt
bx::EaseFn cached_ease_pos
particle_soa particles_
bgfx::UniformHandle eye_pos_
std::vector< math::vec4 > ease_lut
bgfx::DynamicVertexBufferHandle ease_lut_vb
std::vector< math::vec2 > uv_offset
uint32_t emitter_idx
std::vector< math::color > color
std::vector< math::vec4 > color_lut
bool cached_need_ease
emitter_shape shape_
std::vector< uint32_t > prefix_scratch_
std::vector< float > life
math::bbox trail_bounds
Grow-only world-space trail AABB (union of particle trajectory hulls).
std::vector< emitter_handle > gpu_emitters_scratch_
bool cached_need_color_speed
std::vector< math::vec2 > uv_scale
uint32_t capacity
bgfx::DynamicVertexBufferHandle spawn_vb
emitter_direction direction_
std::vector< float > texsheet_seed
std::vector< emitter_handle > cpu_handles_scratch_
bx::HandleAlloc * emitter_alloc_
uint32_t spawn_upload_capacity
bgfx::DynamicVertexBufferHandle instance_vb
std::vector< batched_particle > radix_scratch_
particle_sim_backend backend_override
emitter_sim_constants constants
std::vector< gpu_sim_particle > spawn_particles
std::vector< uint32_t > spawn_slots
static bgfx::VertexLayout ms_layout
uint32_t high_water
Exclusive end of slot range that may contain live sim data (for compact dispatch).
uint32_t count
bgfx::UniformHandle tex_color_
bgfx::DynamicVertexBufferHandle color_speed_lut_vb
bool trail_bounds_valid
emitter_feature cached_features
std::vector< math::vec3 > end1
bgfx::DynamicVertexBufferHandle sim_vb
bgfx::IndexBufferHandle quad_ibh_
bx::RngMwc rng_
emitter_gpu_resources gpu_
emitter_sim_constants cached_constants_
bgfx::DynamicIndexBufferHandle counter_ib
bgfx::IndirectBufferHandle indirect_buf
std::vector< uint32_t > active_slots
std::vector< math::vec3 > end0
bgfx::DynamicIndexBufferHandle spawn_slots_ib
bool pending_pack
std::vector< float > scale_end
bool luts_valid
bgfx::UniformHandle view_camera_
bx::AllocatorI * allocator_
bgfx::VertexBufferHandle quad_vbh_
bool has_backend_override
std::vector< float > scale
std::vector< math::quat > rotation
std::vector< float > lifespan
bool luts_gpu_dirty
std::vector< math::vec4 > color_speed_lut
uint32_t particle_idx
bgfx::DynamicVertexBufferHandle color_lut_vb
std::vector< math::vec3 > start
std::vector< emitter > emitters_
uint32_t gpu_capacity
std::vector< float > scale_start
std::vector< uint32_t > free_list
std::vector< batched_particle > batched_scratch_
emitter_sim_state sim
#define APP_SCOPE_PERF(name_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Definition profiler.h:675
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
bbox & add_point(const vec3 &point)
Grows the bounding box based on the point passed.
Definition bbox.cpp:924
void reset()
Resets the bounding box values.
Definition bbox.cpp:28
bool is_populated() const
Checks if the bounding box is populated.
Definition bbox.cpp:36
vec4 value
Definition color.h:80
auto get_cached() -> T &
Definition context.hpp:49
Full authoring description for an emitter (no transforms / runtime).
Playback gates (playing / paused).
Per-frame transform inputs. Authoring desc never owns these.
gfx::uniform_handle handle
Definition uniform.cpp:9