views:

693

answers:

2

I have an OpenGL ES application for the iPhone I am developing, being a port of a 2d-oriented application from another platform. I have chosen to render the graphics using OpenGL ES for performance reasons. However, the main application runs on a separate thread (due to the original application design), so from within my app delegate I do this:

- (void) applicationDidFinishLaunching:(UIApplication *)application {
    CGRect rect = [[UIScreen mainScreen] bounds];
    glView = [[EAGLView alloc] initWithFrame:rect];
    [window addSubview:glView];

    // launch main application in separate thread
    [NSThread detachNewThreadSelector:@selector(applicationMainThread) toTarget:self withObject:nil];
}

However, I notice that any calls within the applicationMainThread that try to render something to the screen do not render anything, until that thread terminates.

I set up the actual OpenGL ES context on the child application thread, not the UI thread. If I do this:

- (void) applicationMainThread {
    CGRect rect = [[UIScreen mainScreen] bounds];
    [glView createContext]; // creates the open GL ES context

    //Initialize OpenGL states
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glMatrixMode(GL_PROJECTION);
    glOrthof(0, rect.size.width, 0, rect.size.height, -1, 1);
    glMatrixMode(GL_MODELVIEW);

    Texture2D *tex = [[Texture2D alloc] initWithImage:[UIImage imageNamed:@"iphone_default.png"]];

    glBindTexture(GL_TEXTURE_2D, [tex name]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glDisable(GL_BLEND);
    [tex drawInRect:[glView bounds]];
    glEnable(GL_BLEND);

    [tex release];

    [glView drawView];
}

Then the texture is updated to the screen pretty much immediately, as I would expect.

However, if after the [glView drawView] call I add this one line:

[NSThread sleepForTimeInterval:5.0]; // sleep for 5 seconds

Then the screen is only updated after the 5 second delay completes. This leads me to believe that the screen only updates when the thread itself terminates (need to do more testing to confirm). This means that when I substitute the actual application code, which does multiple screen updates, none of the updates actually happen (leaving a white screen) until the application thread exits, not exactly what I wanted!

So - is there any way I can get around this, or have I done something obviously wrong?

+5  A: 

You have to be doing something obviously wrong, as multithreaded OpenGL rendering works just fine on iPhone. I can’t tell you what’s wrong with your code, but I can show you how we do it. It took me several iterations to get there, because the sample OpenGL code from Apple mashes everything together.

In the end I came up with three classes: Stage, Framebuffer and GLView. The Stage contains the game rendering logic and knows how to render itself to a framebuffer. The framebuffer class is a wrapper around the OpenGL framebuffer and renders to a renderbuffer or a EAGLDrawable. GLView is the drawable to render the framebuffer to, it contains all the OpenGL setup stuff. In the application entry point I create an OpenGL context, a GLView, a framebuffer that renders to this GLView and a Stage that renders using the framebuffer. The Stage update method runs in a separate thread and looks a bit like this:

- (void) mainLoop
{
    [fbuffer bind];
    [currentScene visit];
    [[EAGLContext currentContext]
        presentRenderbuffer:GL_RENDERBUFFER_OES];
    [fbuffer unbind];
}

In plain English, it binds the framebuffer, walks the game object graph (= renders the scene), presents the framebuffer contents on the screen and unbinds the framebuffer. The presentRenderbuffer call is a bit misplaced, it belongs somewhere higher in the design – the Stage should just render into framebuffer and let you do whatever you want to do with the framebuffer. But I could not find the right place, so I just left the call there.

Otherwise I am pretty much content with the design: all the classes are simple, coherent and testable. There’s also a Scheduler class that creates the thread and calls Stage’s mainLoop as fast as possible:

- (void) loop
{
    [EAGLContext setCurrentContext:context];
    while (running)
    {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        @synchronized (context)
        {
            [stage mainLoop];
        }
        [fpsCounter update];
        [pool release];
    }
}

- (void) run
{
    NSAssert(!running, @"Scheduler already running.");
    running = YES;
    [fpsCounter reset];
    context = [EAGLContext currentContext];
    [NSThread detachNewThreadSelector:@selector(loop)
        toTarget:self withObject:nil];
}

The game update thread is synchronized using the OpenGL context so that we can be sure that we don’t corrupt the context in the main thread. (Simple rule: All drawing has to be done in the game update loop or synchronized by the GL context.)

Hope that helps.

zoul
did you need to use a share group in order to share the context between threads?
Gabriel
Definitely helps, and indicates that my problem is in something else.
Gabriel
No shared group was needed, I don’t even know what that is.
zoul
A: 

Seems that I missed the bleeding obvious...

glViewport(0, 0, rect.size.width, rect.size.height);
glScissor(0, 0, rect.size.width, rect.size.height);

... and the updates appear as they should. I think what happened is without the viewport and scissor set on the child thread context which used a sharegroup (was set on the parent thread context), only when the child thread exited did the view update with the proper viewport, thus finally displaying my updates. Or something like that! (I'm still an OpenGLES newbie!)

Gabriel