Created
September 23, 2024 21:48
-
-
Save TheGag96/d1a78d19a03b312336a703004c34053a to your computer and use it in GitHub Desktop.
GPU-accelerated Conway's Game of Life
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Simp :: #import "Simp"; | |
Input :: #import "Input"; | |
#import "Basic"; | |
#import "Random"; | |
#import "Math"; | |
#import "Window_Creation"; | |
#import "GL"; | |
Render_Texture :: struct { | |
using #as texutre: Simp.Texture; | |
fbo_handle: GLuint; | |
} | |
window: Window_Type; | |
window_width, window_height: int; | |
framebuffer_textures: [3] Render_Texture; // Our boards will be triple-buffered. Is double-buffering sufficient? | |
prev_buffer: int; | |
shader_game_of_life: Simp.Shader; | |
main :: () { | |
window_init(); | |
quit := false; | |
while !quit { | |
Input.update_window_events(); | |
for Input.get_window_resizes() { | |
Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window. | |
if it.window == window { | |
should_reinit := (it.width != window_width) || (it.height != window_height); | |
window_width = it.width; | |
window_height = it.height; | |
if should_reinit randomize_board(); | |
} | |
} | |
for Input.events_this_frame { | |
if it.type == .QUIT then quit = true; | |
if it.type == { | |
case .KEYBOARD; | |
if it.key_pressed { | |
if it.key_code == .ESCAPE { | |
quit = true; | |
} | |
else if it.key_code == #char "R" { | |
randomize_board(); | |
} | |
} | |
} | |
} | |
window_draw_frame(); | |
} | |
} | |
GAME_OF_LIFE_SHADER :: #string END | |
OUT_IN vec2 TexCoords; | |
OUT_IN vec4 iterated_color; | |
#ifdef VERTEX_SHADER | |
in vec4 vert_position; | |
in vec4 vert_color; | |
in vec2 vert_uv0; | |
uniform mat4 projection; | |
void main() { | |
TexCoords = vec2(vert_uv0.x, 1.0-vert_uv0.y); // @Cleanup: Figure out what to do to avoid this vertical flip... sigh! | |
gl_Position = projection * vec4(vert_position.xy, 0.0, 1.0); | |
iterated_color = vert_color; | |
} | |
#endif // VERTEX_SHADER | |
#ifdef FRAGMENT_SHADER | |
out vec4 color; | |
uniform sampler2D diffuse_texture; | |
void main () { | |
vec2 tex_size = vec2(textureSize(diffuse_texture, 0)); | |
vec2 one_pixel = vec2(1.0) / tex_size; | |
float neighbors = | |
texture(diffuse_texture, TexCoords + vec2( 1.0, 0.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2( 1.0, 1.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2( 0.0, 1.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2(-1.0, 1.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2(-1.0, 0.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2(-1.0, -1.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2( 0.0, -1.0) * one_pixel).r + | |
texture(diffuse_texture, TexCoords + vec2( 1.0, -1.0) * one_pixel).r; | |
float this_cell = texture(diffuse_texture, TexCoords).r; | |
float value; | |
if (this_cell > 0.0) { | |
// Alive: Must be 2 or 3 neighbors to stay alive | |
value = step(2.0, neighbors) * (1.0 - step(4.0, neighbors)); | |
} | |
else { | |
// Dead: Must have 3 neighbors to become alive | |
value = float(neighbors == 3.0); | |
} | |
color = vec4(value, value, value, 1.0); | |
} | |
#endif // FRAGMENT_SHADER | |
END | |
window_init :: () { | |
#if OS == .WINDOWS { | |
// Windows.SetProcessDPIAware(); | |
// Windows is very bad at thread-switching by default unless you do this. Sad. | |
Windows :: #import "Windows"; | |
Windows.timeBeginPeriod(1); | |
} | |
window_width, window_height = 1920, 1080; | |
window = create_window(window_width, window_height, "Game of Life"); | |
window_width, window_height = Simp.get_render_dimensions(window); | |
Simp.set_render_target(window); | |
// Reserve space for front/back buffer textures | |
randomize_board(); | |
for * framebuffer_textures { | |
// @Hack: Simp currently has no way to let me change texture filtering without doing this directly. | |
glBindTexture(GL_TEXTURE_2D, it.gl_handle); | |
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_REPEAT); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); | |
glGenFramebuffers(1, *it.fbo_handle); | |
glBindFramebuffer(GL_FRAMEBUFFER, it.fbo_handle); | |
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, it.gl_handle, 0); | |
DRAW_BUFFERS :: GLenum.[GL_COLOR_ATTACHMENT0]; | |
glDrawBuffers(1, DRAW_BUFFERS.data); | |
} | |
shader_game_of_life.gl_handle = get_shader_program(GAME_OF_LIFE_SHADER); | |
shader_game_of_life.alpha_blend = false; | |
} | |
randomize_board :: () { | |
auto_release_temp(); | |
initial_frame: [..] u8; | |
initial_frame.allocator = temporary_allocator; | |
array_resize(*initial_frame, window_width * window_height * 4); | |
for 0..window_width*window_height-1 { | |
value: u8 = xx (0xFF * (random_get() % 2)); | |
initial_frame[it * 4 + 0] = value; | |
initial_frame[it * 4 + 1] = value; | |
initial_frame[it * 4 + 2] = value; | |
initial_frame[it * 4 + 3] = 0xFF; | |
} | |
bitmap: Simp.Bitmap = .{ | |
width = xx window_width, | |
height = xx window_height, | |
format = .RGBA8, | |
}; | |
for * framebuffer_textures { | |
if it_index == prev_buffer { | |
bitmap.data = initial_frame; | |
} | |
else { | |
bitmap.data = .[]; | |
} | |
Simp.texture_load_from_bitmap(it, *bitmap); | |
} | |
} | |
window_draw_frame :: () { | |
new_buffer := (prev_buffer + 1) % framebuffer_textures.count; | |
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_textures[new_buffer].fbo_handle); | |
glViewport(0, 0, xx window_width, xx window_height); | |
set_shader_for_game_of_life(*framebuffer_textures[prev_buffer]); | |
Simp.immediate_begin(); | |
Simp.immediate_quad( | |
.{0, cast(float) window_height}, | |
.{cast(float) window_width, cast(float) window_height}, | |
.{cast(float) window_width, 0}, | |
.{0, 0}, | |
); | |
Simp.immediate_flush(); | |
glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
glViewport(0, 0, xx window_width, xx window_height); | |
Simp.set_shader_for_images(*framebuffer_textures[new_buffer]); | |
Simp.immediate_begin(); | |
Simp.immediate_quad( | |
.{0, cast(float) window_height}, | |
.{cast(float) window_width, cast(float) window_height}, | |
.{cast(float) window_width, 0}, | |
.{0, 0}, | |
); | |
Simp.immediate_flush(); | |
Simp.swap_buffers(window, vsync = true); // @TODO: Proper frame-limit | |
prev_buffer = new_buffer; | |
reset_temporary_storage(); | |
} | |
// Copied from Simp/shader.jai, since it was #scope_module. | |
set_shader_for_game_of_life :: (texture: *Simp.Texture) { | |
if !context.simp context.simp = New(Simp.Immediate_State); | |
state := context.simp; | |
#if Simp.render_api == .METAL { | |
Simp.init_metal(); | |
} | |
Simp.immediate_flush(); | |
if state.current_shader != *shader_game_of_life { | |
Simp.immediate_set_shader(*shader_game_of_life); | |
Simp.set_projection(); | |
} | |
Simp.immediate_set_texture(texture); | |
} | |
// Copied from Simp/backend/gl.jai, since it was #scope_file. | |
get_shader_program :: (shader_text: string) -> GLuint { | |
LOG_BUFFER_SIZE :: 512; | |
make_shader_object :: (shader: string, prefix: string, shader_type : GLenum) -> GLuint { | |
shader_object := glCreateShader(shader_type); | |
//shader_str := temp_c_string(shader); | |
shaders: [2] *u8; | |
lengths: [2] s32; | |
shaders[0] = prefix.data; | |
shaders[1] = shader.data; | |
lengths[0] = xx prefix.count; | |
lengths[1] = xx shader.count; | |
glShaderSource(shader_object, 2, shaders.data, lengths.data); | |
glCompileShader(shader_object); | |
success : GLint; | |
glGetShaderiv(shader_object, GL_COMPILE_STATUS, *success); | |
if !success then { | |
log_data: [LOG_BUFFER_SIZE] u8; | |
glGetShaderInfoLog(shader_object, log_data.count, null, log_data.data); | |
log("%", to_string(log_data.data), flags=.ERROR); | |
return 0; | |
} | |
return shader_object; | |
} | |
// @MODIFIED GLES has a different version directive and needs to specify float precision. | |
#if OS == .ANDROID { | |
PREFIX_V :: #string END | |
#version 300 es | |
#define VERTEX_SHADER | |
#define OUT_IN out | |
precision mediump float; | |
END | |
PREFIX_F :: #string END | |
#version 300 es | |
#define FRAGMENT_SHADER | |
#define OUT_IN in | |
precision mediump float; | |
END | |
} else { | |
PREFIX_V :: #string END | |
#version 330 core | |
#define VERTEX_SHADER | |
#define OUT_IN out | |
END | |
PREFIX_F :: #string END | |
#version 330 core | |
#define FRAGMENT_SHADER | |
#define OUT_IN in | |
END | |
} | |
shader_v := make_shader_object(shader_text, PREFIX_V, GL_VERTEX_SHADER); | |
shader_f := make_shader_object(shader_text, PREFIX_F, GL_FRAGMENT_SHADER); | |
shader_p := glCreateProgram(); | |
glAttachShader(shader_p, shader_v); | |
glAttachShader(shader_p, shader_f); | |
glLinkProgram(shader_p); | |
success : GLint = 0; | |
glGetProgramiv(shader_p, GL_LINK_STATUS, *success); | |
if !success then { | |
log_data: [LOG_BUFFER_SIZE] u8; | |
glGetProgramInfoLog(shader_p, log_data.count, null, log_data.data); | |
log_error("%", to_string(log_data.data)); | |
return 0; | |
} | |
glDeleteShader(shader_v); | |
glDeleteShader(shader_f); | |
return shader_p; | |
} | |
#run { | |
#if OS == .WINDOWS { | |
WR :: #import "Windows_Resources"; | |
WR.disable_runtime_console(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment