Multithreading.

Started by
35 comments, last by paulecoyote 19 years, 8 months ago
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.

---------------------------Hello, and Welcome to some arbitrary temporal location in the space-time continuum.

Advertisement
First of all, great post [grin]. Now you have me wanting to implement a thread interface in my project. Must...resist...more...features...

I've never really messed with multi-threading, so in your opinion, was it easy to implement? If it only took you 2 hours, it can't be too bad.

Also, how many different threads are you going to make? Right now, you have:

(1) Engine thread
(2) Rendering thread

If you ever added a networking interface, you would need a separate thread for that, as well (soley for collecting + processing messages). It could get *very* complicated, where you have a separate thread for everything (ie graphics, sound, networking, input, physics).

It would be cool to have someone write an article about multithreading, and how it can be used in game engines. It could cover a lot of material, I believe.

Our coding styles are shockingly similar...we even use the same function header comment. The only difference I see is that I almost always use HRESULTs.
Dustin Franklin ( circlesoft :: KBase :: Mystic GD :: ApolloNL )
I note that you have a local boolean in both of those functions which is never used except as a means of keeping a loop going forever. Why not just do while(true) :). Or did you make a mistake while pasting the code?

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Quote:Original post by circlesoft

I've never really messed with multi-threading, so in your opinion, was it easy to implement? If it only took you 2 hours, it can't be too bad.


It was pretty easy, but my Engine layout was already well structured to the point where I didn't have to change a whole lot; if you're directly calling D3D functions from your app, you'll have a hell of a lot more work than I do. I use a rendering queue system (objects never directly draw themselves), and I use a resource manager that controls the creation of all d3d resources (textures, shaders, buffers, etc.) if you do any of this on your own, outside of a centralized location, it will be very, very hard to do this right.

Quote:
Also, how many different threads are you going to make? Right now, you have:

(1) Engine thread
(2) Rendering thread

If you ever added a networking interface, you would need a separate thread for that, as well (soley for collecting + processing messages). It could get *very* complicated, where you have a separate thread for everything (ie graphics, sound, networking, input, physics).


Not really. For networking, you're already using unreliable data (thanks UDP!), so it doesn't matter if your updates are irregular. You're only going to get a few milliseconds (at most) delay in the updates, so you're fine.

Sound is going to be on a seperate thread (hard to do streaming without it), as well as networking. The main thread will deal with physics and input, as those are the things that have no hardware support and require the most CPU time.

Quote:
Our coding styles are shockingly similar...we even use the same function header comment. The only difference I see is that I almost always use HRESULTs.


Yeah, I'm not that fond of HRESULTs (though of course they're required for COM). I prefer using exceptions and error logging than checking for return values; that's kind of a pain in the ass, really.

Also, 9 times out of 10 if an error occurs, I either have some default behavior (if you try to load an invalid texture, it loads a default purple thing that says "INVALID TEXTURE" instead). Most other errors will stop the engine from working right, or will require falling back to some ugly behavior. If a certain feature of D3D isn't supported, I simply have a fallback that automatically picks up. Most other issues are going to be critical failures, and the program has to terminate anyway, so you might as well just log the error and terminate.

---------------------------Hello, and Welcome to some arbitrary temporal location in the space-time continuum.

Quote:Original post by Washu
I note that you have a local boolean in both of those functions which is never used except as a means of keeping a loop going forever. Why not just do while(true) :). Or did you make a mistake while pasting the code?


Nope, no mistake; I use level 4 warnings and break on errors, and while(true) generates a warning under level 4 in visual studio, whereas the local booleans do not. I suppose there's a 'cleaner' way to do it (while(!GraphicsEngine.GetRenderState(RS_PAUSED))), but I wasn't quite sure if I'd need to do any checks before exiting, so I left it as is.

---------------------------Hello, and Welcome to some arbitrary temporal location in the space-time continuum.

I like your idea, it is neat to keep your renderthread locked when updating mutual data. But from personal experience I find multithreading is GENERALLY a bad idea in games. The cost incurred to context switch threads is not worth it unless you have a very specific application such as yours (streaming in music for one). Also synchronizing time between threads, mutex or using semaphores to control access to dual sensitive data is a nightmare even on non-realtime apps (can anyone say Managed C++?)...

And on a single-processor system (most pcs?) you can simulate threading by using access flags and repeatedly polling your WAIT state. This is much faster than using two threads as it spares you the context switch. I had an app which was polling input in a different thread from the engine from the graphics, while I managed to synchronize all three in the end (so that no undefined memory behavior occured), I found it gave me a serious performance drop, especially as I was polling one thread to generate data for another redundantly (say waiting for up arrow to be pressed to move ur ship, but polling a hundred times a second without a press--switching contexts 300 times a second without any point). On a single-threaded system it had no trouble at all. As I said you can reclaim wasted CPU cycles (that you're passing off to the other threads) by using a polling flag.

And I'm not sure but I would imagine that nextgen consoles with multiprocessing (SMP) would have their APIs/kernel autodistribute and manage their tasks (like the Solaris model that 'attaches' kernel threads to applications), so making your own multithreader may be redundant.
All that said, I'm still very impressed by the renderthread locking you have demoed.
________________
"I'm starting to think that maybe it's wrong to put someone who thinks they're a Vietnamese prostitute on a bull"       -- Stan from South Park
Lab74 Entertainment | Project Razor Online: the Future of Racing (flashsite preview)
Direct3D can handle some multithreading sync on itself by using the D3DCREATE_MULTITHREADED flags when calling CreateDevice. I do not prevents you from using critical sections for your own code but it protected DirectX calls from locking when having a concurrent access.
whaoh seem my reply made bugging something on the forum (got an error message). Please moderator can you delete these ugly repeated posts ? Sorry for the disturbance.
Click post only once, not 9 times. If you get an error, go back to the post, and check before reposting.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

thanks, but I clicked only 2 times :(

This topic is closed to new replies.

Advertisement