Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Last active May 17, 2024 19:46
Show Gist options
  • Save partybusiness/13255ad293f8050394acfcb74ca75087 to your computer and use it in GitHub Desktop.
Save partybusiness/13255ad293f8050394acfcb74ca75087 to your computer and use it in GitHub Desktop.
Conway's Game of Life implemented as a compute shader in Godot
#[compute]
#version 450
// Invocations in the (x, y, z) dimension
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
// Current state of the Conway's game
layout(set = 0, binding = 0, std430) restrict buffer GameDataBuffer {
uint data[]; //32bits each
}
game_data;
// Texture size info
//layout(set = 0, binding = 1, std430) restrict buffer SizeDataBuffer {
// uint width;
// uint height;
//}
//size_data;
// The code we want to execute in each invocation
void main() {
//TODO instead of hard coding this, get it from size_data
uint width = 64;
uint height = 64;
uint rC = width / 32; //row count of how many uints are in each row
uint index = gl_GlobalInvocationID.x; // uniquely identifies this invocation across all work groups
uint source = game_data.data[index]; //source data from current uint
uint result = 0;
uint fs = height*rC; //full size of data
//all adjacent uints that we might need
uint below = game_data.data[(index+fs-rC)%(fs)];
uint above = game_data.data[(index+rC)%(fs)];
uint left = game_data.data[(index+1)%(fs)];
uint right = game_data.data[(index-1)%(fs)];
uint botleft = game_data.data[(index+fs-rC+1)%(fs)];
uint botright = game_data.data[(index+fs-rC-1)%(fs)];
uint topleft = game_data.data[(index+rC+1)%(fs)];
uint topright = game_data.data[(index+fs+rC-1)%(fs)];
//TODO maybe pull first and last bits out of the loop, for performance
for (int x =0;x<32;x++) { //loop through bits
uint count = 0;
if (x== 0) { //handle first bit as exception
count += ((above >> x) & 1) + ((above >> (x+1)) & 1) + ((topright >> 31 ) & 1);
count += ((below >> x) & 1) + ((below >> (x+1)) & 1) + ((botright >> 31 ) & 1);
count += ((source >> (x+1)) & 1) + ((right >> 31 ) & 1);
} else if (x==31) { //handle last bit as exception
count += ((above >> x) & 1) + ((above >> (x-1)) & 1)+ ((topleft >> 0 ) & 1);
count += ((below >> x) & 1) + ((below >> (x-1)) & 1)+ ((botleft >> 0 ) & 1);
count += ((source >> (x-1)) & 1) + ((left >> 0 ) & 1);
} else { //handle all bits in the middle
count += ((above >> x) & 1) + ((above >> (x+1)) & 1) + ((above >> (x-1)) & 1);
count += ((below >> x) & 1) + ((below >> (x+1)) & 1) + ((below >> (x-1)) & 1);
count += ((source >> (x+1)) & 1) + ((source >> (x-1)) & 1);
}
if ((count < 2) || (count > 3)) {
//leave it at 0
result = result & ~(1 << x);
} else {
if (count == 3) {
result |= 1 << x;
} else {
result = result | (source & (1 << x));
}
}
}
game_data.data[index] = result;
}
extends Node
@export var display_material:Material
@export var game_state:PackedByteArray
const width:int = 64
const height:int = 64
var rd := RenderingServer.create_local_rendering_device()
var buffer:RID
var pipeline:RID
var uniform_set:RID
var counter:float = 0.5
var processing:bool = false;
func _ready():
game_state.resize(floori(width*height))
game_state.fill(0)
#randomizes initial state
for i in range(0, game_state.size()):
game_state[i] = randi()%256 & randi()%256 #approx one quarter will be filled
#sets up display material
display_material.set_shader_parameter("width", width)
display_material.set_shader_parameter("height", height)
display_material.set_shader_parameter("game_data", game_state)
#sets up compute shader
buffer = rd.storage_buffer_create(game_state.size(), game_state)
# Load GLSL shader
var shader_file:RDShaderFile = load("res://life/compute_life.glsl")
var shader_spirv:RDShaderSPIRV = shader_file.get_spirv()
var shader:RID = rd.shader_create_from_spirv(shader_spirv)
var uniform:RDUniform = RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform.binding = 0 # this needs to match the "binding" in our shader file
uniform.add_id(buffer)
uniform_set = rd.uniform_set_create([uniform], shader, 0)
# the last parameter (the 0) needs to match the "set" in our shader file
# Create a compute pipeline
pipeline = rd.compute_pipeline_create(shader)
run_tick()
func _process(delta):
if processing: # submitted last frame
rd.sync()
processing = false
game_state = rd.buffer_get_data(buffer)
#TODO should I convert game_state to PackedInt32Array to use less space?
display_material.set_shader_parameter("game_data", game_state)
counter-=delta
if counter<0:
counter += 0.1
run_tick()
func run_tick():
#run one tick of the game
var compute_list := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
rd.compute_list_dispatch(compute_list, width*height/256, 1, 1)
rd.compute_list_end()
rd.submit()
processing = true
shader_type canvas_item;
uniform uint game_data[512]; // 64x64 divided by 8 bits each
uniform uint screenWidth = 64;
uniform uint screenHeight = 64;
void fragment() {
float sw = float(screenWidth / 8u); //width in bytes
//TODO if we pass PackedInt32Array switch 8u to 32u
float sh = float(screenHeight);
uint px = uint(floor(UV.x*sw));
uint py = uint(floor(UV.y*sh));
uint index = px + py * screenWidth / 8u;
//get which bit within this uint we need
uint shift = uint(UV.x * float(screenWidth)) - px * 8u;
//get colours
uint sample = game_data[index];
uint pixel = (sample >> shift) & 1u;
COLOR.rgb = vec3((float(pixel)));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment