I decided that some of the things I've been trying to do with my engine are a pain in the ass without multithreading, so I went and took some time today (about 2 hours or so) to implement it.
Short story: Multithreading definitely eased some performance bottlenecks in certain areas, but it's kind of tricky to make sure that you don't totally fuck something up. There was a very, very slight performance loss from the initial setup (I went from about 550fps average to about 530), but it was an overall improvement (frame rate never dips below 400fps now, whereas it used to drop dowwn as low as 200 during normal rendering, and to fractions when loading things).
Long story:
There's one thing to consider when using D3D in a multithreaded environment that's more important than anything else: If you try to create or destroy a resource while D3D has a chance to access it, you're probably going to make your program blow up.
Fortunately, it's fairly easy to resolve this. Here's what I did:
//-----------------------------------
// LockRenderThread()
// Locks the rendering thread
//-----------------------------------
void CEther::LockRenderThread()
{
bool Locked = false;
GraphicsEngine.RequestLock();
while(!Locked)
{
if(GraphicsEngine.GetRenderState(RS_PAUSED))
return;
Sleep(0); // Give the other thread time to respond.
}
}
//-----------------------------------
// UnlockRenderThread()
// Unlocks the rendering thread
//-----------------------------------
void CEther::UnlockRenderThread()
{
if(GraphicsEngine.IsStateLocked(RS_PAUSED)) // We can't unlock at this point; most likely we're still loading.
return;
bool Locked = true;
GraphicsEngine.RequestUnlock();
while(Locked)
{
if(!GraphicsEngine.GetRenderState(RS_PAUSED))
return;
Sleep(0); // Give the other thread time to respond.
}
}
Is in the main engine. Here's the thread that actually does the rendering:
//-----------------------------------
// GraphicsProc()
// Handles rendering.
//-----------------------------------
DWORD WINAPI GraphicsProc(void* Obj)
{
assert(Obj);
bool RunningThread = true;
while(RunningThread)
{
if(GraphicsEngine.GetRenderState(RS_SHUTDOWN))
return 0;
if(!GraphicsEngine.GetRenderState(RS_PAUSED))
{
if(GraphicsEngine.IsLocked())
GraphicsEngine.SetRenderState(RS_PAUSED, true);
}
else
{
if(!GraphicsEngine.IsLocked())
GraphicsEngine.SetRenderState(RS_PAUSED, false);
}
if(GraphicsEngine.GetRenderState(RS_PAUSED))
continue;
// Begin rendering
RSLT rslt = GraphicsEngine.BeginScene();
if(rslt != SUCCESS)
{
PostQuitMessage(0);
break;
}
//...snipped drawing routines.
// Render the scene.
rslt = GraphicsEngine.EndScene();
if(rslt != SUCCESS)
{
PostQuitMessage(0);
break;
}
Sleep(0);
}
return 0;
}
RequestLock() sets the Lock variable of the GraphicsEngine class to true, but NOT the RS_PAUSED render state. That way, we can wait until the RS_PAUSED render state does get set, and return control of the program once it does.
By doing this, we can be 100% sure that between LockRenderThread() and UnlockRenderThread(), D3D will be doing absolutely nothing. This is key.
Now, when loading a new resource (Vertex Buffer, texture, shader, whatever), you simply:
- Call LockRenderThread()
- Make your changes.
- Call UnlockRenderThread()
Note: My version displays a blank screen if the update takes a long time since it renders nothing. You could easily have some default text that could be rendered ("Loading...please wait") or something as well.
The biggest benefit that I'm getting out of running the rendering on a seperate thread is that I can better control the resolution of different things, and I can very easily control how much CPU time is being dedicated to non-graphics work.
In most time-based rendering loops, you suddenly drop to only updating at a resolution of about 16/17 ms when you have v-synch on. Doing it this way, I can still have a resolution that's a fraction of that, even if my frame rate happens to dip to 20fps or something.
It's important that you don't make any of your rendering functions dependent on the execution time of your rendering loop. For example, if you have a shader that takes a constant representing the time, poll the time from your main thread, not the rendering thread; this will keep everything in synch, even if the video card starts chugging along.
Some people may think that this is kind of worthless, but just look at all the plans for both new game consoles and new pcs ready to ship: Multi processors. If you want to take advantage of that, it's important to use multithreading, or else you'll find that you can't really take advantage of the additional processing power at your finger tips.