7
\$\begingroup\$

I'm trying to implement a multithreaded game loop. I already did that but had to use a few locks for that, which ruined the performance. After researching a bit I came up with this idea:

Instead of splitting the engines subsystems into different threads (e.g. physics, animation), all subsystems run on all threads. So when we got four CPUs, four threads are created, with each thread having one loop for all subsystems. So the single-core game loop is copied on all four threads. These game loops are controlled by one other loop, which sends messages (or 'jobs', 'tasks') to one of these threads (depending on their usage) according to user-input or scripts. This could be done with a double buffered command buffer.

Only the rendering loop is alone in a thread for maximum rendering performance. Now I'm thinking of the best way to communicate with the rendering loop. The best idea I could come up with is to again use a command buffer and swap it when the rendering loop is complete. That way the rendering loop doesn't have to wait for any of the loops and can go on rendering. If the game loop hasn't finished when the rendering loop swapped the buffer, all commands after that will be executed in the next frame of the rendering loop. To make sure that all objects will be drawn even if the game loop hasn't finished, the rendering loop holds all objects that will be drawn and draws them until it gets the command to stop drawing them.

Example

My goal is to make the engine scalable to cpu numbers and make it to use all cores. Is this a way to do that? What is the best approach to this and how are modern engines handling this?

\$\endgroup\$
  • \$\begingroup\$ A similar approach is also used by Valve's Source Engine. You might find some useful research with some google-fu. \$\endgroup\$ – akaltar Aug 24 '15 at 17:50
8
\$\begingroup\$

Best practices:

  • One central loop in main / rendering thread which also handles sound, network buffering etc. - basically, this centralises communication with OS and other threads.
  • All processor-intensive tasks (for example, mesh building, AI, physics) may be submitted ad-hoc, in bite-sized work units, to existing worker threads. These threads are kept alive and re-used over and over for different tasks. A typical implementation pattern is Thread Pooling.

You can see examples of this in modern platforms like Unity (WorkerThread), Xamarin (ThreadPool), and HTML5 (WebWorker), and even in older tech like Java 6 (various approaches).

If you find you are waiting around a lot on mutexes, you probably need to restructure things. Try to parallelise your tasks such that only one thread accesses a given piece of memory at a given time. For example, if one worker thread is continuously working on bite-sized dynamic lighting tasks, while another is working on AI, then you cannot have a conflict of interests. Alternatively, have 2 worker threads on AI, but let them consume work units that don't affect one another's results.

And yes, rendering should be continuous, regardless. In the main loop, prior to render, copy results of any completed worker thread tasks into your main data model if they are complete (see below). But don't try to read the same model data from the renderer, that you are currently reading/writing in worker threads. That's obviously a recipe for disaster.

I strongly discourage the use of multiple loops which you will need to periodically synchronise - in fact it just sounds like a recipe for pain.

Knowing when a work unit is complete & synchronising with main loop

The exact mechanism for notifiying when a job on another thread has completed depends on your configuration: platform, language, threading environment. Even within C++ there are various threading environments like pthreads, OpenMP threading, etc. You will either be notified by some sort of callback, or you will have to poll some condition from the main loop regularly to check if each work unit is complete. Search for the mechanism that is used in your threading environment. I suggest finding and running some simple examples first, to thoroughly grok threading concepts. If you want to go standard, go pthreads, but understand that it is a very low-level threading library. P.S. If you are using pthreads, the book Multi-Threaded Game Engine Design, 1st Edition, handles this on page 84 onward... you should be able to find the preview on google books.

\$\endgroup\$
  • \$\begingroup\$ But how do I synchronise the threads with the main loop? \$\endgroup\$ – Liess Jemai Aug 24 '15 at 22:54
  • 1
    \$\begingroup\$ @LiessJemai implicitly with submitted and completed tasks \$\endgroup\$ – ratchet freak Aug 25 '15 at 8:08
  • \$\begingroup\$ @LiessJemai ratchet freak is correct; fuller explanation added above. \$\endgroup\$ – Engineer Aug 25 '15 at 9:44
  • \$\begingroup\$ Is my answer below a way for synchronising these threads? \$\endgroup\$ – Liess Jemai Aug 25 '15 at 14:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.