diff --git a/fallingCand b/fallingCand index a89e0c5..61939f4 100755 Binary files a/fallingCand and b/fallingCand differ diff --git a/main.c b/main.c index 445a967..28af000 100644 --- a/main.c +++ b/main.c @@ -8,18 +8,32 @@ #define GRID_H 2048 static int g_fbWidth = 800; static int g_fbHeight = 800; +static double g_cursorX = 0.0; +static double g_cursorY = 0.0; SandSim sim; +static void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) { + (void)window; + g_cursorX = xpos; + g_cursorY = ypos; +} + static void render_sand(SandSim* sim, GLuint program, GLuint vao, int fbW, - int fbH) { + int fbH, + float brushX, + float brushY, + float brushRadius) { glUseProgram(program); GLint uResLoc = glGetUniformLocation(program, "u_resolution"); GLint uGridLoc = glGetUniformLocation(program, "u_gridSize"); GLint uStateLoc = glGetUniformLocation(program, "u_state"); + GLint uBrushPosLoc = glGetUniformLocation(program, "u_brushPos"); + GLint uBrushRadLoc = glGetUniformLocation(program, "u_brushRadius"); + GLint uShowBrushLoc = glGetUniformLocation(program, "u_showBrush"); if (uResLoc >= 0) { glUniform2f(uResLoc, (float)fbW, (float)fbH); @@ -27,13 +41,22 @@ static void render_sand(SandSim* sim, if (uGridLoc >= 0) { glUniform2i(uGridLoc, sim->gridW, sim->gridH); } - if (uStateLoc >= 0) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, sim->tex_curr); glUniform1i(uStateLoc, 0); } + if (uBrushPosLoc >= 0) { + glUniform2f(uBrushPosLoc, brushX, brushY); + } + if (uBrushRadLoc >= 0) { + glUniform1f(uBrushRadLoc, brushRadius); + } + if (uShowBrushLoc >= 0) { + glUniform1i(uShowBrushLoc, 1); // always show for now + } + glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES, 0, 3); } @@ -43,6 +66,7 @@ int main(void) { if (!window) { return EXIT_FAILURE; } + glfwSetCursorPosCallback(window, cursor_pos_callback); glfwGetFramebufferSize(window, &g_fbWidth, &g_fbHeight); glViewport(0, 0, g_fbWidth, g_fbHeight); printf("Renderer: %s\n", (const char*)glGetString(GL_RENDERER)); @@ -91,6 +115,8 @@ int main(void) { double sim_dt = 1.0 / 360.0; double currentTime = glfwGetTime(); double accumulator = 0.0; + float brushX = 0.0f; + float brushY = 0.0f; // --- Main loop --- while (!glfwWindowShouldClose(window)) { @@ -99,14 +125,19 @@ int main(void) { currentTime = newTime; if (frameTime > 0.25) frameTime = 0.25; accumulator += frameTime; - - + while (accumulator >= sim_dt) { - sand_step_gpu(&sim); // one discrete CA step on the GPU (commented bc unimplemeted) + sand_step_gpu(&sim); // one discrete CA step on the GPU + sand_relax_gpu(&sim); accumulator -= sim_dt; } + int paintMode = 0; glfwPollEvents(); - + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + paintMode = 1; + } else if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { + paintMode = -1; + } // Re-query framebuffer size each frame (cheap and simple) int fbw, fbh; glfwGetFramebufferSize(window, &fbw, &fbh); @@ -115,9 +146,18 @@ int main(void) { g_fbHeight = fbh; glViewport(0, 0, g_fbWidth, g_fbHeight); } + if (g_fbWidth > 0 && g_fbHeight > 0) { + float uvx = (float)g_cursorX / (float)g_fbWidth; + float uvy = (float)g_cursorY / (float)g_fbHeight; + brushX = uvx * (float)sim.gridW; + brushY = uvy * (float)sim.gridH; + } + float brushRadius = 60.0f; + sand_paint_gpu(&sim, brushX, brushY, brushRadius, paintMode); glClear(GL_COLOR_BUFFER_BIT); - render_sand(&sim, program, vao, g_fbWidth, g_fbHeight); + render_sand(&sim, program, vao, g_fbWidth, g_fbHeight, brushX, brushY, brushRadius); + glfwSwapBuffers(window); } diff --git a/sand_hello b/sand_hello deleted file mode 100755 index 250e73b..0000000 Binary files a/sand_hello and /dev/null differ diff --git a/sand_sim.c b/sand_sim.c index 48ebbe2..1d1c86c 100644 --- a/sand_sim.c +++ b/sand_sim.c @@ -1,11 +1,45 @@ // sand_sim.c #include "sand_sim.h" -#include "gl_utils.h" // for create_compute_program_from_file #include #include #include +#include "gl_utils.h" // for create_compute_program_from_file +void sand_paint_gpu(SandSim* sim, + float brushX, + float brushY, + float brushRadius, + int mode) { + if (!sim || !sim->prog_paint) return; + if (mode == 0) return; // nothing to do + + glUseProgram(sim->prog_paint); + + GLint locGrid = glGetUniformLocation(sim->prog_paint, "u_gridSize"); + GLint locPos = glGetUniformLocation(sim->prog_paint, "u_brushPos"); + GLint locRad = glGetUniformLocation(sim->prog_paint, "u_brushRadius"); + GLint locMode = glGetUniformLocation(sim->prog_paint, "u_mode"); + + if (locGrid >= 0) glUniform2i(locGrid, sim->gridW, sim->gridH); + if (locPos >= 0) glUniform2f(locPos, brushX, brushY); + if (locRad >= 0) glUniform1f(locRad, brushRadius); + if (locMode >= 0) glUniform1i(locMode, mode); + + // Bind current state as read-write image + glBindImageTexture(0, sim->tex_curr, + 0, GL_FALSE, 0, + GL_READ_WRITE, GL_R8UI); + + // Dispatch over entire grid; 16x16 workgroup like your sim shader + GLuint groupsX = (sim->gridW + 16 - 1) / 16; + GLuint groupsY = (sim->gridH + 16 - 1) / 16; + glDispatchCompute(groupsX, groupsY, 1); + + // Ensure writes visible to subsequent rendering/compute + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); +} + static void init_textures(SandSim* sim) { // tex_curr glGenTextures(1, &sim->tex_curr); @@ -25,6 +59,15 @@ static void init_textures(SandSim* sim) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + // NEW: tex_claim for the atomic “ownership” buffer + glGenTextures(1, &sim->tex_claim); + glBindTexture(GL_TEXTURE_2D, sim->tex_claim); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, sim->gridW, sim->gridH); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + // Unbind glBindTexture(GL_TEXTURE_2D, 0); } @@ -75,14 +118,26 @@ bool sand_init(SandSim* sim, int gridW, int gridH, const char* computeShaderPath sim->gridH = gridH; sim->tex_curr = 0; sim->tex_next = 0; + sim->tex_claim = 0; sim->prog_sim = 0; + sim->prog_paint = 0; init_textures(sim); upload_initial_state(sim); sim->prog_sim = create_compute_program_from_file(computeShaderPath); if (!sim->prog_sim) { - fprintf(stderr, "[sand_init] Failed to create compute program\n"); + fprintf(stderr, "[sand_init] Failed to create compute cell tick program\n"); + return false; + } + sim->prog_paint = create_compute_program_from_file("./shaders/sand_paint.comp"); + if (!sim->prog_paint) { + fprintf(stderr, "[sand_init] Failed to create compute paint program\n"); + return false; + } + sim->prog_relax = create_compute_program_from_file("./shaders/sand_relax.comp"); + if (!sim->prog_relax) { + fprintf(stderr, "[sand_init] Failed to create compute relax program\n"); return false; } @@ -90,27 +145,110 @@ bool sand_init(SandSim* sim, int gridW, int gridH, const char* computeShaderPath } void sand_step_gpu(SandSim* sim) { - if (!sim || !sim->prog_sim) return; - glUseProgram(sim->prog_sim); + static unsigned int frameCounter = 0; + frameCounter++; - // Set grid size uniform - GLint uGridLoc = glGetUniformLocation(sim->prog_sim, "u_gridSize"); - if (uGridLoc >= 0) { - glUniform2i(uGridLoc, sim->gridW, sim->gridH); + GLint locFrame = glGetUniformLocation(sim->prog_sim, "u_frame"); + if (locFrame >= 0) { + glUniform1ui(locFrame, frameCounter); + } + GLint locGrid = glGetUniformLocation(sim->prog_sim, "u_gridSize"); + if (locGrid >= 0) { + glUniform2i(locGrid, sim->gridW, sim->gridH); } - // Bind images (must match bindings in sand_step.comp) - glBindImageTexture(0, sim->tex_curr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8UI); - glBindImageTexture(1, sim->tex_next, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8UI); + // Clear next state to AIR (0) - GLuint groupsX = (sim->gridW + 15) / 16; - GLuint groupsY = (sim->gridH + 15) / 16; + const GLubyte zeroByte[1] = {0}; + glClearTexImage(sim->tex_next, + 0, + GL_RED_INTEGER, + GL_UNSIGNED_BYTE, + zeroByte); + // Clear claim buffer to 0 + const GLuint zero1[1] = {0}; + glClearTexImage(sim->tex_claim, + 0, + GL_RED_INTEGER, // format doesn't really matter; data is uint + GL_UNSIGNED_INT, + zero1); + + // Bind images + glBindImageTexture(0, sim->tex_curr, + 0, GL_FALSE, 0, + GL_READ_ONLY, GL_R8UI); + glBindImageTexture(1, sim->tex_next, + 0, GL_FALSE, 0, + GL_WRITE_ONLY, GL_R8UI); + glBindImageTexture(2, sim->tex_claim, + 0, GL_FALSE, 0, + GL_READ_WRITE, GL_R32UI); + + GLuint groupsX = (sim->gridW + 16 - 1) / 16; + GLuint groupsY = (sim->gridH + 16 - 1) / 16; glDispatchCompute(groupsX, groupsY, 1); + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); - // Ping-pong + // ping-pong: curr <-> next + GLuint tmp = sim->tex_curr; + sim->tex_curr = sim->tex_next; + sim->tex_next = tmp; +} +void sand_relax_gpu(SandSim* sim) { + if (!sim || !sim->prog_relax) return; + + glUseProgram(sim->prog_relax); + + static unsigned int relaxFrame = 0; + relaxFrame++; + + GLint locFrame = glGetUniformLocation(sim->prog_relax, "u_frame"); + if (locFrame >= 0) { + glUniform1ui(locFrame, relaxFrame); + } + + GLint locGrid = glGetUniformLocation(sim->prog_relax, "u_gridSize"); + if (locGrid >= 0) { + glUniform2i(locGrid, sim->gridW, sim->gridH); + } + + // Clear next state to AIR (0) – tex_next is GL_R8UI + const GLubyte zeroByte[1] = {0}; + glClearTexImage(sim->tex_next, + 0, + GL_RED_INTEGER, + GL_UNSIGNED_BYTE, + zeroByte); + + // Clear claim buffer (GL_R32UI) to 0 + const GLuint zero1[1] = {0}; + glClearTexImage(sim->tex_claim, + 0, + GL_RED_INTEGER, + GL_UNSIGNED_INT, + zero1); + + // Bind images: curr = read, next = write, claim = read/write + glBindImageTexture(0, sim->tex_curr, + 0, GL_FALSE, 0, + GL_READ_ONLY, GL_R8UI); + glBindImageTexture(1, sim->tex_next, + 0, GL_FALSE, 0, + GL_WRITE_ONLY, GL_R8UI); + glBindImageTexture(2, sim->tex_claim, + 0, GL_FALSE, 0, + GL_READ_WRITE, GL_R32UI); + + GLuint groupsX = (sim->gridW + 16 - 1) / 16; + GLuint groupsY = (sim->gridH + 16 - 1) / 16; + glDispatchCompute(groupsX, groupsY, 1); + + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + // ping-pong: curr <-> next GLuint tmp = sim->tex_curr; sim->tex_curr = sim->tex_next; sim->tex_next = tmp; @@ -119,16 +257,14 @@ void sand_step_gpu(SandSim* sim) { void sand_destroy(SandSim* sim) { if (!sim) return; - if (sim->tex_curr) { - glDeleteTextures(1, &sim->tex_curr); - sim->tex_curr = 0; - } - if (sim->tex_next) { - glDeleteTextures(1, &sim->tex_next); - sim->tex_next = 0; - } - if (sim->prog_sim) { - glDeleteProgram(sim->prog_sim); - sim->prog_sim = 0; - } + if (sim->tex_curr) glDeleteTextures(1, &sim->tex_curr); + if (sim->tex_next) glDeleteTextures(1, &sim->tex_next); + if (sim->tex_claim) glDeleteTextures(1, &sim->tex_claim); + sim->tex_curr = sim->tex_next = sim->tex_claim = 0; + + if (sim->prog_sim) glDeleteProgram(sim->prog_sim); + if (sim->prog_paint) glDeleteProgram(sim->prog_paint); + if (sim->prog_relax) glDeleteProgram(sim->prog_relax); + sim->prog_sim = sim->prog_paint = sim->prog_relax = 0; } + diff --git a/sand_sim.h b/sand_sim.h index da0dde6..50208eb 100644 --- a/sand_sim.h +++ b/sand_sim.h @@ -7,21 +7,29 @@ // Simple binary sand vs air simulation typedef struct { - GLuint tex_curr; // R8UI; 0 = air, 1 = sand - GLuint tex_next; // R8UI; ping-pong target - GLuint prog_sim; // compute shader program int gridW; int gridH; + GLuint tex_curr; + GLuint tex_next; + GLuint tex_claim; // NEW: claim buffer + GLuint prog_sim; + GLuint prog_paint; + GLuint prog_relax; } SandSim; + // Initialize sim, allocate textures, upload initial state, load compute shader. // Returns true on success, false on failure. bool sand_init(SandSim* sim, int gridW, int gridH, const char* computeShaderPath); // Advance simulation by 1 discrete tick (one CA step). void sand_step_gpu(SandSim* sim); - +void sand_relax_gpu(SandSim* sim); // Destroy all GL objects owned by the sim. void sand_destroy(SandSim* sim); - +void sand_paint_gpu(SandSim* sim, + float brushX, + float brushY, + float brushRadius, + int mode); #endif // SAND_SIM_H diff --git a/shaders/sand_display.frag b/shaders/sand_display.frag index 4004316..1ee371e 100644 --- a/shaders/sand_display.frag +++ b/shaders/sand_display.frag @@ -5,7 +5,9 @@ out vec4 FragColor; uniform vec2 u_resolution; // framebuffer size in pixels uniform ivec2 u_gridSize; // sand grid size uniform usampler2D u_state; // GL_R8UI texture - +uniform vec2 u_brushPos; +uniform float u_brushRadius; +uniform int u_showBrush; void main() { vec2 fragCoord = gl_FragCoord.xy; @@ -21,6 +23,23 @@ void main() { vec3 color = (v == 1u) ? vec3(0.9, 0.8, 0.1) // sand : vec3(0.05, 0.05, 0.10); // background + if (u_showBrush != 0) { + // center of this cell in grid coordinates + vec2 cellCenter = vec2(cell) + vec2(0.5); + float dist = length(cellCenter - u_brushPos); + + // how thick the ring is, in cells + float thickness = 1.0; // 1-cell-thick ring + + // abs(dist - radius) < thickness => inside ring + float d = abs(dist - u_brushRadius); + + // Smooth edge: edge = 1 at center of ring, 0 outside + float edge = smoothstep(thickness, 0.0, d); + + vec3 ringColor = vec3(1.0); // white ring + color = mix(color, ringColor, edge); + } FragColor = vec4(color, 1.0); } diff --git a/shaders/sand_paint.comp b/shaders/sand_paint.comp new file mode 100644 index 0000000..986115b --- /dev/null +++ b/shaders/sand_paint.comp @@ -0,0 +1,42 @@ +#version 430 core + +layout(local_size_x = 16, local_size_y = 16) in; + +// Read-write access to the current grid +layout(r8ui, binding = 0) coherent uniform uimage2D state; + +uniform ivec2 u_gridSize; +uniform vec2 u_brushPos; // in grid-space (cells) +uniform float u_brushRadius; // in cells +uniform int u_mode; // 0 = none, 1 = add sand, -1 = erase + +void main() +{ + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + if (pos.x >= u_gridSize.x || pos.y >= u_gridSize.y) { + return; + } + + if (u_mode == 0) { + return; + } + + // Center of this cell in grid coords + vec2 cellCenter = vec2(pos) + vec2(0.5); + float dist = length(cellCenter - u_brushPos); + + if (dist > u_brushRadius) { + return; + } + + const uint AIR = 0u; + const uint SAND = 1u; + + if (u_mode > 0) { + // left click: fill with sand + imageStore(state, pos, uvec4(SAND, 0u, 0u, 0u)); + } else if (u_mode < 0) { + // right click: erase sand + imageStore(state, pos, uvec4(AIR, 0u, 0u, 0u)); + } +} diff --git a/shaders/sand_relax.comp b/shaders/sand_relax.comp new file mode 100644 index 0000000..dfbe34e --- /dev/null +++ b/shaders/sand_relax.comp @@ -0,0 +1,135 @@ +#version 430 core +layout(local_size_x = 16, local_size_y = 16) in; + +// state_curr: read-only; state_next: write-only +layout(r8ui, binding = 0) readonly uniform uimage2D state_curr; +layout(r8ui, binding = 1) writeonly uniform uimage2D state_next; + +// claim buffer to resolve conflicts +layout(r32ui, binding = 2) coherent uniform uimage2D claim; + +uniform ivec2 u_gridSize; +uniform uint u_frame; + +const uint AIR = 0u; +const uint SAND = 1u; + +// How far down we scan to approximate column thickness +const int MAX_SCAN = 6; + +// How much “taller” this column must be vs neighbor before we slide +// Smaller -> shallower, more flowy piles +const int SLOPE_THRESHOLD = 2; + +uint hash(uvec2 p, uint seed) { + p = p * 1664525u + 1013904223u + seed; + p.x ^= p.y >> 16; + p.y ^= p.x >> 16; + return p.x ^ p.y; +} + +// Approximate thickness of the sand column under (x, y) +int columnThickness(int x, int y) { + int t = 0; + for (int dy = 0; dy < MAX_SCAN; ++dy) { + int yy = y + dy; + if (yy >= u_gridSize.y) break; + uint v = imageLoad(state_curr, ivec2(x, yy)).r; + if (v == SAND) { + t++; + } else { + break; + } + } + return t; +} + +void main() { + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + if (pos.x >= u_gridSize.x || pos.y >= u_gridSize.y) return; + + uint self = imageLoad(state_curr, pos).r; + if (self != SAND) return; + + // Lock bottom row in place + if (pos.y == u_gridSize.y - 1) { + imageStore(state_next, pos, uvec4(SAND, 0u, 0u, 0u)); + return; + } + + ivec2 dest = pos; + + // Only relax grains that are resting on something + ivec2 down = pos + ivec2(0, 1); + uint below = imageLoad(state_curr, down).r; + if (below == SAND) { + int hSelf = columnThickness(pos.x, pos.y); + + bool canLeft = false; + bool canRight = false; + + ivec2 left = pos + ivec2(-1, 0); + ivec2 right = pos + ivec2( 1, 0); + + int hLeft = 0; + int hRight = 0; + + // Check left side + if (left.x >= 0) { + uint leftCell = imageLoad(state_curr, left).r; + if (leftCell == AIR) { + hLeft = columnThickness(left.x, pos.y); + int slopeL = hSelf - hLeft; + if (slopeL > SLOPE_THRESHOLD) { + canLeft = true; + } + } + } + + // Check right side + if (right.x < u_gridSize.x) { + uint rightCell = imageLoad(state_curr, right).r; + if (rightCell == AIR) { + hRight = columnThickness(right.x, pos.y); + int slopeR = hSelf - hRight; + if (slopeR > SLOPE_THRESHOLD) { + canRight = true; + } + } + } + + if (canLeft || canRight) { + if (canLeft && canRight) { + // Prefer the *lower* column; randomize if equal + if (hLeft < hRight) { + dest = left; + } else if (hRight < hLeft) { + dest = right; + } else { + uint h = hash(uvec2(pos), u_frame); + bool chooseLeft = (h & 1u) != 0u; + dest = chooseLeft ? left : right; + } + } else if (canLeft) { + dest = left; + } else { // canRight + dest = right; + } + } + } + + bool staying = (dest.x == pos.x && dest.y == pos.y); + + if (staying) { + imageStore(state_next, pos, uvec4(SAND, 0u, 0u, 0u)); + } else { + // Moving to new cell; resolve conflicts via claim buffer + uint old = imageAtomicCompSwap(claim, dest, 0u, 1u); + if (old == 0u) { + imageStore(state_next, dest, uvec4(SAND, 0u, 0u, 0u)); + } else { + // lost: stay put + imageStore(state_next, pos, uvec4(SAND, 0u, 0u, 0u)); + } + } +} diff --git a/shaders/sand_step.comp b/shaders/sand_step.comp index 67bd284..be89790 100644 --- a/shaders/sand_step.comp +++ b/shaders/sand_step.comp @@ -2,10 +2,25 @@ layout(local_size_x = 16, local_size_y = 16) in; -layout(r8ui, binding = 0) readonly uniform uimage2D state_curr; -layout(r8ui, binding = 1) writeonly uniform uimage2D state_next; +// state_curr: read-only; state_next: write-only +layout(r8ui, binding = 0) readonly uniform uimage2D state_curr; +layout(r8ui, binding = 1) writeonly uniform uimage2D state_next; + +// claim buffer to resolve conflicts +layout(r32ui, binding = 2) coherent uniform uimage2D claim; uniform ivec2 u_gridSize; +uniform uint u_frame; + +const uint AIR = 0u; +const uint SAND = 1u; + +uint hash(uvec2 p, uint seed) { + p = p * 1664525u + 1013904223u + seed; + p.x ^= p.y >> 16; + p.y ^= p.x >> 16; + return p.x ^ p.y; +} void main() { ivec2 pos = ivec2(gl_GlobalInvocationID.xy); @@ -13,30 +28,90 @@ void main() { return; } - const uint AIR = 0u; - const uint SAND = 1u; - uint self = imageLoad(state_curr, pos).r; - - bool canFallDown = - (self == SAND) && - (pos.y + 1 < u_gridSize.y) && - (imageLoad(state_curr, pos + ivec2(0, 1)).r == AIR); - - bool filledFromAbove = - (self == AIR) && - (pos.y > 0) && - (imageLoad(state_curr, pos + ivec2(0, -1)).r == SAND); - - uint next; - - if (canFallDown) { - next = AIR; - } else if (filledFromAbove) { - next = SAND; - } else { - next = self; + if (self != SAND) { + // AIR cells: do nothing (next is cleared to AIR on CPU) + return; } - imageStore(state_next, pos, uvec4(next, 0u, 0u, 0u)); + ivec2 dest = pos; + + // Hard floor: bottom row does not move down + if (pos.y < u_gridSize.y - 1) { + // 1) Try straight down + ivec2 down = pos + ivec2(0, 1); + bool canDown = (imageLoad(state_curr, down).r == AIR); + + if (canDown) { + dest = down; + } else { + // 2) Try diagonals + bool canLeft = false; + bool canRight = false; + + ivec2 downLeft = pos + ivec2(-1, 1); + ivec2 downRight = pos + ivec2( 1, 1); + + if (downLeft.x >= 0 && downLeft.y < u_gridSize.y) { + if (imageLoad(state_curr, downLeft).r == AIR) { + canLeft = true; + } + } + if (downRight.x < u_gridSize.x && downRight.y < u_gridSize.y) { + if (imageLoad(state_curr, downRight).r == AIR) { + canRight = true; + } + } + + // Flip which side is "first" based on frame parity + bool flip = ((u_frame & 1u) != 0u); + + ivec2 firstDest, secondDest; + bool canFirst, canSecond; + + if (!flip) { + // even frames: left is "first" + firstDest = downLeft; + secondDest = downRight; + canFirst = canLeft; + canSecond = canRight; + } else { + // odd frames: right is "first" + firstDest = downRight; + secondDest = downLeft; + canFirst = canRight; + canSecond = canLeft; + } + + if (canFirst && canSecond) { + // both free: pick randomly, but "first"/"second" swap each frame + uint h = hash(uvec2(pos), u_frame); + bool useFirst = (h & 1u) != 0u; + dest = useFirst ? firstDest : secondDest; + } else if (canFirst) { + dest = firstDest; + } else if (canSecond) { + dest = secondDest; + } + // else: no diagonal move, dest stays = pos + } + } + + // Write into next state with claim resolution + if (dest.x == pos.x && dest.y == pos.y) { + // Not moving; just keep sand here + imageStore(state_next, pos, uvec4(SAND, 0u, 0u, 0u)); + } else { + // Moving to a new cell; try to claim it + uint old = imageAtomicCompSwap(claim, dest, 0u, 1u); + + if (old == 0u) { + // Won the slot: move + imageStore(state_next, dest, uvec4(SAND, 0u, 0u, 0u)); + // Original pos stays AIR because state_next was cleared + } else { + // Lost the slot: stay put + imageStore(state_next, pos, uvec4(SAND, 0u, 0u, 0u)); + } + } }