Multi-threaded loading screen

Hi,

I thought it’d be fun to add a loading screen to my OpenGL game (as a lot of computations are done before the program is loaded, and waiting ~30secs with no reaction at all is pretty dull.)

I’ve worked out that the simplest way to do it would be to:

  • create a thread, where all initialization data is handled
  • in the main thread, in te display function, check, whether the initialization has finished, and if not, display something (eg. some text.) If it has finished - the game is loaded and the standard screen can be shown.

However, the problem is that the initialization stuff handled in the newly created thread uses glCreateList, glNewList, drawing onto that list, etc. As I figured out after reviewing some threads in the forum, my newly created thread doesn’t have the OpenGL context, also that I shouldn’t even attempt to share the same context over several threads.

Has anyone have the idea how to make this work? Or perhaps, the better way to create graphical loading screen? Thanks in advance!

Create one context per thread. You may let them share their lists (i.e. wglShareLists).
Now start loading/creating your resources in one thread (context 1), while you display the loading progress in the other(context 2).

If they share lists, be careful, not to make concurrent modifications of the same objects (textures, display lists, VBOs etc) at the same time.

Why not let the new thread display the loading window and keep the init stuff in the main thread? The new thread + the loading window get self terminated once init is done… Possible?

Here’s a simplified version:


struct{
	bool EverythingLoaded;
	int level_to_load;
	
	critical_section cs;
	ObjVector newStuff;
}LLL;

void thread2(){ // loads things from HDD to RAM
	startLoadingLevel(LLL.level_to_load);
	for(;;){
		if(noMoreDataToLoad())break;
		
		Object edx = LoadNextThingFromHDD();
		EnterCriticalSection(&LLL.cs);
		LLL.newStuff.add(edx);
		LeaveCriticalSection(&LLL.cs);		
	}
	LLL.EverythingLoaded=true;
}

void LoadLevel(int id){ // uploads things from RAM to GPU
	LLL.EverythingLoaded=false;
	LLL.level_to_load = id;
	LLL.newStuff = new ObjVector();
	InitializeCriticalSection(&LLL.cs);
	
	ObjVector newStuff = new ObjVector();
	
	startThread(thread2);
	
	for(;;){
		EnterCriticalSection(&LLL.cs);
		newStuff = CopyVectorEntries(LLL.newStuff);
		LLL.newStuff.clear();
		LeaveCriticalSection(&LLL.cs);
		
		foreach(newStuff,Object edx){
			switch(edx->type){
			case texture:
				uploadTexture(edx); // upload texture from RAM to GPU
				break;
			case vbo:
				uploadVbo(edx); // upload vbo from RAM to GPU
				break;			
		}
		newStuff.clear();
		
		if(LLL.EverythingLoaded)break;
		
		
		
		glClear(depthbit | colorbit);
		drawLoadingScreen();
		
		SwapBuffers();		
	}

}

HDD->RAM is the real bottleneck. Now, GLSL compilation can require processing of 1 shader/frame. Or more smartly: You can put timing measurement, if 10ms have been spent inside that “for(;;){” in LoadLevel(), return remaining object to LLL.newStuff and break out from the loop.

I found myself doing it the way skynet suggested, but I still haven’t managed to make it work (still get error 1280, which means the display lists are not being shared properly.) Guess I execute the wglShareLists the wrong way, or something else is wrong. Anyway, I do it that way:


// the display function of main thread
void display()
{
    if (!loadingDone)
    {
        threadInit();
        // display the loading stuff
    }
    else
    {
        // draw some else stuff
    }
}

// thread init function
void JobQueue::start()
{
    if (!started)
    {
        // remember display and render context for the main thread
        started = true;
        hMainDC = wglGetCurrentDC();
        hMainRC = wglGetCurrentContext();

        // (...) - create the thread
    }
}

// thread function
DWORD WINAPI ThreadFunction (LPVOID lpParam)
{
    // create render context based on main window's device context
    HGLRC hRC = wglCreateContext(queue->hMainDC);
    wglMakeCurrent(queue->hMainDC, hRC);
    wglShareLists(queue->hMainRC,hRC);

    // (...) - execute all jobs
    loadingDone = true;

    return 1;
}

The problem might be that I don’t understand gow wglShareLists and wglMakeCurrent work, and I use them the inapropriate way.

You mean like creating two separate windows? Yeah, that might work, though I’m not exactly sure that would look nice. Or perhaps both threads drawing onto the same window, and after finishing the loading, making a switch between their display functions? But again, this is roughly what skynet proposed, only without the wglShareLists call. However, I might find myself doing it that way, if what I’ve tried doesn’t work.

I guess, you problem is that you want to use the same DC in both threads. (Always check return values!!!)I doubt that you can bind two RCs to one DC at the same time. You may need to create a second “dummy” window for your second RC. It can be invisible and never needs to show up. Its just needed to make wglMakeCurrent() happy. (btw, this got a bit relaxed with WGL_ARB_create_context)

The call to wglShareLists() is ok, but I suggest doing the creation of those DC’s and RC’s at initialization time in the main thread and handing the handles over the loading thread when its being created. I have no idea, how a wglShareLists call works, if one of the RC’s is already doing something at the same time in a different thread…

Btw, I never claimed, that the loading must be done in that second thread. You can switch roles of both threads, if it suits you better. The main idea is just to have two separate threads doing their work in two separate contexts.