diff --git a/app/darwin.go b/app/darwin.go index 380a88c..ac5a2a4 100644 --- a/app/darwin.go +++ b/app/darwin.go @@ -15,6 +15,7 @@ package app #cgo LDFLAGS: -framework Cocoa -framework OpenGL -framework QuartzCore #import #import +#include void glGenVertexArrays(GLsizei n, GLuint* array); void glBindVertexArray(GLuint array); @@ -23,9 +24,12 @@ void runApp(void); void lockContext(GLintptr); void unlockContext(GLintptr); double backingScaleFactor(); +uint64 threadID(); + */ import "C" import ( + "log" "runtime" "sync" @@ -34,26 +38,39 @@ import ( "code.google.com/p/go.mobile/gl" ) +var initThreadID uint64 + func init() { + // Lock the goroutine responsible for initialization to an OS thread. + // This means the goroutine running main (and calling the run function + // below) is locked to the OS thread that started the program. This is + // necessary for the correct delivery of Cocoa events to the process. + // + // A discussion on this topic: + // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ runtime.LockOSThread() + initThreadID = uint64(C.threadID()) } func run(callbacks Callbacks) { + if tid := uint64(C.threadID()); tid != initThreadID { + log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID) + } cb = callbacks C.runApp() } //export setGeom -func setGeom(pixelsPerPt, width, height float64) { +func setGeom(pixelsPerPt float32, width, height int) { // Macs default to 72 DPI, so scales are equivalent. - geom.PixelsPerPt = float32(pixelsPerPt) - geom.Width = geom.Pt(width) - geom.Height = geom.Pt(height) + geom.PixelsPerPt = pixelsPerPt + geom.Width = geom.Pt(float32(width) / pixelsPerPt) + geom.Height = geom.Pt(float32(height) / pixelsPerPt) } func initGL() { // Using attribute arrays in OpenGL 3.3 requires the use of a VBA. - // But VBAs don't exist in ES 2. So we bind one default one. + // But VBAs don't exist in ES 2. So we bind a default one. var id C.GLuint C.glGenVertexArrays(1, &id) C.glBindVertexArray(id) @@ -61,43 +78,51 @@ func initGL() { var cb Callbacks var initGLOnce sync.Once -var events = make(chan event.Touch, 1<<6) + +var events struct { + sync.Mutex + pending []event.Touch +} func sendTouch(ty event.TouchType, x, y float32) { - events <- event.Touch{ + events.Lock() + events.pending = append(events.pending, event.Touch{ Type: ty, Loc: geom.Point{ - X: geom.Pt(x), - Y: geom.Height - geom.Pt(y), + X: geom.Pt(x / geom.PixelsPerPt), + Y: geom.Height - geom.Pt(y/geom.PixelsPerPt), }, - } + }) + events.Unlock() } //export eventMouseDown func eventMouseDown(x, y float32) { sendTouch(event.TouchStart, x, y) } -//export eventMouseMove -func eventMouseMove(x, y float32) { sendTouch(event.TouchMove, x, y) } +//export eventMouseDragged +func eventMouseDragged(x, y float32) { sendTouch(event.TouchMove, x, y) } //export eventMouseEnd func eventMouseEnd(x, y float32) { sendTouch(event.TouchEnd, x, y) } //export drawgl func drawgl(ctx C.GLintptr) { + // The call to lockContext loads the OpenGL context into + // thread-local storage for use by the underlying GL calls + // done in the user's Draw function. We need to stay on + // the same thread throughout Draw, so we LockOSThread. runtime.LockOSThread() C.lockContext(ctx) initGLOnce.Do(initGL) -loop: - for { - select { - case e := <-events: - if cb.Touch != nil { - cb.Touch(e) - } - default: - break loop + events.Lock() + pending := events.pending + events.pending = nil + events.Unlock() + for _, e := range pending { + if cb.Touch != nil { + cb.Touch(e) } } @@ -109,5 +134,9 @@ loop: } C.unlockContext(ctx) + + // This may unlock the original main thread, but that's OK, + // because by the time it does the thread has already entered + // C.runApp, which won't give the thread up. runtime.UnlockOSThread() } diff --git a/app/darwin.m b/app/darwin.m index ab377e3..e8517f2 100644 --- a/app/darwin.m +++ b/app/darwin.m @@ -5,6 +5,7 @@ // +build darwin #include "_cgo_export.h" +#include #include #import @@ -34,6 +35,14 @@ void unlockContext(GLintptr context) { } +uint64 threadID() { + uint64 id; + if (pthread_threadid_np(pthread_self(), &id)) { + abort(); + } + return id; +} + @interface MobileGLView : NSOpenGLView { @@ -43,7 +52,7 @@ void unlockContext(GLintptr context) { @implementation MobileGLView - (void)prepareOpenGL { - [self setWantsBestResolutionOpenGLSurface:true]; + [self setWantsBestResolutionOpenGLSurface:YES]; GLint swapInt = 1; [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; @@ -57,24 +66,52 @@ void unlockContext(GLintptr context) { } - (void)reshape { + // Calculate screen PPI. + // + // Note that the backingScaleFactor converts from logical + // pixels to actual pixels, but both of these units vary + // independently from real world size. E.g. + // + // 13" Retina Macbook Pro, 2560x1600, 227ppi, backingScaleFactor=2, scale=3.15 + // 15" Retina Macbook Pro, 2880x1800, 220ppi, backingScaleFactor=2, scale=3.06 + // 27" iMac, 2560x1440, 109ppi, backingScaleFactor=1, scale=1.51 + // 27" Retina iMac, 5120x2880, 218ppi, backingScaleFactor=2, scale=3.03 + NSScreen *screen = [NSScreen mainScreen]; + double screenPixW = [screen frame].size.width * [screen backingScaleFactor]; + + CGDirectDisplayID display = (CGDirectDisplayID)[[[screen deviceDescription] valueForKey:@"NSScreenNumber"] intValue]; + CGSize screenSizeMM = CGDisplayScreenSize(display); // in millimeters + float ppi = 25.4 * screenPixW / screenSizeMM.width; + float pixelsPerPt = ppi/72.0; + + // The width and height reported to the geom package are the + // bounds of the OpenGL view. Several steps are necessary. + // First, [self bounds] gives us the number of logical pixels + // in the view. Multiplying this by the backingScaleFactor + // gives us the number of actual pixels. NSRect r = [self bounds]; - double scale = [[NSScreen mainScreen] backingScaleFactor]; - setGeom(scale, r.size.width, r.size.height); + int w = r.size.width * [screen backingScaleFactor]; + int h = r.size.height * [screen backingScaleFactor]; + + setGeom(pixelsPerPt, w, h); } - (void)mouseDown:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; NSPoint p = [theEvent locationInWindow]; - eventMouseDown(p.x, p.y); + eventMouseDown(p.x * scale, p.y * scale); } - (void)mouseUp:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; NSPoint p = [theEvent locationInWindow]; - eventMouseEnd(p.x, p.y); + eventMouseEnd(p.x * scale, p.y * scale); } -- (void)mouseMoved:(NSEvent *)theEvent { +- (void)mouseDragged:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; NSPoint p = [theEvent locationInWindow]; - eventMouseMove(p.x, p.y); + eventMouseDragged(p.x * scale, p.y * scale); } @end @@ -97,7 +134,7 @@ runApp(void) { [menu addItem:quitMenuItem]; [menuItem setSubmenu:menu]; - NSRect rect = NSMakeRect(0, 0, 200, 200); + NSRect rect = NSMakeRect(0, 0, 400, 400); id window = [[[NSWindow alloc] initWithContentRect:rect styleMask:NSTitledWindowMask