Custom main application loop in cocoa
Asked Answered
S

4

5

I have been following the Handmade Hero project where Casey Muratori creates a complete game engine from scratch without the use of libraries. This engine is highly portable since it renders its own bitmap which the platform specific code then draws to the screen.

Under windows there normally is an main application loop where you can put your code which should be executed repeatedly until the application gets terminated. However there is no such thing in Cocoa. As soon as [NSApp run]; is called int main() gets pretty much useless and you have to put your code into delegate methods to get it executed. But thats not how I want do do it. I found some code online where someone already did exactly what I wanted but the code has some flaws or lets say I just don't know how to deal with it.

#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CoreGraphics.h>
#include <stdint.h>


#define internal static
#define local_persist static
#define global_variable static

typedef uint8_t uint8;

global_variable bool running = false;

global_variable void *BitmapMemory;
global_variable int BitmapWidth = 1024;
global_variable int BitmapHeight = 768;
global_variable int BytesPerPixel = 4;

global_variable int XOffset = 0;
global_variable int YOffset = 0;


@class View;
@class AppDelegate;
@class WindowDelegate;


global_variable AppDelegate *appDelegate;
global_variable NSWindow *window;
global_variable View *view;
global_variable WindowDelegate *windowDelegate;


@interface AppDelegate: NSObject <NSApplicationDelegate> {
}
@end

@implementation AppDelegate

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
    // Cocoa will kill your app on the spot if you don't stop it
    // So if you want to do anything beyond your main loop then include this method.
    running = false;
    return NSTerminateCancel;
}

@end


@interface WindowDelegate : NSObject <NSWindowDelegate> {
}
@end
@implementation WindowDelegate

- (BOOL)windowShouldClose:(id)sender {
    running = false;
    return YES;
}

-(void)windowWillClose:(NSNotification *)notification {
    if (running) {
        running = false;
        [NSApp terminate:self];
    }
}

@end




@interface View : NSView <NSWindowDelegate> {
@public
    CGContextRef backBuffer_;
}
- (instancetype)initWithFrame:(NSRect)frameRect;
- (void)drawRect:(NSRect)dirtyRect;
@end

@implementation View
// Initialize
- (id)initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    if (self) {
        int bitmapByteCount;
        int bitmapBytesPerRow;

        bitmapBytesPerRow = (BitmapWidth * 4);
        bitmapByteCount = (bitmapBytesPerRow * BitmapHeight);
        BitmapMemory = mmap(0,
                            bitmapByteCount,
                            PROT_WRITE |
                            PROT_READ,
                            MAP_ANON |
                            MAP_PRIVATE,
                            -1,
                            0);
        //CMProfileRef prof;
        //CMGetSystemProfile(&prof);
        CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
        backBuffer_ = CGBitmapContextCreate(BitmapMemory, BitmapWidth, BitmapHeight, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
        CGColorSpaceRelease(colorSpace);
        //CMCloseProfile(prof);
    }
    return self;
}



- (void)drawRect:(NSRect)dirtyRect {
    CGContextRef gctx = [[NSGraphicsContext currentContext] graphicsPort];
    CGRect myBoundingBox;
    myBoundingBox = CGRectMake(0, 0, 1024, 768);
    //RenderWeirdGradient(XOffset, YOffset);
    CGImageRef backImage = CGBitmapContextCreateImage(backBuffer_);
    CGContextDrawImage(gctx, myBoundingBox, backImage);
    CGImageRelease(backImage);
}


internal void RenderWeirdGradient(int BlueOffset, int GreenOffset) {
    int Width = BitmapWidth;
    int Height = BitmapHeight;

    int Pitch = Width*BytesPerPixel;
    uint8 *Row = (uint8 *)BitmapMemory;
    for(int Y = 0;
        Y < BitmapHeight;
        ++Y)
    {
        uint8 *Pixel = (uint8 *)Row;
        for(int X = 0;
            X < BitmapWidth;
            ++X)
        {
            *Pixel = 0;
            ++Pixel;

            *Pixel = (uint8)Y + XOffset;
            ++Pixel;

            *Pixel = (uint8)X + YOffset;
            ++Pixel;

            *Pixel = 255;
            ++Pixel;

        }

        Row += Pitch;
    }
}



@end


static void createWindow() {
    NSUInteger windowStyle = NSTitledWindowMask  | NSClosableWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask;

    NSRect screenRect = [[NSScreen mainScreen] frame];
    NSRect viewRect = NSMakeRect(0, 0, 1024, 768);
    NSRect windowRect = NSMakeRect(NSMidX(screenRect) - NSMidX(viewRect),
                                   NSMidY(screenRect) - NSMidY(viewRect),
                                   viewRect.size.width,
                                   viewRect.size.height);

    window = [[NSWindow alloc] initWithContentRect:windowRect
                                                    styleMask:windowStyle
                                                      backing:NSBackingStoreBuffered
                                                        defer:NO];

    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];

    id menubar = [[NSMenu new] autorelease];
    id appMenuItem = [[NSMenuItem new] autorelease];
    [menubar addItem:appMenuItem];
    [NSApp setMainMenu:menubar];

    // Then we add the quit item to the menu. Fortunately the action is simple since terminate: is
    // already implemented in NSApplication and the NSApplication is always in the responder chain.
    id appMenu = [[NSMenu new] autorelease];
    id appName = [[NSProcessInfo processInfo] processName];
    id quitTitle = [@"Quit " stringByAppendingString:appName];
    id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:quitTitle
                                                  action:@selector(terminate:) keyEquivalent:@"q"] autorelease];
    [appMenu addItem:quitMenuItem];
    [appMenuItem setSubmenu:appMenu];

    NSWindowController * windowController = [[NSWindowController alloc] initWithWindow:window];
    [windowController autorelease];

    //View
    view = [[[View alloc] initWithFrame:viewRect] autorelease];
    [window setContentView:view];

    //Window Delegate
    windowDelegate = [[WindowDelegate alloc] init];
    [window setDelegate:windowDelegate];

    [window setAcceptsMouseMovedEvents:YES];
    [window setDelegate:view];

    // Set app title
    [window setTitle:appName];

    // Add fullscreen button
    [window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenPrimary];
    [window makeKeyAndOrderFront:nil];
}

void initApp() {
    [NSApplication sharedApplication];

    appDelegate = [[AppDelegate alloc] init];
    [NSApp setDelegate:appDelegate];

    running = true;

    [NSApp finishLaunching];
}

void frame() {
    @autoreleasepool {
        NSEvent* ev;
        do {
            ev = [NSApp nextEventMatchingMask: NSAnyEventMask
                                    untilDate: nil
                                       inMode: NSDefaultRunLoopMode
                                      dequeue: YES];
            if (ev) {
                // handle events here
                [NSApp sendEvent: ev];
            }
        } while (ev);
    }
}

int main(int argc, const char * argv[])  {
    initApp();
    createWindow();
    while (running) {
        frame();
        RenderWeirdGradient(XOffset, YOffset);
        [view setNeedsDisplay:YES];
        XOffset++;
        YOffset++;
    }

    return (0);
}

This is all the code the application needs to run so far. Just copy and paste it into an empty Xcode Command Line Project and it will work.

However as you inspect the hardware while the application is running you will see that the CPU is pretty much running at 100%. I read that the reason for this problem is that the application has to search for new events the whole time because of the custom run loop.

Moreover since the loop doesn't hand control over to the delegate objects, methods like - (BOOL)windowShouldClose:(id)sender do not work anymore.

Questions:

  1. Is there a better way of implementing a custom main application loop with the style below that doesn't waste CPU time as much as the one I'm using?

    while (running) { //do stuff }

  2. How do I get the application terminated with pressing the window's close button since the Application Delegate and Window Delegate methods do not respond anymore?

I've spent hours now searching the web for custom main run loops in Cocoa but just came across multithreading and stuff that wouldn't help me.

Could you recommend some online resources/books that would help me in my case? I would really like to get my hands on some resources that handle unusual stuff like a custom run loop.

Sophomore answered 4/8, 2016 at 13:25 Comment(2)
This is no consulting/discussion site. Your question is far too broad. See How to Ask. And this is not C! Don't spam tags!Gree
Gosh look at that semantics add #defines at the top..Sandglass
R
1

This, simply put, isn't how well-behaved Cocoa applications are put together; and as you've discovered through your delegate method problems, not how the Cocoa framework works.

Besides the fact that a lot of code in AppKit expects NSApplicationMain() to be called, the system at large does as well, and with your approach you will probably end up with your application doing a bunch of annoying things like interacting poorly with the Dock and Launchpad.

There's also the issue of bundle resources and so on; which impacts, among other things, code signing, meaning that unless this is something you do for personal use only, you're going to have trouble getting the application out into the world.

What you want to do is to set up a single window with a single view to do the drawing, and a thread to act as the logic loop, as appropriate. Do the frame, tell the system to update the view, be happy.

Rudd answered 4/8, 2016 at 13:37 Comment(8)
Ok, thank you for your reply. But can you tell if there is another way exiting the application than using the delegates?Sophomore
@Sophomore Don't. Set up the application in the usual way (with NSApplicationMain() and call [NSApp terminate:nil].Rudd
I will not! I promise! I just want to know. "If one is to understand the greater mystery one must study all its aspects not just the dogmatic narrow view of the jedi!"Sophomore
@Sophomore The other way of exiting the application is by using exit(), which really does just that. It should only be used prior to calling NSApplicationMain or if your application is somehow irreparably stuck (but if you can detect that condition, it doesn't exist, so basically never.) It is the nuclear option.Rudd
And how do I recognize when the red X is pressed? Of course without delegates? I have to set my running to false somewhere. Of course just in theory!Sophomore
@Sophomore You don't. You set up the application correctly and use delegates. (And when I say you don't, I'm being literal: The events don't get propagated without the NSApplication run loop.)Rudd
Let us continue this discussion in chat.Sophomore
Williham is just saying we can't be sure this code, although it appears to work, is covering all the bases that NSApplicationMain does. So it might not behave properly inside the OS, due to not handling a certain message properly, etc. It'd have to be very thoughly vetted.Sandglass
C
3

I know it's like two years late, but I found an article over at Cocoa With Love that you might find useful.

https://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html

I tried implementing the main event loop this way, looked at the CPU usage, and it's waaaay more reasonable than what I was getting earlier. I don't entirely know why, but I'll do some more research on that.

Callaghan answered 19/2, 2019 at 2:42 Comment(0)
R
1

This, simply put, isn't how well-behaved Cocoa applications are put together; and as you've discovered through your delegate method problems, not how the Cocoa framework works.

Besides the fact that a lot of code in AppKit expects NSApplicationMain() to be called, the system at large does as well, and with your approach you will probably end up with your application doing a bunch of annoying things like interacting poorly with the Dock and Launchpad.

There's also the issue of bundle resources and so on; which impacts, among other things, code signing, meaning that unless this is something you do for personal use only, you're going to have trouble getting the application out into the world.

What you want to do is to set up a single window with a single view to do the drawing, and a thread to act as the logic loop, as appropriate. Do the frame, tell the system to update the view, be happy.

Rudd answered 4/8, 2016 at 13:37 Comment(8)
Ok, thank you for your reply. But can you tell if there is another way exiting the application than using the delegates?Sophomore
@Sophomore Don't. Set up the application in the usual way (with NSApplicationMain() and call [NSApp terminate:nil].Rudd
I will not! I promise! I just want to know. "If one is to understand the greater mystery one must study all its aspects not just the dogmatic narrow view of the jedi!"Sophomore
@Sophomore The other way of exiting the application is by using exit(), which really does just that. It should only be used prior to calling NSApplicationMain or if your application is somehow irreparably stuck (but if you can detect that condition, it doesn't exist, so basically never.) It is the nuclear option.Rudd
And how do I recognize when the red X is pressed? Of course without delegates? I have to set my running to false somewhere. Of course just in theory!Sophomore
@Sophomore You don't. You set up the application correctly and use delegates. (And when I say you don't, I'm being literal: The events don't get propagated without the NSApplication run loop.)Rudd
Let us continue this discussion in chat.Sophomore
Williham is just saying we can't be sure this code, although it appears to work, is covering all the bases that NSApplicationMain does. So it might not behave properly inside the OS, due to not handling a certain message properly, etc. It'd have to be very thoughly vetted.Sandglass
M
0

for question 1: you can put your custommainloop function inside an NSApplication inherit class, then use performSelectorOnMainThread to invode it.

[app
        performSelectorOnMainThread:@selector(run)
        withObject:nil
        waitUntilDone:YES];

for question 2: I think you should return NSTerminateNow inside event applicationShouldTerminate instead of NSTerminateCancel.

Mischance answered 26/12, 2023 at 14:17 Comment(0)
S
0

I experimented with this and found out a few different things.

  1. Even though the sample code you posted can run at like 50,000 fps, practically speaking you only get an irregular, frame draw rate that unpredictably fluctuates between 30 - 60 fps. You're drawing very often and saying to the system "I've updated the display, go ahead and redraw", but the system is taking it's sweet time to do so. So sometimes fps actually drops to something like 30.

  2. To fix that, you can actually get a faster frame draw rate from the code you posted if you replace [view setNeedsDisplay:YES] with [view display]

  3. With [view setNeedsDisplay:YES], the app consumes nearly 3x CPU.

Also, I think it may be a good idea to heed Williham's cautions in his answer and stick with the more commonly used route of CADisplayLink (on both iOS and Mac 14+ desktop apps (before macOS 14 it was CVDisplayLink)) (the structure you showed in your question doesn't appear to be common and I'm not sure has been thoroughly vetted. So it will probably show odd glitches and have weird bugs for some things)

Using CADisplayLink is easy and it looks like this:

In your NSView object:

- (void) update:(CADisplayLink*) sender {
  [self runGameFrame]; // run your game logic
  [self display]; // triggers a call to drawRect:(NSRect)bounds
}

- (void) createDisplayLink {
  CADisplayLink *displayLink = [self displayLinkWithTarget:self selector:@selector(update:)];
  [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
  displayLink.preferredFrameRateRange = CAFrameRateRangeMake( 60, 60, 60 );  
}

SDL uses DisplayLink

Sandglass answered 15/8 at 17:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.