pub const PARTICLE_SIMULATE: &str = "// GPU particle simulation kernel: single source for every backend.\n//\n// One thread per slot in the per-emitter pool. Each thread ages and integrates\n// whatever particle currently occupies its slot; if the age reaches the\n// lifetime the slot is marked dead. A dead slot then tries to consume one unit\n// of the frame\'s spawn budget (atomically) and respawns with a fresh velocity\n// sampled inside a cone of half-angle acos(spread_cos) around `direction`.\n//\n// `Particle` and `ParticleParams` arrive from the shared PARTICLE_TYPES\n// fragment, which the render half (`particle.slang`) splices too, so the pool\n// this kernel writes and the pool that pass reads have one declaration. Both\n// are locked to the Rust struct by `particle_params_layout_matches_msl` in\n// render_types.rs.\n\n{PARTICLE_TYPES}\n\n[[vk::binding(0, 0)]] RWStructuredBuffer<Particle> pool : register(u0);\n// Remaining spawn budget for this dispatch, as a single counter at element 0.\n[[vk::binding(1, 0)]] RWStructuredBuffer<uint> spawn_counter : register(u1);\n\n// A host difference, not a target one: Vulkan pushes the params and DirectX\n// takes them as root constants at b0 (where a bare push constant lands there),\n// while the Metal encoder writes them to buffer(2).\n#ifdef METAL_BINDINGS\nConstantBuffer<ParticleParams> params : register(b2);\n#else\n[[vk::push_constant]] ConstantBuffer<ParticleParams> params;\n#endif\n\n// Cheap fixed-point hash to a unit float in [0, 1). Mutates `state` so a thread\n// needing several uncorrelated samples advances it between calls.\nfloat prng(inout uint state)\n{\n state = state * 1664525u + 1013904223u;\n // Top 24 bits of the hash become the mantissa.\n return float(state >> 8) * (1.0 / 16777216.0);\n}\n\n// Sample a unit vector inside a cone of half-angle acos(cone_cos) centred on\n// `axis`. The cap is uniformly sampled in solid angle, so the spawn cloud has\n// no axial bunching.\nfloat3 sample_cone(inout uint rng, float3 axis, float cone_cos)\n{\n // `u` picks a polar angle whose cosine is uniform in [cone_cos, 1].\n float u = lerp(cone_cos, 1.0, prng(rng));\n float r = sqrt(max(1.0 - u * u, 0.0));\n float phi = prng(rng) * 6.2831853;\n float3 local = float3(r * cos(phi), r * sin(phi), u);\n\n // Any orthonormal basis around `axis`. Picking the world axis least\n // parallel to it keeps the cross product well conditioned.\n float3 up = abs(axis.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);\n float3 t = normalize(cross(up, axis));\n float3 b = cross(axis, t);\n return normalize(local.x * t + local.y * b + local.z * axis);\n}\n\n[shader(\"compute\")]\n[numthreads(64, 1, 1)]\nvoid particle_simulate(uint3 gid : SV_DispatchThreadID)\n{\n uint id = gid.x;\n if (id >= params.max_particles)\n {\n return;\n }\n Particle pt = pool[id];\n\n // Age the particle in this slot, if any. A lifetime of 0 flags a dead slot.\n if (pt.velocity_lifetime.w > 0.0)\n {\n pt.position_age.w += params.dt;\n if (pt.position_age.w >= pt.velocity_lifetime.w)\n {\n pt.velocity_lifetime.w = 0.0;\n }\n else\n {\n pt.velocity_lifetime.xyz =\n pt.velocity_lifetime.xyz + params.gravity_speed_max.xyz * params.dt;\n pt.position_age.xyz = pt.position_age.xyz + pt.velocity_lifetime.xyz * params.dt;\n }\n }\n\n // A dead slot claims one unit of the remaining budget. Adding -1 in two\'s\n // complement returns the pre-subtract value, so only threads that observed\n // a positive remaining count spawn; threads racing past zero see 0 or a\n // wrapped very large unsigned number.\n if (pt.velocity_lifetime.w == 0.0 && params.spawn_budget > 0u)\n {\n uint claimed;\n InterlockedAdd(spawn_counter[0], 0xFFFFFFFFu, claimed);\n if (claimed > 0u && claimed <= params.spawn_budget)\n {\n uint rng = (id * 747796405u) ^ (params.random_seed * 2891336453u);\n // Warm the RNG so adjacent threads decorrelate; the value is dropped.\n float warm = prng(rng);\n float3 dir = sample_cone(\n rng,\n normalize(params.direction_speed_min.xyz),\n params.position_spread.w);\n float speed = lerp(\n params.direction_speed_min.w, params.gravity_speed_max.w, prng(rng));\n float life = lerp(params.lifetime_min, params.lifetime_max, prng(rng));\n pt.position_age = float4(params.position_spread.xyz, 0.0);\n pt.velocity_lifetime = float4(dir * speed, max(life, 0.001));\n }\n }\n\n pool[id] = pt;\n}\n";Expand description
particle_simulate.slang.