#version 450 #extension GL_GOOGLE_include_directive : require layout(local_size_x = 8, local_size_y = 8) in; #include "common.glsl" // Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_ // (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the // same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame. layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1) layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536 void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); if (p.x >= pc.gw || p.y >= pc.gh) return; int ph = pc.pass_ & 3; ivec2 q; if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0); else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1); if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return; int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x; uint ci = cells[i], cj = cells[j]; uint mi = cell_mat(ci), mj = cell_mat(cj); if (mi == 0u && mj == 0u) return; uint n = mats[mats.length() - 1u].w; uint h = rhead[mi * n + mj]; uint cnt = h & 255u; if (cnt == 0u) return; uint lo = uint(min(i, j)), hi = uint(max(i, j)); uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u); uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0) // 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per // update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of // each of its two cells separately (rec / 4 for a material next to air, not rec / 2). { const bool iLo = i < j; const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi; const ivec2 pA = iLo ? p : q, pB = iLo ? q : p; const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA]; const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u; const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u]; const ivec2 dAB = pB - pA; const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u; const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u; // each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u; const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u); const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u; const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB]; const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u; const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u; if (!fireA && !fireB) return; k = fireA ? kA : kB; } uint e = ((h >> 8) + k % cnt) * 5u; uint outs = rrec[e]; uint third = rrec[e + 2u]; uint blob = rrec[e + 3u]; uint entityIdx = rrec[e + 4u] & 0x7FFFFFFFu; bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output uint o = second ? (outs >> 16) : (outs & 0xFFFFu); const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair) const uint reqLife = blob >> 20; if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed) const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u; if (le && ri) o = 0u; } // three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side) // must hold the third material; the thread of the first cell writes its output if ((third & 0xFFFFu) != 0xFFFFu) { const bool firstIsI = firstIsI0; const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p; const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1)); ivec2 p3 = pf + dirs[(rr >> 20) & 3u]; if (p3 == ps) p3 = 2 * pf - ps; if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return; const int k3 = p3.y * pc.gw + p3.x; const uint c3 = cells[k3]; if (cell_mat(c3) != (third & 0xFFFFu)) return; const uint o3 = (third >> 16) & 0xFFFFu; if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); } } if (o == 0xFFFFu || o == mi) return; // this side stays as it is cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits ext[i] = vec2(0.0); birth[i] = birth_frame(); if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it) const int ei = atomicAdd(ctrl.reactN.x, 1); if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0); else atomicAdd(ctrl.reactN.x, -1); } // blob_radius with blob_restrict_to_input_material (0071ab50 conditional mode): trunc(r * 2 pi + 0.5) rays of unit steps from the cell; a cell of the input material converts, // one that already holds the output lets the ray go on, anything else ends it (a flood of the connected input material up to the radius) { const uint rad = second ? ((blob >> 8) & 255u) : (blob & 255u); const bool restricted = ((blob >> (second ? 17 : 16)) & 1u) != 0u; if (rad > 0u && restricted) { const int nr = int(float(rad) * 6.2831853 + 0.5); for (int ray = 0; ray < nr; ++ray) { const float a = float(ray) * 6.2831853 / float(nr); const vec2 dir = vec2(cos(a), sin(a)); for (int st = 1; st <= int(rad); ++st) { const ivec2 t = p + ivec2(trunc(dir * float(st))); if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break; const int ti = t.y * pc.gw + t.x; const uint tc = cells[ti], tm = cell_mat(tc); if (tm == o) continue; if (tm != mi) break; if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0) } } } else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds const int ri = int(rad); for (int dy = -ri; dy <= ri; ++dy) for (int dx = -ri; dx <= ri; ++dx) { if (dx * dx + dy * dy >= ri * ri) continue; const ivec2 t = p + ivec2(dx, dy); if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue; const int ti = t.y * pc.gw + t.x; const uint tc = cells[ti]; if (cell_mat(tc) == o) continue; if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } } } } }