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?
The message loop is an obligatory section of code in every program that uses a graphical user interface under Microsoft Windows. Windows programs that have a GUI are event-driven. Windows maintains an individual message queue for each thread that has created a window. Usually only the first thread creates windows.
In an application developed with MFC, the main message loop in the CWinThread class contains a message loop that calls the PeekMessage Win32 API. This loop also calls the OnIdle member function of CWinThread between messages. An application can process messages in this idle time by overriding the OnIdle function.
A message loop allows PPAPI calls to be issued on a thread. You may not issue any API calls on a thread without creating a message loop. It also allows you to post work to the message loop for a thread.
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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With