Cocoa message loop? (vs. windows message loop)
Asked Answered
J

2

15

While trying to port my game engine to mac, I stumble upon one basic (but big) problem. On windows, my main code looks like this (very simplified):

PeekMessage(...) // check for windows messages
switch (msg.message)
{
case WM_QUIT: ...;
case WM_LBUTTONDOWN: ...;
...
}
TranslateMessage(&msg);
DispatchMessage (&msg);

for (std::vector<CMyBackgroundThread*>::iterator it = mythreads.begin(); it != mythreads.end(); ++it)
{
  (*it)->processEvents();
}

... do other useful stuff like look if the window has resized and other stuff...

drawFrame(); // draw a frame using opengl
SwapBuffers(hDC); // swap backbuffer/frontbuffer

if (g_sleep > 0) Sleep(g_sleep);

I already read that this is not the mac way. The mac way is checking for some kind of event like the v-sync of the screen to render a new frame. But as you can see, I want to do a lot more than only rendering, I want other threads to do work. Some threads need to be called faster than every 1/60th of a second. (by the way, my thread infrastructure is: thread puts event in a synchronized queue, main thread calls processEvents which handles the items in that synchronized queue within the main thread. This is used for network stuff, image loading/processing stuff, perlin noise generation, etc... etc...)

I would love to be able to be able to do this a similar way, but very little information is available about this. I wouldn't mind putting the rendering on a v-sync event (I will implement this also on windows), but I would like to have a bit more responsiveness on the rest of the code.

Look at it this way: I would love to be able to process the rest while the GPU is doing stuff anyway, I do not want to wait for a v-sync to then start doing stuff that should already have been processed to only then start sending stuff to the GPU. Do you understand what I mean?

If I need to look at this from a completely different point of view, please tell me.

If I need to read books/guides/tutorials/anything for this, please tell me what to read!

I'm no cocoa developer, I'm no object-c programmer, my game engine is entirely in C++, but I know my way around xcode enough to make a window appear and show what I draw inside that window. It just doesn't update like my windows version because I don't know how to get it right.

Update: I even believe that I need some kind of loop, even if I want to synchronize on the vertical retrace. The OpenGL programming on MacOSX documentation shows that this is done by setting the SwapInterval. So if I understand correctly, I will always need some kind of loop when rendering real time on the Mac, using the swapInterval setting to lower power usage. Is this true?

Juryman answered 11/7, 2011 at 11:51 Comment(0)
J
20

Although I can't be the only person in the world trying to achieve this, nobody seems to want to answer this (not point at you stackoverflow guys, just pointing at mac developers who know how it works). That's why I want to change this typical behaviour and help those who are looking for the same thing. In other words: I found the solution! I still need to further improve this code, but this is basically it!

1/ When you need a real own loop, just like in windows, you need to take care of it yourself. So, let cocoa build your standard code, but take main.m (change it to .mm for c++ if necessary, which is in this example because I used c++) and remove the one and only line of code. You will not allow cocoa to set up your application/window/view for you.

2/ Cocoa normally creates an AutoreleasePool for you. It helps you with memory management. Now, this will no longer happen, so you need to initialize it. Your first line in the main function will be:


    [[NSAutoreleasePool alloc] init];

3/ Now you need to set up your application delegate. The XCode wizard has already set up a AppDelegate class for you, so you just need to use that one. Also the wizard has already set up your main menu and probably called it MainMenu.xib. The default ones are fine to get started. Make sure you #import "YourAppDelegate.h" after the #import <Cocoa/Cocoa.h> line. Then add the following code in your main function:


    [NSApplication sharedApplication];
    [NSApp setDelegate:[[[YourAppDelegate alloc] init] autorelease]];
    [NSBundle loadNibNamed:@"MainMenu" owner:[NSApp delegate]];
    [NSApp finishLaunching];

4/ Mac OS will now know what the application is about, will add a main menu to it. Running this won't do much anyhow, because there is no window yet. Let's create it:


    [Window setDelegate:[NSApp delegate]];
    [Window setAcceptsMouseMovedEvents:TRUE];
    [Window setIsVisible:TRUE];
    [Window makeKeyAndOrderFront:nil];
    [Window setStyleMask:NSTitledWindowMask|NSClosableWindowMask];

To show what you can do here, I added a title bar and enabled the close button. You can do a lot more, but I also need to study this first.

5/ Now if you run this, you might get lucky and see a window for a microsecond, but you will probably see nothing because... the program is not in a loop yet. Let's add a loop and listen to incoming events:


    bool quit = false;
    while (!quit)
    {
        NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES];
        switch([(NSEvent *)event type])
        {
        case NSKeyDown:
            quit = true;
            break;
        default:
            [NSApp sendEvent:event];
            break;
        }
        [event release];
    }

If you run this, you will see the window appear and it will stay visible until you just press a key. When you press the key, the application will quit immediately.

This is it! But beware... check out the Activity Monitor. You will see that your application is using 100% CPU. Why? Because the loop doesn't put the CPU in sleep mode. That's up to you now. You can make it easy on yourself and put a usleep(10000); in the while. This will do a lot, but isn't optimal. I will probably use the vertical-sync of opengl to make sure the CPU isn't overly used, and I will also wait for events from my threads. Maybe I will even check out the passed time myself and do a calculated usleep to make the total time per frame so that I have a decent frame rate.

For help with Opengl:

1/ Add the cocoa.opengl framework to the project.

2/ Before [Window...] put:


    NSOpenGLPixelFormat *format = [[NSOpenGLPixelFormat alloc] initWithAttributes:windowattribs];
    NSOpenGLContext *OGLContext = [[NSOpenGLContext alloc] initWithFormat:format shareContext:NULL];
    [format release];

3/ After [Window setDelegate:...] put:


    [OGLContext setView:[Window contentView]];

4/ Somewhere after the window handling, put:


    [OGLContext makeCurrentContext];

5/ After [event release], put:


    glClearColor(1, 0, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    [OGLContext flushBuffer];

This will clear your window to fully red on a framerate of 3300 on my mac book pro 2011.

Again, this code is not complete. When you click on the close button and reopen it, it will no longer work. I probably need to hook events for that and I don't know them yet (after further research, these things can be done in the AppDelegate). Also, there are probably lots of other things to do/consider. But this is a start. Please feel free to correct me/fill me in/...

If I get it fully right, I might set up a tutorial web page for this if no one beats me to it.

I hope this helps you all!

Juryman answered 12/7, 2011 at 13:36 Comment(10)
You shouldn't do this. There is no good reason to spin the CPU in your game; all it does is waste battery life on laptops and make the fans go crazy on desktop systems.Psych
Please don't -1 me for this. Game developers have perfectly good reasons to do this:Juryman
Please don't -1 me for this, game developers have perfectly good reasons to do this: 1. this way, they can put everything to max to check out if their optimizations have worked (you can see a framerate raise from 2000 to 2050, but you can't with one maxed out). 2. You have total control on how everything is handled. And 3. As I said in my text myself, this is NOT the way to publish your game. You need to put in events/sleeps/whatever to make the CPU not overly used. Syncing with v-sync is one great solution for example. This loop remains terribly useful, even with those wait-methods.Juryman
If this is what worries you: Using sleep is by the way a great way to save on CPU cycles. You don't need events to save on CPU cycles, by saying sleep, you force the CPU to not work for you for that amount of time. It's perfectly ok. I can for example make my loop so that it renders every 1/100th of a second, but also checks on my threads every 1/20th of a second, and make sure that when those things are finished, the CPU gets to sleep for the remaining time by clocking my work and sleeping out the rest. This is a great way to do this.Juryman
On the Mac, the way to test your optimisations is to use the excellent performance tools provided by Apple (Instruments being the primary one these days).Psych
As for sleep(), you’re missing the point entirely. This isn’t about saving CPU cycles, it’s about not running when you don’t need to. If you want to do work several times a second, use a timer (that’s what they’re there for), though as the Apple document says, for actual rendering you should use CVDisplayLink.Psych
I don't think any FPS game or other game on that level can do with timers. Every game developer on big game projects knows that timers = lagging = everything is always too late. I agree with you on app-level and pinball or point&click adventure games and the like. And yes... sleep = saving CPU = battery life. The above test with a simple usleep(10000); makes it use 2% CPU with a framerate of 90. Again: a vsync-option MUST be available. But I tell you: I fully agree with you when we talk about normal applications and simple games.Juryman
In the days of the 16-bit micro, you might have been right. However, there's no good reason you can't use a repeating timer to e.g. call your "threads" twenty times a second if that's what you want to do. If you're saying that you'll get called a little bit less than twenty times a second because of scheduling, well, yes, for sure. But that's true of your looping approach also because you'll get pre-empted and/or hit synchronisation points that block your thread.Psych
All high performance games busy wait between frames. Otherwise there's no guarantee that you won't drop frames. There's not really such a thing as "not running when you don't need to" if you have time to process another frame, most users will expect you to do it.Cate
Just a heads up, [NSApp setDelegate:[[[YourAppDelegate alloc] init] autorelease]]; was the culprit when my AppDelegate's applicationShouldTerminate: wasn't being called. You need to retain the AppDelegate (I used a global variable) or it'll be released by the end of the pool.Unhandy
P
1

You could use a Core Foundation runloop instead, and have event sources for your “threads” so that the run loop will wake up and call their processEvents() methods. This is an improvement over your polling code, since the run loop will let the thread sleep if there are no events waiting.

See CFRunLoopSourceCreate(), CFRunLoopSourceSignal() and CFRunLoopWakeUp() for more information.

Note that, if your application is built on top of the Cocoa framework, you can (and probably should) use the default NSRunLoop, but this isn’t a problem because you can get the underlying Core Foundation CFRunLoop by sending it a -getCFRunLoop message.

Psych answered 11/7, 2011 at 13:15 Comment(4)
I will look into this. The first concern that comes into my mind, is how to keep things cross-platform when using this. Of course, there's always the possibility that I change the way my threads work on windows too, because this feels like a great solution. The disadvantage however is that I will not be able to run the application on maximum usage (just for development testing, not for release version). On a message loop override, I can put everything to maximum usage, to see if my optimizations work.Juryman
You might also care to look at <developer.apple.com/library/mac/#qa/qa1385/_index.html>, which deals with the separate but related issue of OpenGL and frame rendering.Psych
I should perhaps add that an alternative is to use Carbon APIs (which bear more resemblance to the Windows API). If you do that, though, you won’t be able to build a 64-bit version of your application, because Carbon isn’t supported on the 64-bit ABI on OS X. Nor would that work on iOS (whereas using Core Foundation will work there too).Psych
Yes, I know about Carbon, don't want to use it because of the 64-bit issue. But as you can see by my own answer to this question, digging somewhat deeper into cocoa can be just as low level.Juryman

© 2022 - 2024 — McMap. All rights reserved.