Introduction

In this post, I will discuss about how did I implement double threading in my game engine to do game logic update and rendering at the same time. The following picture shows how my structure of my game engine is.

Double threading game engine
Double threading game engine

After all modules have been initialized, data have been loaded, I created an another thread to handle game play logic update and use the main thread to do the render job.

Why we need two threads to handle gameplay and render separately?

Two threads allow two progress that spends a lot of time to be executed synchronously. The two progress is the gameplay update and the frame rendering. Additionally, it ensures the application thread to run normally even when the render thread has a hard time rendering stuff. In this case, the FPS is very low, but the input query, physics update, and other modules are still having healthy behaviors in a correct time scale.

Data formats

The following two blocks shows my data formats for rendering a frame.

// Data required to render a frame
struct sDataRequiredToRenderAFrame
{
    // Clip plane data
    UniformBufferFormats::sClipPlane s_ClipPlane;
    // A list to store all lists
    std::vector<sPass> s_renderPasses;
    // Lighting data
    std::vector<cPointLight> s_pointLights;
    std::vector<cSpotLight> s_spotLights;
    cAmbientLight s_ambientLight;
    cDirectionalLight s_directionalLight;
};
// Data required to go through a pass (a rendering pipeline)
struct sPass
{
    // projection * view matrix
    UniformBufferFormats::sFrame FrameData;
    // Modle handle and transform information list
    std::vector<std::pair<Graphics::cModel::HANDLE, cTransform>> ModelToTransform_map;
    // Pass execution function
    void(*RenderPassFunction) ();
    sPass() {}
};

In Graphic.cpp, I stored an array of sDataRequiredToRenderAFrame to handle data swaping between application thread and render thread.

sDataRequiredToRenderAFrame s_dataRequiredToRenderAFrame[2];
auto* s_dataSubmittedByApplicationThread = &s_dataRequiredToRenderAFrame[0];
auto* s_dateRenderingByGraphicThread = &s_dataRequiredToRenderAFrame[1];

And I use two contional variables ( std::condition_variable in <condition_variable>) to do event handling between threads.

std::condition_variable s_whenAllDataHasBeenSubmittedFromApplicationThread;
std::condition_variable s_whenDataHasBeenSwappedInRenderThread;
std::mutex s_graphicMutex;

s_whenAllDataHasBeenSubmittedFromApplicationThread will be notifyed at the end of the while loop of the application thread.

s_whenAllDataHasBeenSubmittedFromApplicationThread will be notifyed after data is swapped in the render frame while loop in Graphic.cpp.

Submission functions

There are a lot of data that is required to be submitted to the render thread.

/** Submit function*/
void SubmitClipPlaneData(const glm::vec4& i_plane0, const glm::vec4& i_plane1 = glm::vec4(0, 0, 0, 0), const glm::vec4& i_plane2 = glm::vec4(0, 0, 0, 0), const glm::vec4& i_plane3 = glm::vec4(0, 0, 0, 0));

void SubmitLightingData(const std::vector<cPointLight>& i_pointLights, const std::vector<cSpotLight>& i_spotLights, const cAmbientLight& i_ambientLight, const cDirectionalLight& i_directionalLight);

void SubmitDataToBeRendered(const UniformBufferFormats::sFrame& i_frameData, const std::vector<std::pair<Graphics::cModel::HANDLE, cTransform>>& i_modelToTransform_map, void(*func_ptr)());

void SubmitTransformToBeDisplayedWithTransformGizmo(const std::vector< cTransform*>& i_transforms);

void ClearApplicationThreadData();