Game of Life – a special case of a Cellular Automaton (CA), that works on the premise of using a set of simple rules that creates complex systems/behaviors. Game of Life is just one of the many interesting rule patterns that creates a very complex-looking system. Yet the defined rules are commonly just a set of simple rules.


Game Of Life Rule
The rule-set is pretty simple, there exist cells that can either be in the state dead or alive. Based on the number of cells neighboring a dead or alive cell, This will change the state to be either become dead, alive or nose change. See the following rule-set.
- Any living cell with fewer than two live neighbors dies as if caused by under-population.
- Any living cell with two or three neighbors lives on to the next generation.
- Any living cell with more than three live neighbors dies, as if by overpopulation.
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
Implementation
The following implementation uses a compute shader for both computing Game of Life cellular automaton, as well the color for each cell. The following is the compute shader. It is designed to use no condition. Because GPU does not handle conditional branching too well from a performance perspective. Even though this version requires more computation, than if it had used conditional branching.
Side Note
Since the implementation uses effectively a kernel of 3×3, it needs to use a double buffer for the cell state. Because otherwise, it would start to update region of the current cell states as other parts of the compute shader are reading the current state. This will yield corruption because of race condition. Thus a round-robin of two is required.
this->gameoflife_texture_width = width; this->gameoflife_texture_height = height; glBindFramebuffer(GL_FRAMEBUFFER, this->gameoflife_framebuffer); std::vector<uint8_t> textureData(this->gameoflife_texture_width * this->gameoflife_texture_height * sizeof(uint8_t)); /* Generate random game state. */ for (size_t j = 0; j < this->gameoflife_texture_height; j++) { for (size_t i = 0; i < this->gameoflife_texture_width; i++) { /* Random value between dead and alive cells. */ textureData[this->gameoflife_texture_width * j + i] = Random::range(0, 2); } } /* Create game of life state textures. */ for (size_t i = 0; i < this->gameoflife_state_texture.size(); i++) { glBindTexture(GL_TEXTURE_2D, this->gameoflife_state_texture[i]); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8UI, this->gameoflife_texture_width, this->gameoflife_texture_height, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, textureData.data()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glBindTexture(GL_TEXTURE_2D, 0); } /* Create render target texture to show the result. */ glBindTexture(GL_TEXTURE_2D, this->gameoflife_render_texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, this->gameoflife_texture_width, this->gameoflife_texture_height, 0 GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glBindTexture(GL_TEXTURE_2D, 0); /* */ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this->gameoflife_render_texture, 0); const GLenum drawAttach = GL_COLOR_ATTACHMENT0; glDrawBuffers(1, &drawAttach); /* Validate if created properly.*/ int frameStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (frameStatus != GL_FRAMEBUFFER_COMPLETE) { throw RuntimeException("Failed to create framebuffer, {}", frameStatus); } glBindFramebuffer(GL_FRAMEBUFFER, 0);
When updated the game of life cell state.
glUseProgram(this->gameoflife_program); /* Previous game of life state. */ glBindImageTexture( 0, this->gameoflife_state_texture[this->nthTexture % this->gameoflife_state_texture.size()], 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8UI); /* The resulting game of life state. */ glBindImageTexture( 1, this->gameoflife_state_texture[(this->nthTexture + 1) % this->gameoflife_state_texture.size()], 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8UI); /* The image where the graphic version will be stored as. */ glBindImageTexture(2, this->gameoflife_render_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); glDispatchCompute(std::ceil(this->gameoflife_texture_width / (float)this->localWorkGroupSize[0]), std::ceil(this->gameoflife_texture_height / (float)this->localWorkGroupSize[1]), 1); glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
Once the game of life cell state and final render image are rendered. it can be presented on the screen. Here the options are either triangle with texture or blit/copy it directly to the default framebuffer. That will present the final result when swapped.
/* Blit game of life render framebuffer to default framebuffer. */ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_READ_FRAMEBUFFER, this->gameoflife_framebuffer); glReadBuffer(GL_COLOR_ATTACHMENT0); /* Blit with nearset to retain the details of each of the cells states. */ glBlitFramebuffer(0, 0, this->gameoflife_texture_width, this->gameoflife_texture_height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); /* Update the next frame in round robin. */ this->nthTexture = (this->nthTexture + 1) % this->gameoflife_state_texture.size();
Source Code
The source code for this article can be found at the following links.

Free/Open software developer, Linux user, Graphic C/C++ software developer, network & hardware enthusiast.