4#include <bx/allocator.h>
6#include <bx/handlealloc.h>
22#define POOLSTL_STD_SUPPLEMENT 1
23#include <poolstl/poolstl.hpp>
32constexpr float k_min_particle_lifespan = 1.0e-4f;
33constexpr float k_emit_dir_zero_len_sq = 1.0e-12f;
35constexpr uint16_t k_instance_stride = 96;
39constexpr uint32_t k_parallel_particle_threshold = 512;
40constexpr uint32_t k_min_rows_per_job = 256;
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;
64auto next_pow2_u32(uint32_t value) -> uint32_t
70 uint32_t
v = value - 1u;
79void ensure_gpu_layouts()
81 if(g_gpu_layouts_ready)
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)
94 g_gpu_layouts_ready =
true;
97struct emitter_gpu_resources
112 bgfx::DynamicVertexBufferHandle
sim_vb = BGFX_INVALID_HANDLE;
113 bgfx::DynamicVertexBufferHandle
instance_vb = BGFX_INVALID_HANDLE;
114 bgfx::DynamicIndexBufferHandle
counter_ib = BGFX_INVALID_HANDLE;
118 bgfx::DynamicVertexBufferHandle
ease_lut_vb = BGFX_INVALID_HANDLE;
119 bgfx::DynamicVertexBufferHandle
spawn_vb = BGFX_INVALID_HANDLE;
134 void clear_trail_bounds()
140 void destroy_buffers()
142 if(bgfx::isValid(sim_vb))
144 bgfx::destroy(sim_vb);
145 sim_vb = BGFX_INVALID_HANDLE;
147 if(bgfx::isValid(instance_vb))
149 bgfx::destroy(instance_vb);
152 if(bgfx::isValid(counter_ib))
154 bgfx::destroy(counter_ib);
157 if(bgfx::isValid(indirect_buf))
159 bgfx::destroy(indirect_buf);
162 if(bgfx::isValid(color_lut_vb))
164 bgfx::destroy(color_lut_vb);
167 if(bgfx::isValid(color_speed_lut_vb))
169 bgfx::destroy(color_speed_lut_vb);
172 if(bgfx::isValid(ease_lut_vb))
174 bgfx::destroy(ease_lut_vb);
177 if(bgfx::isValid(spawn_vb))
179 bgfx::destroy(spawn_vb);
182 if(bgfx::isValid(spawn_slots_ib))
184 bgfx::destroy(spawn_slots_ib);
196 clear_trail_bounds();
199 void ensure_spawn_upload_capacity(uint32_t spawn_count)
205 if(spawn_upload_capacity >= spawn_count && bgfx::isValid(spawn_vb) && bgfx::isValid(spawn_slots_ib))
209 if(bgfx::isValid(spawn_vb))
211 bgfx::destroy(spawn_vb);
214 if(bgfx::isValid(spawn_slots_ib))
216 bgfx::destroy(spawn_slots_ib);
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;
227 spawn_slots_ib = bgfx::createDynamicIndexBuffer(spawn_upload_capacity, slot_flags);
230 void reset_freelist(uint32_t max_particles)
233 for(uint32_t i = 0;
i < max_particles; ++
i)
240 auto ensure_capacity(uint32_t max_particles) ->
bool
242 ensure_gpu_layouts();
243 if(gpu_capacity >= max_particles && bgfx::isValid(sim_vb) && bgfx::isValid(counter_ib) &&
244 bgfx::isValid(indirect_buf))
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;
264 instance_vb = bgfx::createDynamicVertexBuffer(max_particles, g_gpu_instance_layout, instance_flags);
265 counter_ib = bgfx::createDynamicIndexBuffer(1, counter_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)));
280auto thread_local_rng() -> bx::RngMwc&
282 thread_local bx::RngMwc rng;
286auto frand01(bx::RngMwc& rng) ->
float
288 return bx::frnd(&rng);
291auto frand_range(bx::RngMwc& rng,
float lo,
float hi) ->
float
293 return math::mix(lo, hi, frand01(rng));
296auto random_unit_vector(bx::RngMwc& rng) -> math::vec3
304 x = frand_range(rng, -1.0f, 1.0f);
305 y = frand_range(rng, -1.0f, 1.0f);
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);
312auto random_in_unit_ball(bx::RngMwc& rng) -> math::vec3
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);
322auto random_in_unit_disk(bx::RngMwc& rng) -> math::vec2
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);
332auto random_on_unit_circle(bx::RngMwc& rng) -> math::vec2
334 const float a = frand_range(rng, 0.0f, 6.28318530718f);
335 return math::vec2(std::cos(
a), std::sin(
a));
338struct particle_vertex
345 .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
346 .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
353bgfx::VertexLayout particle_vertex::ms_layout;
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},
362static const uint16_t s_quad_indices[6] = {0, 1, 2, 2, 3, 0};
384 void resize(uint32_t max_particles)
388 start.resize(max_particles);
389 end0.resize(max_particles);
390 end1.resize(max_particles);
393 life.resize(max_particles);
397 color.resize(max_particles);
398 scale.resize(max_particles);
411 void move_sim_particle(uint32_t dst, uint32_t src)
424void expand_aabb_sphere(
math::bbox& aabb,
const math::vec3& pos,
float radius)
426 const math::vec3 pad(radius);
431void bake_constants(
const emitter_desc&
desc,
432 const emitter_transform_state& transform,
433 emitter_sim_constants& out_constants)
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;
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;
461float calculate_particle_speed(
const math::vec3&
start,
462 const math::vec3&
end0,
463 const math::vec3&
end1,
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);
475void update_particle_basic(particle_soa& particles,
477 const emitter_desc&
desc,
480 const float life = particles.life[
index];
486 particles.color[
index] = sampled_color;
487 particles.scale[
index] =
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);
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);
498void update_particle_full(particle_soa& particles,
500 const emitter_desc&
desc,
503 const float life = particles.life[
index];
508 float particle_speed = 0.0f;
511 particle_speed = calculate_particle_speed(particles.start[
index],
512 particles.end0[
index],
513 particles.end1[
index],
514 particles.lifespan[
index],
516 particles.cached_speed[
index] = particle_speed;
521 const float speed_factor = math::clamp(
525 const math::color speed_color =
desc.appearance.color_by_speed_gradient.sample(speed_factor);
532 particles.color[
index] = sampled_color;
536 const float speed_factor = math::clamp(
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);
549 particles.position[
index] = math::vec3(world_pos4.x, world_pos4.y, world_pos4.z);
553 particles.position[
index] = local_pos;
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)
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)
567 up_ref = math::vec3(1.0f, 0.0f, 0.0f);
573 particles.rotation[
index] = math::identity<math::quat>();
580 const uint32_t total_frames =
585 anim_progress += particles.texsheet_seed[
index];
587 anim_progress = math::fmod(anim_progress, 1.0f);
588 const uint32_t current_frame = uint32_t(anim_progress *
float(total_frames)) % total_frames;
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);
596 particles.uv_offset[
index] = math::vec2(0.0f, 0.0f);
597 particles.uv_scale[
index] = math::vec2(1.0f, 1.0f);
606 return static_cast<uint32_t
>(heavy) != 0
u;
609void update_particle_properties(particle_soa& particles,
611 const emitter_desc&
desc,
622void update_particles_range(particle_soa& particles,
625 const emitter_desc&
desc,
630 for(uint32_t i = begin;
i <
end; ++
i)
636 for(uint32_t i = begin;
i <
end; ++
i)
655 gpu_.destroy_buffers();
669 gpu_.pending_pack =
false;
670 gpu_.luts_valid =
false;
671 gpu_.active_slots.clear();
673 gpu_.spawn_particles.clear();
674 gpu_.spawn_slots.clear();
675 gpu_.clear_trail_bounds();
676 if(
gpu_.gpu_capacity > 0)
678 gpu_.reset_freelist(
gpu_.gpu_capacity);
679 if(bgfx::isValid(
gpu_.sim_vb))
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,
685 bgfx::copy(zeros.data(), uint32_t(
sizeof(gpu_sim_particle) *
gpu_.gpu_capacity)));
692 if(!g_gpu_sim_available)
696 if(
gpu_.has_backend_override)
698 return gpu_.backend_override;
700 return g_default_sim_backend;
703 auto wants_gpu_pack() const ->
bool
716 out_lut[
i] = math::vec4(
c.value.r,
c.value.g,
c.value.b,
c.value.a);
720 void ensure_gpu_luts(
const emitter_desc&
desc,
const emitter_sim_constants&
constants)
725 gpu_.cached_need_color_speed == need_color_speed &&
gpu_.cached_need_ease == need_ease &&
730 fill_gradient_lut(
desc.appearance.color_gradient,
gpu_.color_lut);
733 fill_gradient_lut(
desc.appearance.color_by_speed_gradient,
gpu_.color_speed_lut);
744 gpu_.ease_lut[
i] = math::vec4(eased, 0.0f, 0.0f, 0.0f);
747 gpu_.cached_need_color_speed = need_color_speed;
748 gpu_.cached_need_ease = need_ease;
750 gpu_.luts_valid =
true;
751 gpu_.luts_gpu_dirty =
true;
754 auto compute_gpu_particle_radius(
const emitter_desc&
desc,
const emitter_sim_constants&
constants)
const ->
float
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;
763 size_speed_mul = math::max(
constants.size_by_speed_range.min,
constants.size_by_speed_range.max);
765 const float base_extent = math::max(
constants.particle_scale_3d.x,
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;
775 void accumulate_conservative_gpu_bounds(
math::bbox& aabb,
776 const emitter_desc&
desc,
778 const math::vec3& emitter_pos)
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);
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)
792 if(!
gpu_.trail_bounds_valid)
794 gpu_.trail_bounds.reset();
795 gpu_.trail_bounds_valid =
true;
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);
802 void rebuild_gpu_trail_bounds_from_live(
float particle_radius)
804 gpu_.clear_trail_bounds();
805 for(uint32_t slot :
gpu_.active_slots)
807 expand_gpu_trail_bounds_for_particle(
particles_.start[slot],
814 void rebuild_gpu_slot_lists()
816 gpu_.free_list.clear();
817 gpu_.active_slots.clear();
825 gpu_.free_list.push_back(i);
828 gpu_.active_slots.push_back(i);
835 void reclaim_gpu_slots(
float sim_dt)
837 if(
gpu_.active_slots.empty())
841 gpu_.clear_trail_bounds();
847 for(uint32_t i = 0;
i <
gpu_.active_slots.size(); ++
i)
849 const uint32_t slot =
gpu_.active_slots[
i];
855 gpu_.free_list.push_back(slot);
858 gpu_.active_slots[write++] = slot;
861 gpu_.active_slots.resize(write);
866 gpu_.clear_trail_bounds();
870 rebuild_gpu_slot_lists();
874 void fill_gpu_sim_particle(uint32_t slot, gpu_sim_particle& dst)
const
887 dst.scale_start =
particles_.scale_start[slot];
889 dst.texsheet_seed =
particles_.texsheet_seed[slot];
892 const math::quat rot = math::identity<math::quat>();
899 void stage_gpu_spawn(uint32_t slot)
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);
909 void upload_gpu_sim_slot(uint32_t slot)
911 if(!bgfx::isValid(
gpu_.sim_vb) || slot >=
gpu_.gpu_capacity)
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)));
923 void flush_gpu_spawn_uploads_cpu()
925 const uint32_t spawn_count = uint32_t(
gpu_.spawn_slots.size());
926 if(spawn_count == 0 || !bgfx::isValid(
gpu_.sim_vb))
928 gpu_.spawn_particles.clear();
929 gpu_.spawn_slots.clear();
932 std::vector<uint32_t> order(spawn_count);
933 for(uint32_t i = 0;
i < spawn_count; ++
i)
937 std::sort(order.begin(),
939 [&](uint32_t
a, uint32_t
b)
941 return gpu_.spawn_slots[a] < gpu_.spawn_slots[b];
943 std::vector<gpu_sim_particle>
run;
945 uint32_t run_start_slot = 0;
946 auto flush_run = [&]()
952 bgfx::update(
gpu_.sim_vb,
954 bgfx::copy(
run.data(), uint32_t(
sizeof(gpu_sim_particle) *
run.size())));
957 for(uint32_t oi = 0; oi < spawn_count; ++oi)
959 const uint32_t src = order[oi];
960 const uint32_t slot =
gpu_.spawn_slots[src];
963 run_start_slot = slot;
964 run.push_back(
gpu_.spawn_particles[src]);
967 const uint32_t expected = run_start_slot + uint32_t(
run.size());
970 run.push_back(
gpu_.spawn_particles[src]);
974 run_start_slot = slot;
975 run.push_back(
gpu_.spawn_particles[src]);
978 gpu_.spawn_particles.clear();
979 gpu_.spawn_slots.clear();
982 void resync_gpu_slots_from_cpu()
990 rebuild_gpu_slot_lists();
991 for(uint32_t slot :
gpu_.active_slots)
993 upload_gpu_sim_slot(slot);
997 void prepare_gpu_resident(
const emitter_desc&
desc,
1000 const math::vec3& emitter_pos,
1007 accumulate_conservative_gpu_bounds(aabb,
desc,
constants, emitter_pos);
1013 gpu_.clear_trail_bounds();
1017 rebuild_gpu_trail_bounds_from_live(compute_gpu_particle_radius(
desc,
constants));
1019 if(
gpu_.trail_bounds_valid)
1026 if(!
gpu_.pending_pack)
1028 gpu_.spawn_particles.clear();
1029 gpu_.spawn_slots.clear();
1034 const emitter_desc&
desc,
1035 emitter_transform_state& transform,
1036 emitter_playback_desc& playback)
1038 if(
sim.first_update)
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)
1048 sim.total_particles_spawned = 0;
1049 if(
sim.playing && !was_playing)
1051 sim.start_delay_elapsed = 0.0f;
1059 else if(
sim.playing)
1061 sim.start_delay_elapsed += dt;
1063 const math::vec3 current_pos =
transform.current.get_position();
1064 if(!playback.paused)
1066 sim.push_temporal_sample(current_pos,
sim_dt);
1070 playback.playing =
false;
1071 sim.playing =
false;
1072 sim.total_particles_spawned = 0;
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();
1086 resync_gpu_slots_from_cpu();
1095 if(
desc.emission.emission_lifetime > 0.0f && playback.playing)
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))
1108 reclaim_gpu_slots(
sim_dt);
1113 accumulate_world_bounds(aabb,
constants);
1115 if(
sim.first_update)
1117 sim.first_update =
false;
1119 sim.world_bounds = aabb;
1123 void update_bounds_only(
const emitter_desc&
desc, emitter_transform_state& transform)
1127 const math::vec3 current_pos =
transform.current.get_position();
1132 aabb.
add_point(current_pos - math::vec3(0.5f));
1133 aabb.
add_point(current_pos + math::vec3(0.5f));
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;
1145 void compact_alive(
float sim_dt)
1154 for(uint32_t i = 0;
i < old_count; ++
i)
1181 const float base_extent = math::max(
constants.particle_scale_3d.x,
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;
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);
1194 void spawn(
const emitter_desc&
desc,
1196 const emitter_transform_state& transform,
1198 bool skip_cpu_properties)
1200 if(
desc.emission.particles_per_second <= 0.0f)
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 =
1210 const uint32_t actual_emit_count = math::min(num_to_emit, max_emittable);
1211 if(actual_emit_count == 0)
1215 if(skip_cpu_properties &&
gpu_.ensure_capacity(
particles_.capacity))
1217 resync_gpu_slots_from_cpu();
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;
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),
1234 lifetime_multiplier =
desc.motion.lifetime_by_emitter_speed_gradient.sample(speed_factor);
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;
1242 gravity_vector.y *= system_scale.y;
1243 force_vector *= system_scale;
1245 const float velocity_damping_factor = (1.0f -
desc.motion.velocity_damping);
1246 const bool expand_world_trail =
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)
1253 prev_pos =
sim.temporal_positions[
sim.temporal_count - 2];
1255 const uint32_t base_index = skip_cpu_properties ? 0
u :
particles_.count;
1256 if(!skip_cpu_properties)
1260 sim.total_particles_spawned += actual_emit_count;
1261 const auto emit_one = [&](uint32_t ii, bx::RngMwc& rng)
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)
1269 gpu_.free_list.pop_back();
1271 const math::vec3
up(0.0f, 1.0f, 0.0f);
1279 pos = random_unit_vector(rng);
1283 math::vec3 sphere_pos = random_unit_vector(rng);
1284 if(sphere_pos.y < 0.0f)
1286 sphere_pos.y = -sphere_pos.y;
1293 const math::vec2 circle_pos = random_on_unit_circle(rng);
1294 pos = math::vec3(circle_pos.x, 0.0f, circle_pos.y);
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);
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;
1315 const int edge_index = int(frand01(rng) * 4.0f);
1316 const float t = frand_range(rng, -1.0f, 1.0f);
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;
1334 pos = random_in_unit_ball(rng);
1338 math::vec3 sphere_pos = random_in_unit_ball(rng);
1339 if(math::dot(sphere_pos,
up) < 0.0f)
1341 sphere_pos = -sphere_pos;
1348 const math::vec2 circle_pos = random_in_unit_disk(rng);
1349 pos = math::vec3(circle_pos.x, 0.0f, circle_pos.y);
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));
1358 pos = math::vec3(frand_range(rng, -1.0f, 1.0f), 0.0f, frand_range(rng, -1.0f, 1.0f));
1362 pos = (
desc.emission.shape_position + pos) * emission_shape_scale;
1372 const float len_sq = math::dot(pos, pos);
1373 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) :
up;
1378 const float len_sq = math::dot(pos, pos);
1379 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) :
up;
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;
1389 std::swap(
start, end);
1394 const math::vec3 interpolated_emitter_pos = math::mix(prev_pos, effective_position, emission_phase);
1405 if(
desc.motion.velocity_damping > 0.0f)
1411 if(expand_world_trail)
1416 gpu_trail_particle_radius);
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));
1423 if(!skip_cpu_properties)
1429 stage_gpu_spawn(
index);
1433 if(skip_cpu_properties)
1435 gpu_.spawn_particles.reserve(
gpu_.spawn_particles.size() + actual_emit_count);
1436 gpu_.spawn_slots.reserve(
gpu_.spawn_slots.size() + actual_emit_count);
1438 for(uint32_t ii = 0; ii < actual_emit_count; ++ii)
1442 if(skip_cpu_properties)
1457struct batched_particle
1464auto float_to_sortable_uint(
float value) -> uint32_t
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;
1473void radix_sort_desc_distances(std::vector<batched_particle>&
items, std::vector<batched_particle>& scratch)
1475 const uint32_t
n =
static_cast<uint32_t
>(
items.size());
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)
1487 const uint32_t shift = pass * k_bits;
1488 std::memset(counts, 0,
sizeof(counts));
1489 for(uint32_t i = 0;
i <
n; ++
i)
1491 const uint32_t
key = ~float_to_sortable_uint(
items[i].
dist);
1492 ++counts[(
key >> shift) & (k_bins - 1u)];
1495 for(uint32_t
b = 0;
b < k_bins; ++
b)
1497 const uint32_t
c = counts[
b];
1501 for(uint32_t i = 0;
i <
n; ++
i)
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];
1507 items.swap(scratch);
1511struct particle_system_soa
1513 void init(uint16_t max_emitters)
1515 static bx::DefaultAllocator allocator;
1517 emitter_alloc_ = bx::createHandleAlloc(allocator_, 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);
1530 for(
auto& em : emitters_)
1532 em.gpu_.destroy_buffers();
1534 if(bgfx::isValid(g_u_pack0))
1536 bgfx::destroy(g_u_pack0);
1537 g_u_pack0 = BGFX_INVALID_HANDLE;
1539 if(bgfx::isValid(g_u_pack1))
1541 bgfx::destroy(g_u_pack1);
1542 g_u_pack1 = BGFX_INVALID_HANDLE;
1544 if(bgfx::isValid(g_u_pack2))
1546 bgfx::destroy(g_u_pack2);
1547 g_u_pack2 = BGFX_INVALID_HANDLE;
1549 if(bgfx::isValid(g_u_pack3))
1551 bgfx::destroy(g_u_pack3);
1552 g_u_pack3 = BGFX_INVALID_HANDLE;
1554 if(bgfx::isValid(g_u_pack4))
1556 bgfx::destroy(g_u_pack4);
1557 g_u_pack4 = BGFX_INVALID_HANDLE;
1559 if(bgfx::isValid(g_u_pack5))
1561 bgfx::destroy(g_u_pack5);
1562 g_u_pack5 = BGFX_INVALID_HANDLE;
1564 if(bgfx::isValid(g_u_local_to_world))
1566 bgfx::destroy(g_u_local_to_world);
1567 g_u_local_to_world = BGFX_INVALID_HANDLE;
1569 if(bgfx::isValid(g_u_args0))
1571 bgfx::destroy(g_u_args0);
1572 g_u_args0 = BGFX_INVALID_HANDLE;
1574 if(bgfx::isValid(g_u_sort0))
1576 bgfx::destroy(g_u_sort0);
1577 g_u_sort0 = BGFX_INVALID_HANDLE;
1579 if(bgfx::isValid(g_u_sort1))
1581 bgfx::destroy(g_u_sort1);
1582 g_u_sort1 = BGFX_INVALID_HANDLE;
1584 if(bgfx::isValid(g_u_spawn0))
1586 bgfx::destroy(g_u_spawn0);
1587 g_u_spawn0 = BGFX_INVALID_HANDLE;
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;
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_);
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");
1614 APPLOG_WARNING(
"Particles: GPU resident compact shader missing; CPU backend only");
1615 g_gpu_sim_available =
false;
1618 g_compact_pack_program = std::make_shared<gpu_program>(cs_compact);
1619 if(!g_compact_pack_program || !g_compact_pack_program->is_valid())
1621 APPLOG_WARNING(
"Particles: GPU resident compact program invalid; CPU backend only");
1622 g_compact_pack_program.reset();
1623 g_gpu_sim_available =
false;
1628 g_spawn_scatter_program = std::make_shared<gpu_program>(cs_spawn);
1629 if(!g_spawn_scatter_program || !g_spawn_scatter_program->is_valid())
1631 APPLOG_WARNING(
"Particles: GPU spawn scatter program invalid; using CPU coalesce uploads");
1632 g_spawn_scatter_program.reset();
1637 g_indirect_args_program = std::make_shared<gpu_program>(cs_args);
1638 if(!g_indirect_args_program || !g_indirect_args_program->is_valid())
1640 g_indirect_args_program.reset();
1645 g_sort_program = std::make_shared<gpu_program>(cs_sort);
1646 if(!g_sort_program || !g_sort_program->is_valid())
1648 APPLOG_WARNING(
"Particles: GPU sort program invalid; Normal blend will be unsorted on GPU path");
1649 g_sort_program.reset();
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)");
1668 void flush_emitter_gpu_spawns(emitter& em, bgfx::ViewId
view)
1670 const uint32_t spawn_count = uint32_t(em.gpu_.spawn_slots.size());
1671 if(spawn_count == 0)
1678 if(g_spawn_scatter_program && g_spawn_scatter_program->begin() && bgfx::isValid(em.gpu_.sim_vb))
1680 em.gpu_.ensure_spawn_upload_capacity(spawn_count);
1681 if(bgfx::isValid(em.gpu_.spawn_vb) && bgfx::isValid(em.gpu_.spawn_slots_ib))
1683 bgfx::update(em.gpu_.spawn_vb,
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,
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);
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();
1702 g_spawn_scatter_program->end();
1704 em.flush_gpu_spawn_uploads_cpu();
1707 auto dispatch_gpu_resident(emitter& em,
bool sort_by_depth,
const math::vec3& eye, bgfx::ViewId pack_view) ->
bool
1709 if(!em.gpu_.pending_pack || em.particles_.count == 0 || !g_compact_pack_program)
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))
1719 flush_emitter_gpu_spawns(em, pack_view);
1720 if(em.gpu_.luts_gpu_dirty)
1722 bgfx::update(em.gpu_.color_lut_vb,
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,
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,
1730 bgfx::copy(em.gpu_.ease_lut.data(), uint32_t(
sizeof(math::vec4) *
k_gpu_lut_size)));
1731 em.gpu_.luts_gpu_dirty =
false;
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,
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,
1755 if(!g_compact_pack_program->begin())
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]);
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;
1780 auto draw_gpu_emitter(emitter& em,
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
1789 const uint32_t draw_count = em.particles_.count;
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 |
1798 bgfx::setTexture(0, tex_color_, texture);
1799 bgfx::setUniform(view_camera_, view_camera);
1800 bgfx::setUniform(eye_pos_, eye_pos_vec4);
1804 bgfx::setInstanceDataBuffer(em.gpu_.instance_vb, 0, draw_count);
1805 bgfx::submit(
view, program);
1834 const emitter_desc&
desc,
1835 emitter_transform_state& transform,
1836 emitter_playback_desc& playback)
1840 bake_constants(
desc, transform, emitters_[
handle.idx].cached_constants_);
1844 const emitter_desc&
desc,
1845 emitter_transform_state& transform)
1847 BX_ASSERT(
is_valid(
handle),
"update_emitter_bounds_only invalid handle");
1849 bake_constants(
desc, transform, emitters_[
handle.idx].cached_constants_);
1854 if(!g_gpu_sim_available || !emitter_alloc_)
1859 if(num_handles == 0)
1863 bool any_work =
false;
1865 for(uint16_t i = 0;
i < num_handles; ++
i)
1868 if(!em.wants_gpu_pack())
1872 if(em.gpu_.pending_pack || !em.gpu_.spawn_slots.empty())
1887 const math::vec3 eye(0.0f);
1888 for(uint16_t i = 0;
i < num_handles; ++
i)
1891 if(!em.wants_gpu_pack())
1895 if(!em.gpu_.pending_pack && em.gpu_.spawn_slots.empty())
1899 dispatch_gpu_resident(em,
false, eye, pass.id);
1923 BX_ASSERT(
is_valid(
handle),
"set_emitter_sim_backend invalid handle");
1930 BX_ASSERT(
is_valid(
handle),
"get_emitter_sim_backend invalid handle");
1934 static void write_instance_row(uint8_t* row,
const emitter& em, uint32_t
particle_idx)
1936 const auto&
p = em.particles_;
1937 float* pos =
reinterpret_cast<float*
>(row);
1941 pos[3] = em.cached_constants_.pivot.x;
1942 float* rot =
reinterpret_cast<float*
>(row + 16);
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);
1957 float*
color =
reinterpret_cast<float*
>(row + 64);
1962 float* facing =
reinterpret_cast<float*
>(row + 80);
1963 facing[0] =
static_cast<float>(
static_cast<int>(em.cached_constants_.render_mode));
1969 auto build_prefixes(
const emitter_handle* handles, uint32_t
count) -> uint32_t
1973 for(uint32_t i = 0;
i <
count; ++
i)
1985 void build_sorted(
const emitter_handle* handles, uint32_t
count,
const math::vec3& eye, uint32_t total)
1991 const auto fill_emitter_keys = [&](uint32_t
emitter_idx)
2002 const math::vec3 delta = eye - em.particles_.position[
p];
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)
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),
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);
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)
2032 for(uint32_t g = range_begin;
g < range_end; ++
g)
2034 const uint32_t
p =
g - emit_begin;
2035 const math::vec3 delta = eye - em.particles_.position[
p];
2041 else if(
count >= k_parallel_emitter_threshold)
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),
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);
2065 constexpr uint32_t k_radix_sort_threshold = 512;
2066 if(total < k_radix_sort_threshold)
2071 [](
const batched_particle&
a,
const batched_particle&
b)
2073 return a.dist > b.dist;
2079 radix_sort_desc_distances(batched_scratch_, radix_scratch_);
2083 void write_sorted_chunk(uint8_t* data, uint32_t
start, uint32_t
count,
const emitter_handle* handles)
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)
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);
2094 if(
count < k_parallel_write_threshold)
2096 for(uint32_t i = 0;
i <
count; ++
i)
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),
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)
2117 void write_direct_chunk(uint8_t* data,
2118 uint32_t global_start,
2120 const emitter_handle* handles,
2121 uint32_t emitter_count)
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)
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)
2139 for(uint32_t g = range_begin;
g < range_end; ++
g)
2142 const uint32_t out_idx =
g - global_start;
2143 write_instance_row(data +
size_t(out_idx) * k_instance_stride, em,
particle_idx);
2147 if(emitter_count < k_parallel_emitter_threshold)
2149 write_emitter_range(0, emitter_count);
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),
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);
2164 auto render_cpu_batch(
const emitter_handle* handles,
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
2175 const uint32_t total = build_prefixes(handles,
count);
2182 build_sorted(handles,
count, eye, total);
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 |
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)
2195 write_sorted_chunk(data,
start, chunk_count, handles);
2199 write_direct_chunk(data,
start, chunk_count, handles,
count);
2202 const uint32_t avail_all = bgfx::getAvailInstanceDataBuffer(total, k_instance_stride);
2203 if(avail_all >= total)
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);
2212 uint32_t rendered = 0;
2213 uint32_t offset = 0;
2214 while(offset < total)
2216 const uint32_t remaining = total - offset;
2217 uint32_t chunk = bgfx::getAvailInstanceDataBuffer(remaining, k_instance_stride);
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);
2234 auto render_batch(
const emitter_handle* handles,
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
2244 if(
count == 0 || !bgfx::isValid(texture))
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};
2269 for(uint32_t i = 0;
i <
count; ++
i)
2278 if(em.wants_gpu_pack())
2280 if(em.gpu_.pending_pack)
2290 uint32_t rendered = 0;
2308 for(emitter_handle
handle : gpu_emitters_scratch_)
2311 rendered += draw_gpu_emitter(em,
view, program, view_camera, eye_pos_vec4, texture, blend_state);
2314 (void)sort_by_depth;
2333particle_system_soa g_system;
2339 g_system.init(max_emitters);
2344 g_system.init_gpu(ctx);
2349 g_system.shutdown();
2354 g_default_sim_backend = backend;
2359 return g_default_sim_backend;
2364 g_system.set_emitter_sim_backend(
handle, backend);
2369 return g_system.get_emitter_sim_backend(
handle);
2374 return g_gpu_sim_available;
2379 return g_system.create_emitter(
shape, direction, max_particles);
2384 g_system.destroy_emitter(
handle);
2389 g_system.reset_emitter(
handle);
2398 g_system.update_emitter(
handle, dt,
desc, transform, playback);
2405 g_system.update_emitter_bounds_only(
handle,
desc, transform);
2410 g_system.sync_gpu_simulation();
2415 return g_system.has_updated(
handle);
2420 g_system.get_aabb(
handle, out_aabb);
2425 return g_system.get_num_particles(
handle);
2431 bgfx::ProgramHandle program,
2432 const float* mtx_view,
2433 const math::vec3& eye,
2434 bgfx::TextureHandle texture,
2436 bool sort_by_depth) -> uint32_t
2438 return g_system.render_batch(handles,
count,
view, program, mtx_view, eye, texture,
blend_state, sort_by_depth);
auto sample(float progress) const -> T
#define APPLOG_WARNING(...)
void reset(uint32_t _width, uint32_t _height, uint32_t _flags)
void destroy(index_buffer_handle _handle)
void update(dynamic_index_buffer_handle _handle, uint32_t _startIndex, const memory_view *_mem)
bgfx::Transform transform
void end(encoder *_encoder)
auto look_rotation(const glm::vec3 &forward, const glm::vec3 &upwards) -> glm::quat
void run(bool use_random_inputs)
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.
@ lifetime_by_emitter_speed
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
bx::EaseFn cached_ease_pos
bgfx::UniformHandle eye_pos_
std::vector< math::vec4 > ease_lut
bgfx::DynamicVertexBufferHandle ease_lut_vb
std::vector< math::vec2 > uv_offset
std::vector< math::color > color
std::vector< math::vec4 > color_lut
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
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).
bgfx::UniformHandle tex_color_
bgfx::DynamicVertexBufferHandle color_speed_lut_vb
emitter_feature cached_features
std::vector< math::vec3 > end1
bgfx::DynamicVertexBufferHandle sim_vb
bgfx::IndexBufferHandle quad_ibh_
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
std::vector< float > scale_end
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
std::vector< math::vec4 > color_speed_lut
bgfx::DynamicVertexBufferHandle color_lut_vb
std::vector< math::vec3 > start
std::vector< emitter > emitters_
std::vector< float > scale_start
std::vector< uint32_t > free_list
std::vector< batched_particle > batched_scratch_
#define APP_SCOPE_PERF(name_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Storage for box vector values and wraps up common functionality.
bbox & add_point(const vec3 &point)
Grows the bounding box based on the point passed.
void reset()
Resets the bounding box values.
bool is_populated() const
Checks if the bounding box is populated.
Full authoring description for an emitter (no transforms / runtime).
Playback gates (playing / paused).
math::mat4 local_to_world
frange_t size_by_speed_range
float inv_color_by_speed_velocity_span
frange_t color_by_speed_velocity_range
float inv_size_by_speed_velocity_span
frange_t size_by_speed_velocity_range
math::vec2 tex_sheet_tiles