objective-cmacoscocoa

Custom main application loop in cocoa


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.


Solution

  • 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.