From 7045273c39a9fcfda0d84fad69708f268644ba67 Mon Sep 17 00:00:00 2001 From: Andrew Cohn Date: Sat, 29 Nov 2025 13:59:31 -0800 Subject: [PATCH] yeah boy this sand be fallin --- .gitignore | 1 - .vscode/settings.json | 13 ++++ Makefile | 2 +- fallingCand | Bin 86000 -> 90576 bytes gl_utils.c | 28 ++++++++ gl_utils.h | 2 +- main.c | 69 ++++++++++++++++---- sand_sim.c | 134 ++++++++++++++++++++++++++++++++++++++ sand_sim.h | 27 ++++++++ shaders/sand_display.frag | 26 ++++++++ shaders/sand_step.comp | 42 ++++++++++++ 11 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 sand_sim.c create mode 100644 sand_sim.h create mode 100644 shaders/sand_display.frag create mode 100644 shaders/sand_step.comp diff --git a/.gitignore b/.gitignore index 03126d8..1530978 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -.vscode *.o \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3eaba84 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "makefile.launchConfigurations": [ + { + "name": "sand_mangohud", + "cwd": "${workspaceFolder}", + "binaryPath": "/usr/bin/mangohud", // use full path to be safe + "binaryArgs": [ + "--dlsym", + "${workspaceFolder}/fallingCand" + ] + } + ] +} diff --git a/Makefile b/Makefile index 5bd163d..a166029 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC = gcc CFLAGS = -Wall -Wextra -O2 $(shell pkg-config --cflags glfw3) LDFLAGS = $(shell pkg-config --libs glfw3) -ldl -SRC = glad.c gl_utils.c main.c +SRC = glad.c gl_utils.c main.c sand_sim.c OBJ = $(SRC:.c=.o) BIN = fallingCand diff --git a/fallingCand b/fallingCand index 7695f50d260f275e9c91afa5f1c3e0717ed17654..5db11e81c3aa66c38644346a90d88cf4e6f99c73 100755 GIT binary patch literal 90576 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 Q1^>P&{#7z+NY!KWA9^OHTmS$7 literal 86000 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 diff --git a/gl_utils.c b/gl_utils.c index 284926e..9993480 100644 --- a/gl_utils.c +++ b/gl_utils.c @@ -110,6 +110,34 @@ GLuint create_program_from_files(const char* vsPath, const char* fsPath) { return prog; } +GLuint create_compute_program_from_file(const char* path) { + char* source = load_text_file(path); + if (!source) { + fprintf(stderr, "Could not load compute shader from '%s'\n", path); + exit(EXIT_FAILURE); + } + + // Reuse compile_shader; type is GL_COMPUTE_SHADER + GLuint cs = compile_shader(GL_COMPUTE_SHADER, source, path); + free(source); + + GLuint prog = glCreateProgram(); + glAttachShader(prog, cs); + glLinkProgram(prog); + glDeleteShader(cs); + + GLint linked = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &linked); + if (!linked) { + char log[1024]; + glGetProgramInfoLog(prog, sizeof(log), NULL, log); + fprintf(stderr, "Compute program link failed (%s):\n%s\n", path, log); + glDeleteProgram(prog); + exit(EXIT_FAILURE); + } + + return prog; +} GLFWwindow* init_glfw_glad(const char* title, int width, int height) { glfwSetErrorCallback(glfw_error_callback); diff --git a/gl_utils.h b/gl_utils.h index 7de4523..8e337a4 100644 --- a/gl_utils.h +++ b/gl_utils.h @@ -18,7 +18,7 @@ GLuint compile_shader(GLenum type, const char* source, const char* debugName); // Compile & link a vertex+fragment program from files. GLuint create_program_from_files(const char* vsPath, const char* fsPath); - +GLuint create_compute_program_from_file(const char* path); // Minimal GLFW+GLAD init: sets error callback, (optionally) hints Wayland, // calls glfwInit, creates window, makes context current, loads GLAD. // Returns the created window or NULL on fatal error. diff --git a/main.c b/main.c index 7a4c27c..9e2f77f 100644 --- a/main.c +++ b/main.c @@ -3,8 +3,40 @@ #include #include "gl_utils.h" +#include "sand_sim.h" +#define GRID_W 1024 +#define GRID_H 1024 static int g_fbWidth = 800; static int g_fbHeight = 800; +SandSim sim; + +static void render_sand(SandSim* sim, + GLuint program, + GLuint vao, + int fbW, + int fbH) { + glUseProgram(program); + + GLint uResLoc = glGetUniformLocation(program, "u_resolution"); + GLint uGridLoc = glGetUniformLocation(program, "u_gridSize"); + GLint uStateLoc = glGetUniformLocation(program, "u_state"); + + if (uResLoc >= 0) { + glUniform2f(uResLoc, (float)fbW, (float)fbH); + } + 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); + } + + glBindVertexArray(vao); + glDrawArrays(GL_TRIANGLES, 0, 3); +} int main(void) { GLFWwindow* window = init_glfw_glad("Falling Sand - Fullscreen Quad", g_fbWidth, g_fbHeight); @@ -44,22 +76,37 @@ int main(void) { (void*)0); glBindVertexArray(0); - + if (!sand_init(&sim, GRID_W, GRID_H, "shaders/sand_step.comp")) { + fprintf(stderr, "Failed to initialize sand sim\n"); + return EXIT_FAILURE; + } // --- Create shader program --- GLuint program = create_program_from_files( "shaders/fullscreen.vert", - "shaders/gradient.frag"); - GLint uResLoc = glGetUniformLocation(program, "u_resolution"); - if (uResLoc == -1) { - fprintf(stderr, "[warn] u_resolution uniform not found (maybe optimized out?)\n"); - } + "shaders/sand_display.frag"); + glUseProgram(program); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - + double sim_dt = 1.0 / 120.0; + double currentTime = glfwGetTime(); + double accumulator = 0.0; + // --- Main loop --- while (!glfwWindowShouldClose(window)) { + double newTime = glfwGetTime(); + double frameTime = newTime - currentTime; + 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) + accumulator -= sim_dt; + } glfwPollEvents(); + // Re-query framebuffer size each frame (cheap and simple) int fbw, fbh; glfwGetFramebufferSize(window, &fbw, &fbh); @@ -70,13 +117,7 @@ int main(void) { } glClear(GL_COLOR_BUFFER_BIT); - glUseProgram(program); - if (uResLoc != -1) { - glUniform2f(uResLoc, (float)g_fbWidth, (float)g_fbHeight); - } - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - + render_sand(&sim, program, vao, g_fbWidth, g_fbHeight); glfwSwapBuffers(window); } diff --git a/sand_sim.c b/sand_sim.c new file mode 100644 index 0000000..48ebbe2 --- /dev/null +++ b/sand_sim.c @@ -0,0 +1,134 @@ +// sand_sim.c +#include "sand_sim.h" +#include "gl_utils.h" // for create_compute_program_from_file + +#include +#include +#include + +static void init_textures(SandSim* sim) { + // tex_curr + glGenTextures(1, &sim->tex_curr); + glBindTexture(GL_TEXTURE_2D, sim->tex_curr); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, 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); + + // tex_next + glGenTextures(1, &sim->tex_next); + glBindTexture(GL_TEXTURE_2D, sim->tex_next); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, 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); +} + +static void upload_initial_state(SandSim* sim) { + size_t count = (size_t)sim->gridW * (size_t)sim->gridH; + uint8_t* data = calloc(count, 1); + if (!data) { + fprintf(stderr, "[sand_init] Out of memory for initial grid\n"); + exit(EXIT_FAILURE); + } + + // Seed RNG once (safe enough for now) + static bool seeded = false; + if (!seeded) { + seeded = true; + srand((unsigned int)time(NULL)); + } + + // Fill top 1/3 of grid with random sand vs air + for (int y = 0; y < sim->gridH / 3; ++y) { + for (int x = 0; x < sim->gridW; ++x) { + data[(size_t)y * sim->gridW + x] = (rand() & 1) ? 1 : 0; // 0=air,1=sand + } + } + + // Upload into tex_curr and tex_next + glBindTexture(GL_TEXTURE_2D, sim->tex_curr); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, sim->gridW, sim->gridH, + GL_RED_INTEGER, GL_UNSIGNED_BYTE, data); + + glBindTexture(GL_TEXTURE_2D, sim->tex_next); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, sim->gridW, sim->gridH, + GL_RED_INTEGER, GL_UNSIGNED_BYTE, data); + + glBindTexture(GL_TEXTURE_2D, 0); + + free(data); +} + +bool sand_init(SandSim* sim, int gridW, int gridH, const char* computeShaderPath) { + if (!sim || gridW <= 0 || gridH <= 0 || !computeShaderPath) { + fprintf(stderr, "[sand_init] Invalid arguments\n"); + return false; + } + + sim->gridW = gridW; + sim->gridH = gridH; + sim->tex_curr = 0; + sim->tex_next = 0; + sim->prog_sim = 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"); + return false; + } + + return true; +} + +void sand_step_gpu(SandSim* sim) { + if (!sim || !sim->prog_sim) return; + + glUseProgram(sim->prog_sim); + + // Set grid size uniform + GLint uGridLoc = glGetUniformLocation(sim->prog_sim, "u_gridSize"); + if (uGridLoc >= 0) { + glUniform2i(uGridLoc, 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); + + GLuint groupsX = (sim->gridW + 15) / 16; + GLuint groupsY = (sim->gridH + 15) / 16; + + glDispatchCompute(groupsX, groupsY, 1); + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + // Ping-pong + GLuint tmp = sim->tex_curr; + sim->tex_curr = sim->tex_next; + sim->tex_next = tmp; +} + +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; + } +} diff --git a/sand_sim.h b/sand_sim.h new file mode 100644 index 0000000..da0dde6 --- /dev/null +++ b/sand_sim.h @@ -0,0 +1,27 @@ +// sand_sim.h +#ifndef SAND_SIM_H +#define SAND_SIM_H + +#include +#include "glad/glad.h" + +// 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; +} 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); + +// Destroy all GL objects owned by the sim. +void sand_destroy(SandSim* sim); + +#endif // SAND_SIM_H diff --git a/shaders/sand_display.frag b/shaders/sand_display.frag new file mode 100644 index 0000000..4004316 --- /dev/null +++ b/shaders/sand_display.frag @@ -0,0 +1,26 @@ +#version 430 core + +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 + +void main() { + vec2 fragCoord = gl_FragCoord.xy; + + // Normalized coordinates + vec2 uv = fragCoord / u_resolution; + uv.y = 1.0 - uv.y; + // Map to grid coordinates + ivec2 cell = ivec2(uv * vec2(u_gridSize)); + cell = clamp(cell, ivec2(0), u_gridSize - ivec2(1)); + + uint v = texelFetch(u_state, cell, 0).r; + + vec3 color = (v == 1u) + ? vec3(0.9, 0.8, 0.1) // sand + : vec3(0.05, 0.05, 0.10); // background + + FragColor = vec4(color, 1.0); +} diff --git a/shaders/sand_step.comp b/shaders/sand_step.comp new file mode 100644 index 0000000..67bd284 --- /dev/null +++ b/shaders/sand_step.comp @@ -0,0 +1,42 @@ +#version 430 core + +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; + +uniform ivec2 u_gridSize; + +void main() { + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + if (pos.x >= u_gridSize.x || pos.y >= u_gridSize.y) { + 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; + } + + imageStore(state_next, pos, uvec4(next, 0u, 0u, 0u)); +}