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 7695f50..5db11e8 100755 Binary files a/fallingCand and b/fallingCand differ 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)); +}