Rookie Awards 2024 - Open for Entries!
Multi- threaded game engine
Share  

Multi- threaded game engine

by dierickxmathias on 31 May 2020 for Rookie Awards 2020

This was my graduation work for the major game development at Digital Arts and Entertainment. The entire project was finished in just seven weeks from the theoretical research and creating the engine design (3weeks) to creating a working multi-threaded 2D game engine using C++ and SDL (4weeks).

0 1109 0
Round of applause for our sponsors

Multi-threaded game engine

The engine features both a single and multi threaded mode so I could compare performance between both modes. Single threaded mode cuts out most of the multi-threaded managing overhead for a more fair comparison between the two modes.

I created a test scene with a lot of moving and static colliders all being updated in parallel. In order to achieve this I had to write a custom 2D collision engine that would work within my multi-threaded environment. 

Check out this video of comparing single and multi-threaded performance. Below you can find some stills from the video to view the design and some performance comparison charts. 

The above image shows an overview of how the engine is laid out. The engine has a couple of main systems that all work closely together.

Main/Render thread

This thread has a couple of responsibilities that make the engine work. It creates input tasks based on the user input, keeps track of time so the tasks can be executed with the correct delta time, it is responsible for cleaning up resources when no longer needed. Along these managing tasks, the main thread also does all of the rendering. This means the engine does it's logic in parallel but rendering is still done on the main thread. 

Thread Pool

This is a pool of worker threads ready to execute any tasks that get added into the task manager. Tasks get added by the main thread each frame after synchronizing logic and rendering threads. These tasks can consist of executing component logic, executing physics jobs loading and unloading scenes and much more. All worker threads run an infinite loop requesting tasks from the task manager. When no tasks are available the worker threads go to sleep and get woken up by the main thread once new tasks are added to the manager. It is key to keep the time with no tasks as low as possible.

Task Manager

The task manager works closely with both the main thread and the worker threads in the thread pool. It hold a number of possible tasks and is also responsible for syncing the different frame stages (pre-update, update, physics update, post-update, swapping past and future objects). Tasks are indices into the component map which is held and maintained for each scene in the game. They define which components have to be updated every stage, they will have a range of components to be updated in each task and the engine makes sure these components are in a continuous memory block to reduce cache misses.

Game and Scenes

As mentioned above, the game consists of a lot of scenes. Each scene then holds an entity vector and component map. The component map is used by almost all other systems in the engine. Task manager holds indices into this map, the worker threads execute tasks based on those indices and the render thread uses the component map to render the last frame.

Past and Future objects

One last system I want to touch on is the past and future object system. We cannot have the worker threads adjust data in objects while the render thread is reading this data to render the previous frame. This is solved by having a read-only past object that holds the results of the previous logical update and is used by the render thread to render the frame. The future object is a writable data set that is used in the logic update to generate the data needed for the renderer in the next frame. At the end of each frame the data from the future object has to be copied to the past objects and the cycle repeats. Due to this setup we are always rendering a frame behind the logic.

Result

As you can see from the graphs above, the difference in performance in the test scene between the single thread and multi threaded modes are substantial. There are however a couple of problems within the system I had no time to address due to the limited time given for the project. The main problem lies in scalability. The system has a major bottle-neck in the task manager as each worked thread is constantly trying to access the same system to request new tasks. This is an operation that only one thread can access at the same time. When we have too many threads in the system, most of the worker threads will spend a lot of their time waiting to get tasks through this bottle-neck. A possible solution would be moving this responsibility from the worker threads to the task manager itself. The task manager knows how many threads we have in the system and can therefore adjust the tasks it creates based on this knowledge and distribute tasks to the threads. On top of this a task stealing mechanism could be put in place where threads that finish their tasks early can steal some tasks off thread with lots of tasks left.  

Comments (0)

This project doesn't have any comments yet.