Cocoa Event Loop/NSTimer revisited

Oldtimer
Posts: 834
Joined: 2002.09
Post: #1
While reinventing my game engine, I moved to Cocoa and decided to plan ahead for cross-platform-ness. This is a regular discussion we all have, but going through the archives here has thrown me into a fit of confusion: there are a lot of contradictory posts, some dating back to 2003. So, I'd like to poll (ahem) people for their knowledge about the best-practice event handling.

The most koscher way I've found so far is to bring the NSEvents in from an NSView subclass - in effect the key responder. Then, running the "idle"/"update" code from an NSTimer set to fire at either 60 Hz or 1kHz. The problem is: what happens when you don't have a window there, like when you're running in fullscreen? So, I'd like to pose a couple of questions here, and see what you think of the issues.

If you run from within a while(true) loop, and use [NSEvent nextEventMatchingMask: etc] - how do you avoid burning 100% CPU? Do you call select, or is VBL synching the way for you? And if you're ripping through it all as fast as you can - how do you handlle decreased battery life time on laptops and fans going at full speed? (Have you tried booting into single user mode on the RevB iMac G5:s? Chances are the fans will kick into full gear after a while. Je-sus what a noise.)

What about decoupling logic and rendering into two separate threads? What is your methodology here?

I guess the core of the question is: when you're running fullscreen, are you handling events the same way you do when running from a window? And is [NSEvent nextEventMatchingMask:] good enough in all cases?
Quote this message in a reply
Moderator
Posts: 1,140
Joined: 2005.07
Post: #2
When running fullscreen, you don't have much choice in the matter: you have to capture the events yourself. (BTW: you want to use [NSApp nextEventMatchingMask:untilDate:inMode:dequeue:] with a date of NSDistantPast so it always returns immediately) One way to implement it is you have a loop that's while (true), then grab the event, then if the event exists (not nil), you can process it. Then, update your graphics etc. I'm sure it does burn up a lot of CPU cycles if you're in a menu or something similarly simple, but it still remains 100% responsive, at least in my tests. Also, when I had an early build of my game up using that method, nobody complained about an unresponsive menu, so it's not just me. Rasp When you get to the actual game, it takes longer for each loop cycle, and you can also adjust your time steps for animations etc. for framerate. If you use an NSTimer, and you have a complex screen on a slow computer, the entire game will slow down. However, if you have a loop with proper time-based management, the framerate will dip but the game will stay at a constant speed.

However, if you're doing a simple puzzle game, this isn't very necessary: even on slow computers, it shouldn't be fast enough to outrun a timer set at a reasonable speed. (like 30 fps) With that, you can simply do the same thing, but instead of a loop you have the timer, and at the beginning of each one you grab the event.
Quote this message in a reply
Oldtimer
Posts: 834
Joined: 2002.09
Post: #3
Yeah, I know about the full nextEventMatchingMask: selector, just couldn't be bothered to type it all out. Smile

Responsiveness isn't my problem, since those 100% CPU cycles will be in my game. What worries me is, on laptops, the CPU doesn't go to sleep, so it builds up heat and uses up much more battery power than necessary. I guess you mean that your Aqua menu can become unresponsive - that shouldn't be a problem since that menu is serviced from your nextEventAndSoOn: calls.

My thoughts here are to run the app the way you're suggesting, but making sure to block om VBL synch somehow. I wonder if that would yield the CPU the way it should, though...
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #4
vsync and a custom event loop is the best way I've found. See http://onesadcookie.com/svn/repos/GameShell .
Quote this message in a reply
Moderator
Posts: 3,573
Joined: 2003.06
Post: #5
I still haven't found a better way to do it than to set the timer for 0.001f and turn on VBL synch (contrary to what an Apple TN suggests - draw through drawRect using like setNeedsUpdate or something, which I tried and it sucked miserably). With VBL synch turned on, the OS automatically blocks the application's thread from being executed when not needed (at least that's how I understand it). I manually block the graphics drawing myself when the window is minimized or hidden, in the timer callback as: if (!shouldDraw) return; I tried removing the timer instead when minimized and there was no noticeable difference in the activity monitor. For captured fullscreen I just do the ol' custom event loop, as mentioned, and grab the events manually. Again, even in fullscreen, VBL synch blocks extraneous thread execution anyway, so no problems with the CPU usage there that I can tell.

Some related thoughts/ramblings on the subject. Disclaimer: I'm not a computer wiz by any stretch, so take what I'm saying here with lots of NaCl (or just call it plain B.S. if you prefer). First, since OS X is a pre-emptively multi-tasking system, there is no real way to get 100% of the CPU, even in a while() loop. Right, we know that, but I thought I'd mention it anyway. On the subject of 60 Hz or 1000 Hz. It's a bad idea to pick 60 Hz because if the display is synched at 72 Hz, you're going to wind up skipping a whole bunch of frames. So then the next logical step would be to synch to whatever the local display settings are, right? Well I haven't had good luck with that at all. The "high-performance timers" for OS X don't always fire exactly when you might expect. If the local display is 72 Hz I could set a timer to fire at 72 Hz to match it and it still skips some frames from time to time. Better to overdo it and fire as often as reasonably possible to more assuredly catch the next VBL synch window. Apple seems to say that's a bad idea, but... their example of how to do it better just blows chunks for performance if you ask me. I swear, all they do is test it on the latest and greatest hardware sometimes and call it good. On an older machine like my dual G4 867, setNeedsUpdate does not cut the mustard. Which leads to my next thought:

I send a lot of my stuff to my brother-in-law to test on his 12" Al-Book and he hasn't complained about any extra CPU usage. He once mentioned that the fan kicked up but that was with some intense graphics. YMMV. He also mentioned that it's fairly common for the fan to kick up with games so it's not a big deal to him. Which leads me to my next thought:

There seems to be this way of thinking in some circles that your application should be completely CPU friendly and get along seemlessly with the rest of the OS environment, even for games. Now, I can understand that to some extent with like card games and Tetris clones, but the fact of the matter is that with more intense games it just isn't reasonable to expect "great system performance" in my opinion. A game is not designed for being in a "work-space". The most you're likely to do while playing a game is type some chat in another window, or maybe check your email to see if you gotta get back to work. When you're playing a game, you're playing a game. Screw the CPU! Damn the battery! I think greediness in the game environment is A-O-Kay. It's hard enough learning to push the envelope of graphics performance, let alone designing in "should I be nice to their system?". Laptop owners know darn well when they play a game with intense graphics their battery is going to take a beating. That's just the nature of the beast. The only serious provision I make is that when the user wants to hide the app or minimize the window I don't do any processing in the graphics thread, as I mentioned above. But still with the 1000 Hz timer and VBL synch on or not, I haven't had any real system performance problems anyway. OS X (or Darwin rather) seems to know how to delegate time slices pretty efficiently.
Quote this message in a reply
Moderator
Posts: 1,140
Joined: 2005.07
Post: #6
Fenris Wrote:Yeah, I know about the full nextEventMatchingMask: selector, just couldn't be bothered to type it all out. Smile

Responsiveness isn't my problem, since those 100% CPU cycles will be in my game. What worries me is, on laptops, the CPU doesn't go to sleep, so it builds up heat and uses up much more battery power than necessary. I guess you mean that your Aqua menu can become unresponsive - that shouldn't be a problem since that menu is serviced from your nextEventAndSoOn: calls.

My thoughts here are to run the app the way you're suggesting, but making sure to block om VBL synch somehow. I wonder if that would yield the CPU the way it should, though...
I also was talking about the menus and/or a game that has only very simple graphics. Rasp If you have a game with some moderate to heavy graphics, it's better to have the while (true) loop, and it only revs up your CPU when in the menus. Otherwise, if it's with very light graphics, a timer will never slow down enough, and will be fine.
Quote this message in a reply
Member
Posts: 129
Joined: 2005.02
Post: #7
About your question regarding "not having a window" when you are in full screen. Why not? John Stiles also brought this up on the mac game programming list on apple's lists. You can search for it on the archives. I think he had a good post regarding his findings. Either way, I chose to go this route and I haven't run into any problems thus far.
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  f suffix on floats revisited AnotherJake 2 2,810 Dec 22, 2008 11:11 PM
Last Post: Wowbagger
  Using NSTimer in SDL Loop. Talyn 7 4,212 Oct 7, 2008 09:40 PM
Last Post: OneSadCookie
  Proper game update loop in Cocoa? LoneIgadzra 13 6,636 May 30, 2008 05:49 AM
Last Post: ThemsAllTook
  Yield in a Cocoa while loop Fenris 7 4,820 Dec 28, 2007 02:00 AM
Last Post: Fenris
  Starting Menu: a separate event loop? ferum 11 5,529 Sep 8, 2006 03:35 AM
Last Post: imikedaman