1/*
2  Simple DirectMedia Layer
3  Copyright (C) 1997-2016 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#include "SDL_assert.h"
23
24#if SDL_VIDEO_DRIVER_COCOA
25
26#include "SDL_cocoavideo.h"
27
28/* We need this for IODisplayCreateInfoDictionary and kIODisplayOnlyPreferredName */
29#include <IOKit/graphics/IOGraphicsLib.h>
30
31/* We need this for CVDisplayLinkGetNominalOutputVideoRefreshPeriod */
32#include <CoreVideo/CVBase.h>
33#include <CoreVideo/CVDisplayLink.h>
34
35/* we need this for ShowMenuBar() and HideMenuBar(). */
36#include <Carbon/Carbon.h>
37
38/* This gets us MAC_OS_X_VERSION_MIN_REQUIRED... */
39#include <AvailabilityMacros.h>
40
41
42static void
43Cocoa_ToggleMenuBar(const BOOL show)
44{
45    /* !!! FIXME: keep an eye on this.
46     * ShowMenuBar/HideMenuBar is officially unavailable for 64-bit binaries.
47     *  It happens to work, as of 10.7, but we're going to see if
48     *  we can just simply do without it on newer OSes...
49     */
50#if (MAC_OS_X_VERSION_MIN_REQUIRED < 1070) && !defined(__LP64__)
51    if (show) {
52        ShowMenuBar();
53    } else {
54        HideMenuBar();
55    }
56#endif
57}
58
59static int
60CG_SetError(const char *prefix, CGDisplayErr result)
61{
62    const char *error;
63
64    switch (result) {
65    case kCGErrorFailure:
66        error = "kCGErrorFailure";
67        break;
68    case kCGErrorIllegalArgument:
69        error = "kCGErrorIllegalArgument";
70        break;
71    case kCGErrorInvalidConnection:
72        error = "kCGErrorInvalidConnection";
73        break;
74    case kCGErrorInvalidContext:
75        error = "kCGErrorInvalidContext";
76        break;
77    case kCGErrorCannotComplete:
78        error = "kCGErrorCannotComplete";
79        break;
80    case kCGErrorNotImplemented:
81        error = "kCGErrorNotImplemented";
82        break;
83    case kCGErrorRangeCheck:
84        error = "kCGErrorRangeCheck";
85        break;
86    case kCGErrorTypeCheck:
87        error = "kCGErrorTypeCheck";
88        break;
89    case kCGErrorInvalidOperation:
90        error = "kCGErrorInvalidOperation";
91        break;
92    case kCGErrorNoneAvailable:
93        error = "kCGErrorNoneAvailable";
94        break;
95    default:
96        error = "Unknown Error";
97        break;
98    }
99    return SDL_SetError("%s: %s", prefix, error);
100}
101
102static SDL_bool
103GetDisplayMode(_THIS, CGDisplayModeRef vidmode, CVDisplayLinkRef link, SDL_DisplayMode *mode)
104{
105    SDL_DisplayModeData *data;
106    int width = 0;
107    int height = 0;
108    int bpp = 0;
109    int refreshRate = 0;
110    CFStringRef fmt;
111
112    data = (SDL_DisplayModeData *) SDL_malloc(sizeof(*data));
113    if (!data) {
114        return SDL_FALSE;
115    }
116    data->moderef = vidmode;
117
118    fmt = CGDisplayModeCopyPixelEncoding(vidmode);
119    width = (int) CGDisplayModeGetWidth(vidmode);
120    height = (int) CGDisplayModeGetHeight(vidmode);
121    refreshRate = (int) (CGDisplayModeGetRefreshRate(vidmode) + 0.5);
122
123    if (CFStringCompare(fmt, CFSTR(IO32BitDirectPixels),
124                        kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
125        bpp = 32;
126    } else if (CFStringCompare(fmt, CFSTR(IO16BitDirectPixels),
127                        kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
128        bpp = 16;
129    } else if (CFStringCompare(fmt, CFSTR(kIO30BitDirectPixels),
130                        kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
131        bpp = 30;
132    } else {
133        bpp = 0;  /* ignore 8-bit and such for now. */
134    }
135
136    CFRelease(fmt);
137
138    /* CGDisplayModeGetRefreshRate returns 0 for many non-CRT displays. */
139    if (refreshRate == 0 && link != NULL) {
140        CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
141        if ((time.flags & kCVTimeIsIndefinite) == 0 && time.timeValue != 0) {
142            refreshRate = (int) ((time.timeScale / (double) time.timeValue) + 0.5);
143        }
144    }
145
146    mode->format = SDL_PIXELFORMAT_UNKNOWN;
147    switch (bpp) {
148    case 16:
149        mode->format = SDL_PIXELFORMAT_ARGB1555;
150        break;
151    case 30:
152        mode->format = SDL_PIXELFORMAT_ARGB2101010;
153        break;
154    case 32:
155        mode->format = SDL_PIXELFORMAT_ARGB8888;
156        break;
157    case 8: /* We don't support palettized modes now */
158    default: /* Totally unrecognizable bit depth. */
159        SDL_free(data);
160        return SDL_FALSE;
161    }
162    mode->w = width;
163    mode->h = height;
164    mode->refresh_rate = refreshRate;
165    mode->driverdata = data;
166    return SDL_TRUE;
167}
168
169static const char *
170Cocoa_GetDisplayName(CGDirectDisplayID displayID)
171{
172    CFDictionaryRef deviceInfo = IODisplayCreateInfoDictionary(CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName);
173    NSDictionary *localizedNames = [(NSDictionary *)deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
174    const char* displayName = NULL;
175
176    if ([localizedNames count] > 0) {
177        displayName = SDL_strdup([[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] UTF8String]);
178    }
179    CFRelease(deviceInfo);
180    return displayName;
181}
182
183void
184Cocoa_InitModes(_THIS)
185{ @autoreleasepool
186{
187    CGDisplayErr result;
188    CGDirectDisplayID *displays;
189    CGDisplayCount numDisplays;
190    int pass, i;
191
192    result = CGGetOnlineDisplayList(0, NULL, &numDisplays);
193    if (result != kCGErrorSuccess) {
194        CG_SetError("CGGetOnlineDisplayList()", result);
195        return;
196    }
197    displays = SDL_stack_alloc(CGDirectDisplayID, numDisplays);
198    result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays);
199    if (result != kCGErrorSuccess) {
200        CG_SetError("CGGetOnlineDisplayList()", result);
201        SDL_stack_free(displays);
202        return;
203    }
204
205    /* Pick up the primary display in the first pass, then get the rest */
206    for (pass = 0; pass < 2; ++pass) {
207        for (i = 0; i < numDisplays; ++i) {
208            SDL_VideoDisplay display;
209            SDL_DisplayData *displaydata;
210            SDL_DisplayMode mode;
211            CGDisplayModeRef moderef = NULL;
212            CVDisplayLinkRef link = NULL;
213
214            if (pass == 0) {
215                if (!CGDisplayIsMain(displays[i])) {
216                    continue;
217                }
218            } else {
219                if (CGDisplayIsMain(displays[i])) {
220                    continue;
221                }
222            }
223
224            if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay) {
225                continue;
226            }
227
228            moderef = CGDisplayCopyDisplayMode(displays[i]);
229
230            if (!moderef) {
231                continue;
232            }
233
234            displaydata = (SDL_DisplayData *) SDL_malloc(sizeof(*displaydata));
235            if (!displaydata) {
236                CGDisplayModeRelease(moderef);
237                continue;
238            }
239            displaydata->display = displays[i];
240
241            CVDisplayLinkCreateWithCGDisplay(displays[i], &link);
242
243            SDL_zero(display);
244            /* this returns a stddup'ed string */
245            display.name = (char *)Cocoa_GetDisplayName(displays[i]);
246            if (!GetDisplayMode(_this, moderef, link, &mode)) {
247                CVDisplayLinkRelease(link);
248                CGDisplayModeRelease(moderef);
249                SDL_free(display.name);
250                SDL_free(displaydata);
251                continue;
252            }
253
254            CVDisplayLinkRelease(link);
255
256            display.desktop_mode = mode;
257            display.current_mode = mode;
258            display.driverdata = displaydata;
259            SDL_AddVideoDisplay(&display);
260            SDL_free(display.name);
261        }
262    }
263    SDL_stack_free(displays);
264}}
265
266int
267Cocoa_GetDisplayBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect)
268{
269    SDL_DisplayData *displaydata = (SDL_DisplayData *) display->driverdata;
270    CGRect cgrect;
271
272    cgrect = CGDisplayBounds(displaydata->display);
273    rect->x = (int)cgrect.origin.x;
274    rect->y = (int)cgrect.origin.y;
275    rect->w = (int)cgrect.size.width;
276    rect->h = (int)cgrect.size.height;
277    return 0;
278}
279
280int
281Cocoa_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect)
282{
283    SDL_DisplayData *displaydata = (SDL_DisplayData *) display->driverdata;
284    const CGDirectDisplayID cgdisplay = displaydata->display;
285    NSArray *screens = [NSScreen screens];
286    NSScreen *screen = nil;
287
288    /* !!! FIXME: maybe track the NSScreen in SDL_DisplayData? */
289    for (NSScreen *i in screens) {
290        const CGDirectDisplayID thisDisplay = (CGDirectDisplayID) [[[i deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
291        if (thisDisplay == cgdisplay) {
292            screen = i;
293            break;
294        }
295    }
296
297    SDL_assert(screen != nil);  /* didn't find it?! */
298    if (screen == nil) {
299        return -1;
300    }
301
302    const CGRect cgrect = CGDisplayBounds(cgdisplay);
303    const NSRect frame = [screen visibleFrame];
304
305    // !!! FIXME: I assume -[NSScreen visibleFrame] is relative to the origin of the screen in question and not the whole desktop.
306    // !!! FIXME: The math vs CGDisplayBounds might be incorrect if that's not the case, though. Check this.
307    rect->x = (int)(cgrect.origin.x + frame.origin.x);
308    rect->y = (int)(cgrect.origin.y + frame.origin.y);
309    rect->w = (int)frame.size.width;
310    rect->h = (int)frame.size.height;
311
312    return 0;
313}
314
315int
316Cocoa_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi, float * hdpi, float * vdpi)
317{
318    const float MM_IN_INCH = 25.4f;
319
320    SDL_DisplayData *data = (SDL_DisplayData *) display->driverdata;
321
322    CGSize displaySize = CGDisplayScreenSize(data->display);
323    int pixelWidth =  (int) CGDisplayPixelsWide(data->display);
324    int pixelHeight = (int) CGDisplayPixelsHigh(data->display);
325
326    if (ddpi) {
327        *ddpi = SDL_ComputeDiagonalDPI(pixelWidth, pixelHeight, displaySize.width / MM_IN_INCH, displaySize.height / MM_IN_INCH);
328    }
329    if (hdpi) {
330        *hdpi = pixelWidth * MM_IN_INCH / displaySize.width;
331    }
332    if (vdpi) {
333        *vdpi = pixelHeight * MM_IN_INCH / displaySize.height;
334    }
335
336    return 0;
337}
338
339void
340Cocoa_GetDisplayModes(_THIS, SDL_VideoDisplay * display)
341{
342    SDL_DisplayData *data = (SDL_DisplayData *) display->driverdata;
343    CFArrayRef modes = CGDisplayCopyAllDisplayModes(data->display, NULL);
344
345    if (modes) {
346        CVDisplayLinkRef link = NULL;
347        const CFIndex count = CFArrayGetCount(modes);
348        CFIndex i;
349
350        CVDisplayLinkCreateWithCGDisplay(data->display, &link);
351
352        for (i = 0; i < count; i++) {
353            CGDisplayModeRef moderef = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i);
354            SDL_DisplayMode mode;
355            if (GetDisplayMode(_this, moderef, link, &mode)) {
356                CGDisplayModeRetain(moderef);
357                SDL_AddDisplayMode(display, &mode);
358            }
359        }
360
361        CVDisplayLinkRelease(link);
362        CFRelease(modes);
363    }
364}
365
366int
367Cocoa_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode)
368{
369    SDL_DisplayData *displaydata = (SDL_DisplayData *) display->driverdata;
370    SDL_DisplayModeData *data = (SDL_DisplayModeData *) mode->driverdata;
371    CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken;
372    CGError result;
373
374    /* Fade to black to hide resolution-switching flicker */
375    if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess) {
376        CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE);
377    }
378
379    if (data == display->desktop_mode.driverdata) {
380        /* Restoring desktop mode */
381        CGDisplaySetDisplayMode(displaydata->display, data->moderef, NULL);
382
383        if (CGDisplayIsMain(displaydata->display)) {
384            CGReleaseAllDisplays();
385        } else {
386            CGDisplayRelease(displaydata->display);
387        }
388
389        if (CGDisplayIsMain(displaydata->display)) {
390            Cocoa_ToggleMenuBar(YES);
391        }
392    } else {
393        /* Put up the blanking window (a window above all other windows) */
394        if (CGDisplayIsMain(displaydata->display)) {
395            /* If we don't capture all displays, Cocoa tries to rearrange windows... *sigh* */
396            result = CGCaptureAllDisplays();
397        } else {
398            result = CGDisplayCapture(displaydata->display);
399        }
400        if (result != kCGErrorSuccess) {
401            CG_SetError("CGDisplayCapture()", result);
402            goto ERR_NO_CAPTURE;
403        }
404
405        /* Do the physical switch */
406        result = CGDisplaySetDisplayMode(displaydata->display, data->moderef, NULL);
407        if (result != kCGErrorSuccess) {
408            CG_SetError("CGDisplaySwitchToMode()", result);
409            goto ERR_NO_SWITCH;
410        }
411
412        /* Hide the menu bar so it doesn't intercept events */
413        if (CGDisplayIsMain(displaydata->display)) {
414            Cocoa_ToggleMenuBar(NO);
415        }
416    }
417
418    /* Fade in again (asynchronously) */
419    if (fade_token != kCGDisplayFadeReservationInvalidToken) {
420        CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE);
421        CGReleaseDisplayFadeReservation(fade_token);
422    }
423
424    return 0;
425
426    /* Since the blanking window covers *all* windows (even force quit) correct recovery is crucial */
427ERR_NO_SWITCH:
428    CGDisplayRelease(displaydata->display);
429ERR_NO_CAPTURE:
430    if (fade_token != kCGDisplayFadeReservationInvalidToken) {
431        CGDisplayFade (fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE);
432        CGReleaseDisplayFadeReservation(fade_token);
433    }
434    return -1;
435}
436
437void
438Cocoa_QuitModes(_THIS)
439{
440    int i, j;
441
442    for (i = 0; i < _this->num_displays; ++i) {
443        SDL_VideoDisplay *display = &_this->displays[i];
444        SDL_DisplayModeData *mode;
445
446        if (display->current_mode.driverdata != display->desktop_mode.driverdata) {
447            Cocoa_SetDisplayMode(_this, display, &display->desktop_mode);
448        }
449
450        mode = (SDL_DisplayModeData *) display->desktop_mode.driverdata;
451        CGDisplayModeRelease(mode->moderef);
452
453        for (j = 0; j < display->num_display_modes; j++) {
454            mode = (SDL_DisplayModeData*) display->display_modes[j].driverdata;
455            CGDisplayModeRelease(mode->moderef);
456        }
457
458    }
459    Cocoa_ToggleMenuBar(YES);
460}
461
462#endif /* SDL_VIDEO_DRIVER_COCOA */
463
464/* vi: set ts=4 sw=4 expandtab: */
465