src/video/cocoa/SDL_cocoaopengl.m
author Ryan C. Gordon <icculus@icculus.org>
Sun, 16 Dec 2018 01:03:17 -0500
changeset 12477 73f3ca85ac0e
parent 12343 84eaa0636bac
child 12503 806492103856
permissions -rw-r--r--
cocoa: Implement OpenGL swap interval support with CVDisplayLink.

Not only does this fix macOS 10.14 ("Mojave")'s broken NSOpenGLCPSwapInterval
support, it also lets us implement "adaptive vsync" on macOS!

CVDisplayLink is supported back to macOS 10.4 ("Tiger"), so we just use it
universally without version checks and dump NSOpenGLCPSwapInterval, Mojave or
not.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org>
     4 
     5   This software is provided 'as-is', without any express or implied
     6   warranty.  In no event will the authors be held liable for any damages
     7   arising from the use of this software.
     8 
     9   Permission is granted to anyone to use this software for any purpose,
    10   including commercial applications, and to alter it and redistribute it
    11   freely, subject to the following restrictions:
    12 
    13   1. The origin of this software must not be misrepresented; you must not
    14      claim that you wrote the original software. If you use this software
    15      in a product, an acknowledgment in the product documentation would be
    16      appreciated but is not required.
    17   2. Altered source versions must be plainly marked as such, and must not be
    18      misrepresented as being the original software.
    19   3. This notice may not be removed or altered from any source distribution.
    20 */
    21 #include "../../SDL_internal.h"
    22 
    23 /* NSOpenGL implementation of SDL OpenGL support */
    24 
    25 #if SDL_VIDEO_OPENGL_CGL
    26 #include "SDL_cocoavideo.h"
    27 #include "SDL_cocoaopengl.h"
    28 #include "SDL_cocoaopengles.h"
    29 
    30 #include <OpenGL/CGLTypes.h>
    31 #include <OpenGL/OpenGL.h>
    32 #include <OpenGL/CGLRenderers.h>
    33 
    34 #include "SDL_loadso.h"
    35 #include "SDL_opengl.h"
    36 
    37 #define DEFAULT_OPENGL  "/System/Library/Frameworks/OpenGL.framework/Libraries/libGL.dylib"
    38 
    39 static CVReturn
    40 DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
    41 {
    42     SDLOpenGLContext *nscontext = (SDLOpenGLContext *) displayLinkContext;
    43     
    44     /*printf("DISPLAY LINK! %u\n", (unsigned int) SDL_GetTicks()); */
    45     const int setting = SDL_AtomicGet(&nscontext->swapIntervalSetting);
    46     if (setting != 0) { /* nothing to do if vsync is disabled, don't even lock */
    47         SDL_LockMutex(nscontext->swapIntervalMutex);
    48         SDL_AtomicAdd(&nscontext->swapIntervalsPassed, 1);
    49         SDL_CondSignal(nscontext->swapIntervalCond);
    50         SDL_UnlockMutex(nscontext->swapIntervalMutex);
    51     }
    52 
    53     return kCVReturnSuccess;
    54 }
    55 
    56 @implementation SDLOpenGLContext : NSOpenGLContext
    57 
    58 - (id)initWithFormat:(NSOpenGLPixelFormat *)format
    59         shareContext:(NSOpenGLContext *)share
    60 {
    61     self = [super initWithFormat:format shareContext:share];
    62     if (self) {
    63         SDL_AtomicSet(&self->dirty, 0);
    64         self->window = NULL;
    65         SDL_AtomicSet(&self->swapIntervalSetting, 0);
    66         SDL_AtomicSet(&self->swapIntervalsPassed, 0);
    67         self->swapIntervalCond = SDL_CreateCond();
    68         self->swapIntervalMutex = SDL_CreateMutex();
    69         if (!self->swapIntervalCond || !self->swapIntervalMutex) {
    70             [self release];
    71             return nil;
    72         }
    73 
    74         /* !!! FIXME: check return values. */
    75         CVDisplayLinkCreateWithActiveCGDisplays(&self->displayLink);
    76         CVDisplayLinkSetOutputCallback(self->displayLink, &DisplayLinkCallback, self);
    77         CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(self->displayLink, [self CGLContextObj], [format CGLPixelFormatObj]);
    78         CVDisplayLinkStart(displayLink);
    79     }
    80     return self;
    81 }
    82 
    83 - (void)scheduleUpdate
    84 {
    85     SDL_AtomicAdd(&self->dirty, 1);
    86 }
    87 
    88 /* This should only be called on the thread on which a user is using the context. */
    89 - (void)updateIfNeeded
    90 {
    91     int value = SDL_AtomicSet(&self->dirty, 0);
    92     if (value > 0) {
    93         /* We call the real underlying update here, since -[SDLOpenGLContext update] just calls us. */
    94         [super update];
    95     }
    96 }
    97 
    98 /* This should only be called on the thread on which a user is using the context. */
    99 - (void)update
   100 {
   101     /* This ensures that regular 'update' calls clear the atomic dirty flag. */
   102     [self scheduleUpdate];
   103     [self updateIfNeeded];
   104 }
   105 
   106 /* Updates the drawable for the contexts and manages related state. */
   107 - (void)setWindow:(SDL_Window *)newWindow
   108 {
   109     if (self->window) {
   110         SDL_WindowData *oldwindowdata = (SDL_WindowData *)self->window->driverdata;
   111 
   112         /* Make sure to remove us from the old window's context list, or we'll get scheduled updates from it too. */
   113         NSMutableArray *contexts = oldwindowdata->nscontexts;
   114         @synchronized (contexts) {
   115             [contexts removeObject:self];
   116         }
   117     }
   118 
   119     self->window = newWindow;
   120 
   121     if (newWindow) {
   122         SDL_WindowData *windowdata = (SDL_WindowData *)newWindow->driverdata;
   123 
   124         /* Now sign up for scheduled updates for the new window. */
   125         NSMutableArray *contexts = windowdata->nscontexts;
   126         @synchronized (contexts) {
   127             [contexts addObject:self];
   128         }
   129 
   130         if ([self view] != [windowdata->nswindow contentView]) {
   131             [self setView:[windowdata->nswindow contentView]];
   132             if (self == [NSOpenGLContext currentContext]) {
   133                 [self update];
   134             } else {
   135                 [self scheduleUpdate];
   136             }
   137         }
   138     } else {
   139         [self clearDrawable];
   140         if (self == [NSOpenGLContext currentContext]) {
   141             [self update];
   142         } else {
   143             [self scheduleUpdate];
   144         }
   145     }
   146 }
   147 
   148 - (void)dealloc
   149 {
   150     if (self->displayLink) {
   151         CVDisplayLinkRelease(self->displayLink);
   152     }
   153     if (self->swapIntervalCond) {
   154         SDL_DestroyCond(self->swapIntervalCond);
   155     }
   156     if (self->swapIntervalMutex) {
   157         SDL_DestroyMutex(self->swapIntervalMutex);
   158     }
   159     [super dealloc];
   160 }
   161 @end
   162 
   163 
   164 int
   165 Cocoa_GL_LoadLibrary(_THIS, const char *path)
   166 {
   167     /* Load the OpenGL library */
   168     if (path == NULL) {
   169         path = SDL_getenv("SDL_OPENGL_LIBRARY");
   170     }
   171     if (path == NULL) {
   172         path = DEFAULT_OPENGL;
   173     }
   174     _this->gl_config.dll_handle = SDL_LoadObject(path);
   175     if (!_this->gl_config.dll_handle) {
   176         return -1;
   177     }
   178     SDL_strlcpy(_this->gl_config.driver_path, path,
   179                 SDL_arraysize(_this->gl_config.driver_path));
   180     return 0;
   181 }
   182 
   183 void *
   184 Cocoa_GL_GetProcAddress(_THIS, const char *proc)
   185 {
   186     return SDL_LoadFunction(_this->gl_config.dll_handle, proc);
   187 }
   188 
   189 void
   190 Cocoa_GL_UnloadLibrary(_THIS)
   191 {
   192     SDL_UnloadObject(_this->gl_config.dll_handle);
   193     _this->gl_config.dll_handle = NULL;
   194 }
   195 
   196 SDL_GLContext
   197 Cocoa_GL_CreateContext(_THIS, SDL_Window * window)
   198 { @autoreleasepool
   199 {
   200     SDL_VideoDisplay *display = SDL_GetDisplayForWindow(window);
   201     SDL_DisplayData *displaydata = (SDL_DisplayData *)display->driverdata;
   202     SDL_bool lion_or_later = floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_6;
   203     NSOpenGLPixelFormatAttribute attr[32];
   204     NSOpenGLPixelFormat *fmt;
   205     SDLOpenGLContext *context;
   206     NSOpenGLContext *share_context = nil;
   207     int i = 0;
   208     const char *glversion;
   209     int glversion_major;
   210     int glversion_minor;
   211 
   212     if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) {
   213 #if SDL_VIDEO_OPENGL_EGL
   214         /* Switch to EGL based functions */
   215         Cocoa_GL_UnloadLibrary(_this);
   216         _this->GL_LoadLibrary = Cocoa_GLES_LoadLibrary;
   217         _this->GL_GetProcAddress = Cocoa_GLES_GetProcAddress;
   218         _this->GL_UnloadLibrary = Cocoa_GLES_UnloadLibrary;
   219         _this->GL_CreateContext = Cocoa_GLES_CreateContext;
   220         _this->GL_MakeCurrent = Cocoa_GLES_MakeCurrent;
   221         _this->GL_SetSwapInterval = Cocoa_GLES_SetSwapInterval;
   222         _this->GL_GetSwapInterval = Cocoa_GLES_GetSwapInterval;
   223         _this->GL_SwapWindow = Cocoa_GLES_SwapWindow;
   224         _this->GL_DeleteContext = Cocoa_GLES_DeleteContext;
   225         
   226         if (Cocoa_GLES_LoadLibrary(_this, NULL) != 0) {
   227             return NULL;
   228         }
   229         return Cocoa_GLES_CreateContext(_this, window);
   230 #else
   231         SDL_SetError("SDL not configured with EGL support");
   232         return NULL;
   233 #endif
   234     }
   235     if ((_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_CORE) && !lion_or_later) {
   236         SDL_SetError ("OpenGL Core Profile is not supported on this platform version");
   237         return NULL;
   238     }
   239 
   240     attr[i++] = NSOpenGLPFAAllowOfflineRenderers;
   241 
   242     /* specify a profile if we're on Lion (10.7) or later. */
   243     if (lion_or_later) {
   244         NSOpenGLPixelFormatAttribute profile = NSOpenGLProfileVersionLegacy;
   245         if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_CORE) {
   246             profile = NSOpenGLProfileVersion3_2Core;
   247         }
   248         attr[i++] = NSOpenGLPFAOpenGLProfile;
   249         attr[i++] = profile;
   250     }
   251 
   252     attr[i++] = NSOpenGLPFAColorSize;
   253     attr[i++] = SDL_BYTESPERPIXEL(display->current_mode.format)*8;
   254 
   255     attr[i++] = NSOpenGLPFADepthSize;
   256     attr[i++] = _this->gl_config.depth_size;
   257 
   258     if (_this->gl_config.double_buffer) {
   259         attr[i++] = NSOpenGLPFADoubleBuffer;
   260     }
   261 
   262     if (_this->gl_config.stereo) {
   263         attr[i++] = NSOpenGLPFAStereo;
   264     }
   265 
   266     if (_this->gl_config.stencil_size) {
   267         attr[i++] = NSOpenGLPFAStencilSize;
   268         attr[i++] = _this->gl_config.stencil_size;
   269     }
   270 
   271     if ((_this->gl_config.accum_red_size +
   272          _this->gl_config.accum_green_size +
   273          _this->gl_config.accum_blue_size +
   274          _this->gl_config.accum_alpha_size) > 0) {
   275         attr[i++] = NSOpenGLPFAAccumSize;
   276         attr[i++] = _this->gl_config.accum_red_size + _this->gl_config.accum_green_size + _this->gl_config.accum_blue_size + _this->gl_config.accum_alpha_size;
   277     }
   278 
   279     if (_this->gl_config.multisamplebuffers) {
   280         attr[i++] = NSOpenGLPFASampleBuffers;
   281         attr[i++] = _this->gl_config.multisamplebuffers;
   282     }
   283 
   284     if (_this->gl_config.multisamplesamples) {
   285         attr[i++] = NSOpenGLPFASamples;
   286         attr[i++] = _this->gl_config.multisamplesamples;
   287         attr[i++] = NSOpenGLPFANoRecovery;
   288     }
   289 
   290     if (_this->gl_config.accelerated >= 0) {
   291         if (_this->gl_config.accelerated) {
   292             attr[i++] = NSOpenGLPFAAccelerated;
   293         } else {
   294             attr[i++] = NSOpenGLPFARendererID;
   295             attr[i++] = kCGLRendererGenericFloatID;
   296         }
   297     }
   298 
   299     attr[i++] = NSOpenGLPFAScreenMask;
   300     attr[i++] = CGDisplayIDToOpenGLDisplayMask(displaydata->display);
   301     attr[i] = 0;
   302 
   303     fmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
   304     if (fmt == nil) {
   305         SDL_SetError("Failed creating OpenGL pixel format");
   306         return NULL;
   307     }
   308 
   309     if (_this->gl_config.share_with_current_context) {
   310         share_context = (NSOpenGLContext*)SDL_GL_GetCurrentContext();
   311     }
   312 
   313     context = [[SDLOpenGLContext alloc] initWithFormat:fmt shareContext:share_context];
   314 
   315     [fmt release];
   316 
   317     if (context == nil) {
   318         SDL_SetError("Failed creating OpenGL context");
   319         return NULL;
   320     }
   321 
   322     if ( Cocoa_GL_MakeCurrent(_this, window, context) < 0 ) {
   323         Cocoa_GL_DeleteContext(_this, context);
   324         SDL_SetError("Failed making OpenGL context current");
   325         return NULL;
   326     }
   327 
   328     if (_this->gl_config.major_version < 3 &&
   329         _this->gl_config.profile_mask == 0 &&
   330         _this->gl_config.flags == 0) {
   331         /* This is a legacy profile, so to match other backends, we're done. */
   332     } else {
   333         const GLubyte *(APIENTRY * glGetStringFunc)(GLenum);
   334 
   335         glGetStringFunc = (const GLubyte *(APIENTRY *)(GLenum)) SDL_GL_GetProcAddress("glGetString");
   336         if (!glGetStringFunc) {
   337             Cocoa_GL_DeleteContext(_this, context);
   338             SDL_SetError ("Failed getting OpenGL glGetString entry point");
   339             return NULL;
   340         }
   341 
   342         glversion = (const char *)glGetStringFunc(GL_VERSION);
   343         if (glversion == NULL) {
   344             Cocoa_GL_DeleteContext(_this, context);
   345             SDL_SetError ("Failed getting OpenGL context version");
   346             return NULL;
   347         }
   348 
   349         if (SDL_sscanf(glversion, "%d.%d", &glversion_major, &glversion_minor) != 2) {
   350             Cocoa_GL_DeleteContext(_this, context);
   351             SDL_SetError ("Failed parsing OpenGL context version");
   352             return NULL;
   353         }
   354 
   355         if ((glversion_major < _this->gl_config.major_version) ||
   356            ((glversion_major == _this->gl_config.major_version) && (glversion_minor < _this->gl_config.minor_version))) {
   357             Cocoa_GL_DeleteContext(_this, context);
   358             SDL_SetError ("Failed creating OpenGL context at version requested");
   359             return NULL;
   360         }
   361 
   362         /* In the future we'll want to do this, but to match other platforms
   363            we'll leave the OpenGL version the way it is for now
   364          */
   365         /*_this->gl_config.major_version = glversion_major;*/
   366         /*_this->gl_config.minor_version = glversion_minor;*/
   367     }
   368     return context;
   369 }}
   370 
   371 int
   372 Cocoa_GL_MakeCurrent(_THIS, SDL_Window * window, SDL_GLContext context)
   373 { @autoreleasepool
   374 {
   375     if (context) {
   376         SDLOpenGLContext *nscontext = (SDLOpenGLContext *)context;
   377         [nscontext setWindow:window];
   378         [nscontext updateIfNeeded];
   379         [nscontext makeCurrentContext];
   380     } else {
   381         [NSOpenGLContext clearCurrentContext];
   382     }
   383 
   384     return 0;
   385 }}
   386 
   387 void
   388 Cocoa_GL_GetDrawableSize(_THIS, SDL_Window * window, int * w, int * h)
   389 {
   390     SDL_WindowData *windata = (SDL_WindowData *) window->driverdata;
   391     NSView *contentView = [windata->nswindow contentView];
   392     NSRect viewport = [contentView bounds];
   393 
   394     if (window->flags & SDL_WINDOW_ALLOW_HIGHDPI) {
   395         /* This gives us the correct viewport for a Retina-enabled view, only
   396          * supported on 10.7+. */
   397         if ([contentView respondsToSelector:@selector(convertRectToBacking:)]) {
   398             viewport = [contentView convertRectToBacking:viewport];
   399         }
   400     }
   401 
   402     if (w) {
   403         *w = viewport.size.width;
   404     }
   405 
   406     if (h) {
   407         *h = viewport.size.height;
   408     }
   409 }
   410 
   411 int
   412 Cocoa_GL_SetSwapInterval(_THIS, int interval)
   413 { @autoreleasepool
   414 {
   415     SDLOpenGLContext *nscontext = (SDLOpenGLContext *) SDL_GL_GetCurrentContext();
   416     int status;
   417 
   418     if (nscontext == nil) {
   419         status = SDL_SetError("No current OpenGL context");
   420     } else {
   421         SDL_LockMutex(nscontext->swapIntervalMutex);
   422         SDL_AtomicSet(&nscontext->swapIntervalsPassed, 0);
   423         SDL_AtomicSet(&nscontext->swapIntervalSetting, interval);
   424         SDL_UnlockMutex(nscontext->swapIntervalMutex);
   425         status = 0;
   426     }
   427 
   428     return status;
   429 }}
   430 
   431 int
   432 Cocoa_GL_GetSwapInterval(_THIS)
   433 { @autoreleasepool
   434 {
   435     SDLOpenGLContext *nscontext = (SDLOpenGLContext *) SDL_GL_GetCurrentContext();
   436     return nscontext ? SDL_AtomicGet(&nscontext->swapIntervalSetting) : 0;
   437 }}
   438 
   439 int
   440 Cocoa_GL_SwapWindow(_THIS, SDL_Window * window)
   441 { @autoreleasepool
   442 {
   443     SDLOpenGLContext* nscontext = (SDLOpenGLContext*)SDL_GL_GetCurrentContext();
   444     SDL_VideoData *videodata = (SDL_VideoData *) _this->driverdata;
   445     const int setting = SDL_AtomicGet(&nscontext->swapIntervalSetting);
   446 
   447     if (setting == 0) {
   448         /* nothing to do if vsync is disabled, don't even lock */
   449     } else if (setting < 0) {  /* late swap tearing */
   450         SDL_LockMutex(nscontext->swapIntervalMutex);
   451         while (SDL_AtomicGet(&nscontext->swapIntervalsPassed) == 0) {
   452             SDL_CondWait(nscontext->swapIntervalCond, nscontext->swapIntervalMutex);
   453         }
   454         SDL_AtomicSet(&nscontext->swapIntervalsPassed, 0);
   455         SDL_UnlockMutex(nscontext->swapIntervalMutex);
   456     } else {
   457         SDL_LockMutex(nscontext->swapIntervalMutex);
   458         do {  /* always wait here so we know we just hit a swap interval. */
   459             SDL_CondWait(nscontext->swapIntervalCond, nscontext->swapIntervalMutex);
   460         } while ((SDL_AtomicGet(&nscontext->swapIntervalsPassed) % setting) != 0);
   461         SDL_AtomicSet(&nscontext->swapIntervalsPassed, 0);
   462         SDL_UnlockMutex(nscontext->swapIntervalMutex);
   463     }
   464 
   465     /* on 10.14 ("Mojave") and later, this deadlocks if two contexts in two
   466        threads try to swap at the same time, so put a mutex around it. */
   467     SDL_LockMutex(videodata->swaplock);
   468     [nscontext flushBuffer];
   469     [nscontext updateIfNeeded];
   470     SDL_UnlockMutex(videodata->swaplock);
   471     return 0;
   472 }}
   473 
   474 void
   475 Cocoa_GL_DeleteContext(_THIS, SDL_GLContext context)
   476 { @autoreleasepool
   477 {
   478     SDLOpenGLContext *nscontext = (SDLOpenGLContext *)context;
   479 
   480     [nscontext setWindow:NULL];
   481     [nscontext release];
   482 }}
   483 
   484 #endif /* SDL_VIDEO_OPENGL_CGL */
   485 
   486 /* vi: set ts=4 sw=4 expandtab: */