From 7c5ebc89b87650ae649db1b5081f729d311915c4 Mon Sep 17 00:00:00 2001 From: Joshua Moerman Date: Mon, 28 Apr 2014 14:58:27 +0200 Subject: [PATCH] made a c++ interface to the obj-c cocoa api --- CMakeLists.txt | 20 +++++ NSGLWrapper.hpp | 38 +++++++++ NSGLWrapper.mm | 95 +++++++++++++++++++++ NSWrapper.hpp | 41 +++++++++ NSWrapper.mm | 216 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 410 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 NSGLWrapper.hpp create mode 100644 NSGLWrapper.mm create mode 100644 NSWrapper.hpp create mode 100644 NSWrapper.mm diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d988c57 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 2.8) + +set(LIBNAME JNSAppWrapper) + +file(GLOB sources "*.mm") +file(GLOB headers "*.hpp") + +add_library(${LIBNAME} ${headers} ${sources}) + +find_library(FOUNDATION_LIBRARY Foundation) +find_library(COCOA_LIBRARY Cocoa) +find_library(COREGRAPHICS_LIBRARY CoreGraphics) +find_library(OPENGL_LIBRARY OpenGL) + +set(libs ${FOUNDATION_LIBRARY} ${COCOA_LIBRARY} ${COREGRAPHICS_LIBRARY} ${OPENGL_LIBRARY}) + +target_link_libraries(${LIBNAME} ${libs}) + +install(TARGETS ${LIBNAME} DESTINATION lib) +install(FILES ${headers} DESTINATION include/J) diff --git a/NSGLWrapper.hpp b/NSGLWrapper.hpp new file mode 100644 index 0000000..dedd5b2 --- /dev/null +++ b/NSGLWrapper.hpp @@ -0,0 +1,38 @@ +// +// NSGLWrapper.hpp +// XcodeOpenCL +// +// Created by Joshua Moerman on 13/04/14. +// +// + +#pragma once + +#include +#include + +#include +#include + +struct GLContext { + GLContext(); + ~GLContext(); + + void set_as_current_context() const; + void lock(); + void unlock(); + + CGLContextObj ctx{}; + CGLPixelFormatObj pix{}; +}; + +struct CVDisplayLinky { + CVDisplayLinky(GLContext const &); + ~CVDisplayLinky(); + + void start(); + void stop(); + + std::function callback; + CVDisplayLinkRef displayLink; +}; diff --git a/NSGLWrapper.mm b/NSGLWrapper.mm new file mode 100644 index 0000000..2a71755 --- /dev/null +++ b/NSGLWrapper.mm @@ -0,0 +1,95 @@ +// +// NSGLWrapper.mm +// XcodeOpenCL +// +// Created by Joshua Moerman on 13/04/14. +// +// + +#include "NSGLWrapper.hpp" + +#include +#include + +#import +#import + +static void check_cgl(CGLError e) { + if(e != kCGLNoError){ + throw std::runtime_error(CGLErrorString(e)); + } +} + +GLContext::GLContext() { + GLint npix{}; + const CGLPixelFormatAttribute attributes[]{ + kCGLPFADoubleBuffer, + kCGLPFADepthSize, static_cast(24), + kCGLPFAOpenGLProfile, static_cast(kCGLOGLPVersion_3_2_Core), + static_cast(0) + }; + + check_cgl(CGLChoosePixelFormat(attributes, &pix, &npix)); + check_cgl(CGLCreateContext(pix, 0, &ctx)); + + GLint swap_interval = 1; + check_cgl(CGLSetParameter(ctx, kCGLCPSwapInterval, &swap_interval)); + + set_as_current_context(); +} + +GLContext::~GLContext() { + CGLReleaseContext(ctx); + CGLReleasePixelFormat(pix); +} + +void GLContext::set_as_current_context() const { + check_cgl(CGLSetCurrentContext(ctx)); +} + +void GLContext::lock() { + check_cgl(CGLLockContext(ctx)); +} + +void GLContext::unlock() { + check_cgl(CGLUnlockContext(ctx)); +} + + + +static void check_cv(CVReturn r) { + if(r != kCVReturnSuccess){ + throw std::runtime_error("Some CV display link issue"); + } +} + +static CVReturn CVDisplayLinkyInvokeCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { + if(!displayLinkContext){ + throw std::runtime_error("There is no context in the display link"); + } + auto& foo = *(CVDisplayLinky*)displayLinkContext; + if(foo.callback){ + foo.callback(); + } + return kCVReturnSuccess; +} + +CVDisplayLinky::CVDisplayLinky(GLContext const & c) { + check_cv(CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)); + check_cv(CVDisplayLinkSetOutputCallback(displayLink, &CVDisplayLinkyInvokeCallback, this)); + check_cv(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, c.ctx, c.pix)); +} + +CVDisplayLinky::~CVDisplayLinky() { + stop(); + CVDisplayLinkRelease(displayLink); +} + +void CVDisplayLinky::start() { + check_cv(CVDisplayLinkStart(displayLink)); +} + +void CVDisplayLinky::stop() { + check_cv(CVDisplayLinkStop(displayLink)); +} + diff --git a/NSWrapper.hpp b/NSWrapper.hpp new file mode 100644 index 0000000..c325bfc --- /dev/null +++ b/NSWrapper.hpp @@ -0,0 +1,41 @@ +// +// NSWrapper.hpp +// XcodeOpenCL +// +// Created by Joshua Moerman on 13/04/14. +// +// + +#pragma once + +#include +#include + +#include + +struct GLContext; +struct ContextParameters{ + GLContext & context; +}; + +struct GLViewFunctionality { + // All three will only be called with the current gl context already set + // May be called from different threads + std::function initialize_callback; + std::function draw_callback; + std::function resize_callback; +}; + +// returns a GLViewFunctionality with only its draw function set. +GLViewFunctionality simple_draw(std::function f); + +struct NSAppWrapper { + NSAppWrapper(); + ~NSAppWrapper(); + + void run(); + void create_window(GLViewFunctionality const &); + + std::unique_ptr impl; +}; + diff --git a/NSWrapper.mm b/NSWrapper.mm new file mode 100644 index 0000000..f0459bb --- /dev/null +++ b/NSWrapper.mm @@ -0,0 +1,216 @@ +// +// NSWrapper.mm +// XcodeOpenCL +// +// Created by Joshua Moerman on 13/04/14. +// +// + +#include "NSWrapper.hpp" +#include "NSGLWrapper.hpp" + +#include +#import +#import + + +GLViewFunctionality simple_draw(std::function f){ + return {nullptr, [f](ContextParameters){ f(); }, nullptr}; +} + + +@interface GLView : NSOpenGLView +// All three will only be called with the current gl context already set +// May be called from different threads (but the properties are atomic by default) +@property GLViewFunctionality functionality; +@end + + +struct NSAppWrapperImpl { + NSAppWrapperImpl() { + app = [NSApplication sharedApplication]; + app.activationPolicy = NSApplicationActivationPolicyRegular; + appName = [[NSProcessInfo processInfo] processName]; + } + + void set_up_menu() { + NSMenu* appMenu = [NSMenu new]; + [appMenu addItemWithTitle:[@"Quit " stringByAppendingString:appName] + action:@selector(terminate:) + keyEquivalent:@"q"]; + + NSMenuItem * menuItem = [NSMenuItem new]; + menuItem.submenu = appMenu; + + app.mainMenu = [NSMenu new]; + [app.mainMenu addItem:menuItem]; + } + + void create_window(GLViewFunctionality const & f) { + NSWindow * window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 500, 500) + styleMask:NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask + backing:NSBackingStoreBuffered + defer:YES]; + window.title = appName; + window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary; + [window center]; + [window makeKeyAndOrderFront:nil]; + + NSRect frame = [window contentRectForFrameRect:window.frame]; + frame.origin = {0, 0}; + GLView * view = [[GLView alloc] initWithFrame:frame]; + + view.functionality = f; + + window.contentView = view; + + windows.push_back(window); + } + + void run() { + [app run]; + } + + NSApplication * app; + std::vector windows; + NSString * appName; +}; + +NSAppWrapper::NSAppWrapper() +: impl(new NSAppWrapperImpl){ + impl->set_up_menu(); +} + +NSAppWrapper::~NSAppWrapper() = default; + +void NSAppWrapper::run() { + impl->run(); +} + +void NSAppWrapper::create_window(GLViewFunctionality const & f){ + impl->create_window(f); +} + + + +/* + Some general notes about GLView: + - It is in a multithreaded context. The CVDisplayLink will run on a diferent thread than the main thread. However the main thread *will also draw* (eg. when reshaping, to avoid flickering). So there are some locks here and there. + - Hopefully the C++ objects are destructed nicely + - There is syntax for the constructor, so I used unique_ptr... + */ + +@implementation GLView { + std::unique_ptr context; + std::unique_ptr linky; +} + +@synthesize functionality; + +- (id) initWithFrame:(NSRect)frameRect { + if(self = [super initWithFrame:frameRect]){ + context = std::unique_ptr(new GLContext); + + self.pixelFormat = [[NSOpenGLPixelFormat alloc] initWithCGLPixelFormatObj: context->pix]; + self.openGLContext = [[NSOpenGLContext alloc] initWithCGLContextObj: context->ctx]; + self.wantsBestResolutionOpenGLSurface = YES; // Opt-in for retina + } + + return self; +} + +- (void) prepareOpenGL{ + [super prepareOpenGL]; + + context->set_as_current_context(); + + // Synchronize buffer swaps with vertical refresh rate + GLint swapInt = 1; + [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; + + if(functionality.initialize_callback){ + functionality.initialize_callback({*context}); + } + + // Create a display link capable of being used with all active displays + linky = std::unique_ptr(new CVDisplayLinky(*context)); + linky->callback = [=]{ + // will be called in a different thread => need new autoreleasepool + @autoreleasepool { + [self drawView]; + } + }; + linky->start(); + + // Register to be notified when the window closes so we can stop the displaylink + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(windowWillClose:) + name:NSWindowWillCloseNotification + object:[self window]]; +} + +- (void) windowWillClose:(NSNotification*)notification{ + // The default OpenGL render buffers will be destroyed. If display link + // continues to fire without renderbuffers, OpenGL draw calls will set errors. + + linky->stop(); +} + +- (void) reshape{ + [super reshape]; + + context->lock(); + + // [NSView convertRectToBacking] converts point sizes to pixel sizes. + // viewRectPixels will be larger (2x) than viewRectPoints for retina displays. + // viewRectPixels will be the same as viewRectPoints for non-retina displays + NSRect viewRectPixels = [self convertRectToBacking:self.bounds]; + + if(functionality.resize_callback){ + functionality.resize_callback({*context}, viewRectPixels.size.width, viewRectPixels.size.height); + } + + context->unlock(); +} + +- (void)renewGState{ + // Called whenever graphics state updated (such as window resize) + + // OpenGL rendering is not synchronous with other rendering on the OSX. + // Therefore, call disableScreenUpdatesUntilFlush so the window server + // doesn't render non-OpenGL content in the window asynchronously from + // OpenGL content, which could cause flickering. (non-OpenGL content + // includes the title bar and drawing done by the app with other APIs) + [[self window] disableScreenUpdatesUntilFlush]; + + [super renewGState]; +} + +- (void) drawRect: (NSRect) theRect{ + [self drawView]; +} + +- (void) drawView{ + context->set_as_current_context(); + + // We draw on a secondary thread through the display link + // When resizing the view, -reshape is called automatically on the main + // thread. Add a mutex around to avoid the threads accessing the context + // simultaneously when resizing + context->lock(); + + NSRect viewRectPixels = [self convertRectToBacking:self.bounds]; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, (int)viewRectPixels.size.width, (int)viewRectPixels.size.height); + + if(functionality.draw_callback){ + functionality.draw_callback({*context}); + } + + CGLFlushDrawable(context->ctx); + context->unlock(); +} + +@end +