• Register

Greater Good Games is an independent game development studio dedicated to raising money for worthy causes through our passion for developing "casual games for gamers." We are currently developing our first title, Break Blocks, which is scheduled to be released in the second half of 2011 for Windows and Mac.

Report RSS Rendering Threads

A tutorial on creating Rendering Threads on each major OS.

Posted by on - Intermediate Client Side Coding

Setting up a multithreaded OpenGL renderer, that is a single OpenGL context intended to run on a thread other than the main thread, is relatively straight forward on Windows and Mac. On Linux, however, it's a little more complex thanks to the X Windows Server system. To make matters worse, very few people have shared information about how they've accomplished this. So, let's jump right in and see how we can accomplish this common modern necessity called a Rendering Thread on each Platform.

On Windows, the only caveat is that the OpenGL Rendering Context must be created on the Rendering Thread:

#include <windows.h>
#include <process.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <stdio.h>

// Library Includes
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")

HINSTANCE g_hInstance;
HWND g_hWnd;
HDC g_hDC;
HGLRC g_hRC;

void RenderingThread(void* pData)
{
	// Create and set our OpenGL Render Context from the Window's Device Context
	g_hRC = wglCreateContext(g_hDC);
	wglMakeCurrent(g_hDC, g_hRC);

	// Initialize the rest of OpenGL here(glViewport, glGenPerspective, gluLookAt, etc.)

	while(1)
	{
		// Clear the Backbuffer, render OpenGL objects, and Swap Buffers here
	}
}

LRESULT CALLBACK peWindowsWinProc(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
	// Handle Messages with switch(uiMsg)
}

void CreateWindow(int32 i32WindowWidth, int32 i32WindowHeight, wchar* uniWindowName)
{
	WNDCLASSEX winclass;
	PIXELFORMATDESCRIPTOR pixelformatdescriptor;
	int pixelformat;
	
	// Initialize the Window Class object
	winclass.cbSize			= sizeof(WNDCLASSEX);
	winclass.hInstance		= g_hInstance;
	winclass.style			= CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	winclass.cbClsExtra		= 0;
	winclass.cbWndExtra		= 0;
	winclass.hCursor		= LoadCursor(NULL, IDC_ARROW);
	winclass.lpszMenuName	= NULL;
	winclass.hbrBackground	= NULL;
	winclass.lpszClassName	= uniWindowName;
	winclass.lpfnWndProc	= &amp;WinProc;

	// We must register the class with windows
	if(!RegisterClassEx(&amp;winclass))
		return;

	// Make sure the window has a drawable region of the specified size
	RECT winrect = {0, 0, i32WindowWidth, i32WindowHeight};
	AdjustWindowRectEx(&amp;winrect, WS_OVERLAPPEDWINDOW &amp; ~WS_THICKFRAME, FALSE, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE);
	g_hWnd = CreateWindowEx(WS_EX_APPWINDOW | WS_EX_WINDOWEDGE, uniWindowName, uniWindowName,
										WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_VISIBLE | WS_OVERLAPPEDWINDOW &amp; ~WS_THICKFRAME, 0, 0, 
										winrect.right - winrect.left, winrect.bottom - winrect.top, 0, 0, 
										g_hInstance, 0);

	// Show and Draw the window
	ShowWindow(g_hWnd, SW_NORMAL);
	UpdateWindow(g_hWnd);

	// Grab the Window's Device Context
	g_hDC = GetDC(g_hWnd);

	// Set Pixel Format to 32-bit with 24-bit Depth buffer
	peMemset(&amp;pixelformatdescriptor, 0, sizeof(pixelformatdescriptor));
	pixelformatdescriptor.nSize			= sizeof(PIXELFORMATDESCRIPTOR);
	pixelformatdescriptor.nVersion		= 1;													
	pixelformatdescriptor.dwFlags		= PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; 
	pixelformatdescriptor.dwLayerMask	= PFD_MAIN_PLANE;
	pixelformatdescriptor.iPixelType	= PFD_TYPE_RGBA;
	pixelformatdescriptor.cColorBits	= 32;
	pixelformatdescriptor.cDepthBits	= 24;
	pixelformatdescriptor.cAccumBits	= 0;
	pixelformatdescriptor.cStencilBits	= 0;
	
	// Get and set a pixel format that best matches our descriptor
	if(pixelformat = ChoosePixelFormat(g_hDC, &amp;pixelformatdescriptor))
		SetPixelFormat(g_hDC, pixelformat, &amp;pixelformatdescriptor);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR strCommandLine, int iShowCommand)
{
	g_hInstance = hInstance;

	// Create our Window
	CreateWindow(1024, 768, TEXT("Rendering Thread Example"));

	// Create the Rendering Thread
	HANDLE thread = _beginthread(RenderingThread, 0, 0);

	// Windows Message Pump
	while(1)
	{
		if(PeekMessage(&amp;message, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&amp;message);
			DispatchMessage(&amp;message);
		}
		else
		{
			// Update Game Objects
		}
	}
}

On Mac, the Window set up code is a bit different, but the OpenGL initialization is similar: create a Window, get a Pixel Format for it, and use those to create an OpenGL Render Context on the Rendering Thread.

#include <Carbon/Carbon.h>
#include <stdio.h>
#include <AGL/agl.h>

WindowPtr g_pWindow;
EventLoopTimerRef g_mainLoopTimer;
EventHandlerUPP g_eventHandlerUPP;
EventHandlerRef g_eventHandlerRef;
AGLPixelFormat g_pixelFormat;
AGLContext g_aglContext;

void* RenderingThread(void* pArguments)
{
	// Create our OpenGL Render Context from the Pixel Format(which is stored as the Device Context)
	g_aglContext = aglCreateContext(g_pixelFormat, NULL);
	aglSetDrawable(g_aglContext, (AGLDrawable)GetWindowPort(g_pWindow));

	// Set our OpenGL Render Context
	aglSetCurrentContext(g_aglContext);

	// Initialize the rest of OpenGL here(glViewport, glGenPerspective, gluLookAt, etc.)

	while(1)
	{
		// Clear the Backbuffer, render OpenGL objects, and Swap Buffers here
	}
}

static pascal void GameLoop(EventLoopTimerRef theTimer, void* userData)
{
	// Update Game Objects
}

static pascal OSStatus EventHandler(EventHandlerCallRef pEventHandler, EventRef pEvent, void* pUserData)
{
	return noErr;
}

void InitEventHandler()
{
	EventTypeSpec	events[9] = {kEventClassCommand, kEventCommandProcess,	// command events
								 kEventClassKeyboard, kEventRawKeyDown,		// key down events
								 kEventClassKeyboard, kEventRawKeyUp,		// key up events
								 kEventClassKeyboard, kEventRawKeyModifiersChanged,	// modifiers changed event
								 kEventClassMouse, kEventMouseMoved,
								 kEventClassMouse, kEventMouseDragged,
								 kEventClassMouse, kEventMouseUp,
								 kEventClassMouse, kEventMouseDown,
								 kEventClassMouse, kEventMouseWheelMoved};
	
	g_eventHandlerUPP = NewEventHandlerUPP(EventHandler);
	
	InstallEventHandler(GetApplicationEventTarget(), g_eventHandlerUPP, 9, events, nil, &amp;g_eventHandlerRef);
}

void SetMainLoopEventTimer(EventLoopTimerProcPtr myMainLoopFunc)
{
	EventLoopRef mainLoop = GetMainEventLoop();
	
	EventLoopTimerUPP eventLoopTimerUPP = NewEventLoopTimerUPP(myMainLoopFunc);
	
	InstallEventLoopTimer(mainLoop, kEventDurationNoWait, kEventDurationMillisecond, eventLoopTimerUPP, nil, &amp;g_mainLoopTimer);
	
	DisposeEventLoopTimerUPP(eventLoopTimerUPP);
}

void CreateWindow(int32 i32WindowWidth, int32 i32WindowHeight, char* uniWindowName)
{
	GLint windowsattribs[] = {AGL_RGBA, AGL_DOUBLEBUFFER, AGL_DEPTH_SIZE, 16, AGL_ALL_RENDERERS, AGL_ACCELERATED, AGL_NO_RECOVERY, AGL_NONE};
	Rect windowRect;
	
	windowRect.left		= 10;
	windowRect.top		= 50;
	windowRect.right	= windowRect.left + i32WindowWidth;
	windowRect.bottom	= windowRect.top + i32WindowHeight;	

	// Create and show our Window
	CreateNewWindow(kDocumentWindowClass, kWindowStandardFloatingAttributes|kWindowStandardHandlerAttribute|kWindowInWindowMenuAttribute, &amp;windowRect, &amp;g_pWindow);
	ShowWindow(g_pWindow);

	// Register our Window to receive Events
	InitEventHandler();

	// Grab a fitting Pixel Format(used to create the OpenGL Render Context)
	g_pixelFormat = aglChoosePixelFormat(NULL, 0, windowsattribs);
}

int main(int argc, char** argv)
{
	pthread_t threadID;

	// Create our Window
	CreateWindow(1024, 768, "Rendering Thread Example");

	// Create our Rendering Thread
	pthread_create(&amp;threadID, NULL, RenderingThread, 0);

	// Run the Game Update Loop
	SetMainLoopEventTimer(GameLoop);
	RunApplicationEventLoop();
}

On Linux, we need 3 separate objects to create an OpenGL Render Context in our Rendering Thread(rather than just the 1 that both Windows and Mac use): an X Display pointer, a Frame Buffer config object, and a GLX Window pointer. We also need to make sure we're always synchronized with X Windows when calling GLX functions(that is, OpenGL X Windows commands, all prefixed with glX), which is accomplished by Locking and Unlocking the X Display.

#include <stdio.h>
#include <GL/glx.h>
#include <GL/glext.h>

Display* g_pDisplay;
XVisualInfo* g_pVisual;
Window g_pWindow;
GLXFBConfig g_fbConfig;
GLXWindow g_glxWindow;
GLXContext g_glxContext;

void* RenderingThread(void* pArguments)
{
	XLockDisplay(g_pDisplay);
	{
		// Create our OpenGL Render Context from the Display and Visual
		g_glxContext = glXCreateNewContext(g_pDisplay, g_fbConfig, GLX_RGBA_TYPE, None, GL_TRUE);

		// Set our OpenGL Render Context
		glXMakeContextCurrent(g_pDisplay, g_glxWindow, g_glxWindow, g_glxContext);
	}
	XUnlockDisplay(g_pDisplay);

	// Initialize the rest of OpenGL here(glViewport, glGenPerspective, gluLookAt, etc.)

	while(1)
	{
		// Clear the Backbuffer, render OpenGL objects, and Swap Buffers here(make sure you XLockDisplay before any glX functions)
	}
}

void CreateWindow(int32 i32WindowWidth, int32 i32WindowHeight, char* uniWindowName)
{
	GLint windowattribs[] = {GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, GLX_DOUBLEBUFFER, 1, GLX_RED_SIZE, 1, GLX_GREEN_SIZE, 1, GLX_BLUE_SIZE, 1, None};
	XSetWindowAttributes attribs;
	int32 dummy;

	// Get an X Display pointer
	g_pDisplay = XOpenDisplay(0);

	// Grab a list of available Frame Buffer configs
	GLXFBConfig* fbConfigs = glXChooseFBConfig(g_pDisplay, DefaultScreen(g_pDisplay), windowattribs, &amp;dummy);
	g_fbConfig = fbConfigs[0];
	g_pVisual = glXGetVisualFromFBConfig(g_pDisplay, g_fbConfig);

	// Setup Window attributes
	attribs.colormap = XCreateColormap(g_pDisplay, RootWindow(g_pDisplay, DefaultScreen(g_pDisplay)), g_pVisual->visual, AllocNone);
	attribs.event_mask = ExposureMask | KeyPressMask | StructureNotifyMask;
	attribs.border_pixel = 0;

	// Create the X Window
	g_pWindow = XCreateWindow(g_pDisplay, RootWindow(g_pDisplay, DefaultScreen(g_pDisplay)),
		0, 0, i32WindowWidth, i32WindowHeight, 0, g_pVisual->depth, InputOutput, g_pVisual->visual,
		CWColormap | CWEventMask, &amp;attribs);

	// Show the X Window
	XMapWindow(g_pDisplay, g_pWindow);

	// Get the GLX Window binding
	g_glxWindow = glXCreateWindow(g_pDisplay, g_fbConfig, g_pWindow, 0)
}

int main(int argc, char** argv)
{
	pthread_t threadID;

	// Make X Windows thread-safe(must be done first)
	XInitThreads();

	// Create our Window
	CreateWindow(1024, 768, "Rendering Thread Example");

	// Create our Rendering Thread
	pthread_create(&amp;threadID, NULL, RenderingThread, 0);

	// X Windows Message Pump
	while (1)
	{
		while(XPending(g_pDisplay))
		{
			XNextEvent(g_pDisplay, &amp;event);
			switch(event.type)
			{
				// Handle Events
			}
		}
	}
}

Obviously each setup has its pros and cons, but tracking down how all of the elements in Linux interact was a real nightmare. Hopefully you will not have to experience the same issues with some help from this tutorial!

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: