// State of the GPU projectile / effect system (device local, all simulated by proj_*.comp and fx_*.comp). const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512; // (the GPU's ring is 0..255, the host writes 256..511) // FProj.d.z bit 30: a CPU query materialized this instance; automatic Lua yields to its twin. struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades) struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle layout(std430, set = 0, binding = 36) coherent buffer FxState { uint fcnt[4]; // projectile ring, particle ring, animation ring FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj) FPart fparts[16384]; FAnim fanims[512]; }; // Integer alias of the same storage, solely for atomically claiming a particle's // life word. A plain float test allowed concurrent searches to return the same slot. layout(std430, set = 0, binding = 36) coherent buffer FxStateAtomic { uint fxwords[]; }; // A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead // record of the next 64; -1 when there is none. #include "particle_alloc.glsl" // colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent // (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp) int emit_particle_g(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive, bool collide) { // the record, -1 when the pool is full const int si = alloc_part(); if (si < 0) return -1; const uint s = uint(si); fparts[s].a = vec4(pos, vel); fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | (fade ? 0x01000000u : 0u) | (additive ? 0x02000000u : 0u))); fparts[s].c = uvec4(0u, 0u, 0u, collide ? 0u : 2u); // c.z: 255 - the material's alpha (drawn opacity of a non-additive cosmetic) return si; } // the cosmetic constructor 00716ce0 without force-create: the particle is made only where the grid slot is empty (and loaded) bool cell_free_at(vec2 w) { const ivec2 q = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u; } void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); } // A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; c.z = material + 1 supplies the stain colour. It never owns or deposits a cell. uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full const int si = alloc_part(); if (si < 0) return 0xFFFFFFFFu; const uint s = uint(si); fparts[s].a = vec4(pos, vel); fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x01000000u)); fparts[s].c = uvec4(1u, 0u, uint(material + 1), 0u); return s; } // A particle that becomes a cell of the material when it lands (blood, acid... "real" particles): max life is -(material + 1). // the acceleration of a particle record (00712010 copies 0115499c / 011549a0, which the particle manager resets to (0, pixels_per_physics_unit x 10) = (0, 100) px/s^2, // 007128c0 / 00712bc0); the real particles (00716af0 -> 007168c0) keep it: 100 / 3600 cells per frame^2 const float PART_GRAV = 100.0 / 3600.0; bool emit_material_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // false when the pool is full const int si = alloc_part(); if (si < 0) return false; const uint s = uint(si); fparts[s].a = vec4(pos, vel); fparts[s].b = vec4(life, -float(material + 1), grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x04000000u)); fparts[s].c = uvec4(0u); // a flag in the top byte keeps the bits a normal float (a zero top byte is a denormal and may be flushed) return true; } // ---- liquid taken out of the grid by an entity, a projectile or a body (docs/DISPLACEMENT_AND_FIRE.md): the cell becomes a particle that keeps its identity // (the whole cell word) and goes back into the grid when it has landed. c.y: 1 = mode 1 (moving up, passes liquids with a penetration budget c.z), 2 = mode 0 // (falls, stops at liquid or solid), 3 = waiting for a free place (retries every tick). void emit_displaced(vec2 pos, vec2 vel, uint cellword, uint kind, uint birthv) { // birthv: the age (birth frame) of the cell travels with it (00717670 / 007168c0 hand over the same cell object) const int si = alloc_part(); if (si < 0) return; // the pool is full: the cell is gone, as in the original (the caller cleared the grid slot first) const uint s = uint(si); fparts[s].a = vec4(pos, vel); fparts[s].b = vec4(1.0e6, 1.0, uintBitsToFloat(birthv), 0.0); fparts[s].c = uvec4(cellword, kind, kind == 1u ? 20u : 0u, (hash3(s, cellword, 77u) % 101u) < 50u ? 0x100000u : 0u); // bit 20: effective density zero (an inclusive draw below 50) } // Takes a liquid cell (not a powder) out of the grid and makes it a particle with the velocity v (cells per frame). Returns whether it did. bool displace_cell(ivec2 w, vec2 v) { ivec2 p = w - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; int i = p.y * pc.gw + p.x; uint c = cells[i]; uint bv = birth[i]; uint m = cell_mat(c); if (m == 0u) return false; uint f = mflags(m); if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) return false; if (atomicCompSwap(cells[i], c, 0u) != c) return false; // the grid slot is cleared before the particle exists ext[i] = vec2(0.0); emit_displaced(vec2(w) + 0.5, v, c, 1u, bv); // mode 1; it turns into mode 0 when it moves down (fx_particles.comp) return true; } // Is the cell at the world position a liquid that counts for the drag (liquid, not a powder)? bool liquid_nonsand(ivec2 w) { ivec2 p = w - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; uint m = cell_mat(cells[p.y * pc.gw + p.x]); if (m == 0u) return false; uint f = mflags(m); return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too) } // 0071fba0: puts a cell back into the grid. Searches from (x, y) upward to y - 128: an empty cell accepts it, a non-solid cell that is lighter is exchanged (the // lighter cell then looks for a place of its own: it becomes a waiting particle); equal density is rejected, a solid cell ends the search; after more than // five steps a rejection may shift the column (20 %, then 50 % right / left). bool reinsert(uint cw, ivec2 w, uint seed, uint birthv) { float dens = mdensity(cell_mat(cw)); for (int step = 0; step <= 128; ++step) { ivec2 t = ivec2(w.x, w.y - step); ivec2 q = t - ivec2(pc.view_x, pc.view_y); if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; // not loaded: no random draw either int ti = q.y * pc.gw + q.x; uint occ = cells[ti]; uint om = cell_mat(occ); if (om == 0u) { if (atomicCompSwap(cells[ti], 0u, cw) == 0u) { ext[ti] = vec2(0.0); birth[ti] = birthv; return true; } continue; } uint of = mflags(om); if ((of & F_STATIC) != 0u) return false; // a solid cell stops the search if (mdensity(om) < dens && atomicCompSwap(cells[ti], occ, cw) == occ) { // the lighter cell is queued, not destroyed ext[ti] = vec2(0.0); const uint obirth = birth[ti]; birth[ti] = birthv; emit_displaced(vec2(t) + 0.5, vec2(0.0), occ, 3u, obirth); return true; } if (step > 5 && (hash3(seed, uint(step), 61u) % 101u) < 20u) w.x += (hash3(seed, uint(step), 62u) % 101u) < 50u ? 1 : -1; } return false; }