#include "SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_COCOA
#include "SDL_cocoavideo.h"
#include "../../events/SDL_events_c.h"
#include <IOKit/graphics/IOGraphicsLib.h>
#include <CoreVideo/CVBase.h>
#include <CoreVideo/CVDisplayLink.h>
#if (IOGRAPHICSTYPES_REV < 40)
#define kDisplayModeNativeFlag 0x02000000
#endif
static bool CG_SetError(const char *prefix, CGDisplayErr result)
{
const char *error;
switch (result) {
case kCGErrorFailure:
error = "kCGErrorFailure";
break;
case kCGErrorIllegalArgument:
error = "kCGErrorIllegalArgument";
break;
case kCGErrorInvalidConnection:
error = "kCGErrorInvalidConnection";
break;
case kCGErrorInvalidContext:
error = "kCGErrorInvalidContext";
break;
case kCGErrorCannotComplete:
error = "kCGErrorCannotComplete";
break;
case kCGErrorNotImplemented:
error = "kCGErrorNotImplemented";
break;
case kCGErrorRangeCheck:
error = "kCGErrorRangeCheck";
break;
case kCGErrorTypeCheck:
error = "kCGErrorTypeCheck";
break;
case kCGErrorInvalidOperation:
error = "kCGErrorInvalidOperation";
break;
case kCGErrorNoneAvailable:
error = "kCGErrorNoneAvailable";
break;
default:
error = "Unknown Error";
break;
}
return SDL_SetError("%s: %s", prefix, error);
}
static NSScreen *GetNSScreenForDisplayID(CGDirectDisplayID displayID)
{
NSArray *screens = [NSScreen screens];
for (NSScreen *screen in screens) {
const CGDirectDisplayID thisDisplay = (CGDirectDisplayID)[[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
if (thisDisplay == displayID) {
return screen;
}
}
return nil;
}
SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid)
{
for (int i = 0; i < _this->num_displays; i++) {
const SDL_DisplayData *displaydata = _this->displays[i]->internal;
if (displaydata && (displaydata->display == displayid)) {
return _this->displays[i];
}
}
return NULL;
}
static float GetDisplayModeRefreshRate(CGDisplayModeRef vidmode, CVDisplayLinkRef link)
{
float refreshRate = (float)CGDisplayModeGetRefreshRate(vidmode);
if (refreshRate == 0 && link != NULL) {
CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
if ((time.flags & kCVTimeIsIndefinite) == 0 && time.timeValue != 0) {
refreshRate = (float)time.timeScale / time.timeValue;
}
}
return refreshRate;
}
static bool HasValidDisplayModeFlags(CGDisplayModeRef vidmode)
{
uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode);
if (ioflags & (kDisplayModeNeverShowFlag | kDisplayModeNotGraphicsQualityFlag)) {
return false;
}
if (!(ioflags & kDisplayModeValidFlag) || !(ioflags & kDisplayModeSafeFlag)) {
return false;
}
return true;
}
static Uint32 GetDisplayModePixelFormat(CGDisplayModeRef vidmode)
{
CFStringRef fmt = CGDisplayModeCopyPixelEncoding(vidmode);
Uint32 pixelformat = SDL_PIXELFORMAT_UNKNOWN;
if (CFStringCompare(fmt, CFSTR(IO32BitDirectPixels),
kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
pixelformat = SDL_PIXELFORMAT_ARGB8888;
} else if (CFStringCompare(fmt, CFSTR(IO16BitDirectPixels),
kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
pixelformat = SDL_PIXELFORMAT_ARGB1555;
} else if (CFStringCompare(fmt, CFSTR(kIO30BitDirectPixels),
kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
pixelformat = SDL_PIXELFORMAT_ARGB2101010;
} else {
}
CFRelease(fmt);
return pixelformat;
}
static bool GetDisplayMode(CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode)
{
SDL_DisplayModeData *data;
bool usableForGUI = CGDisplayModeIsUsableForDesktopGUI(vidmode);
size_t width = CGDisplayModeGetWidth(vidmode);
size_t height = CGDisplayModeGetHeight(vidmode);
size_t pixelW = width;
size_t pixelH = height;
uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode);
float refreshrate = GetDisplayModeRefreshRate(vidmode, link);
Uint32 format = GetDisplayModePixelFormat(vidmode);
bool interlaced = (ioflags & kDisplayModeInterlacedFlag) != 0;
CFMutableArrayRef modes;
if (format == SDL_PIXELFORMAT_UNKNOWN) {
return false;
}
if (!vidmodeCurrent && !HasValidDisplayModeFlags(vidmode)) {
return false;
}
modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(modes, vidmode);
pixelW = CGDisplayModeGetPixelWidth(vidmode);
pixelH = CGDisplayModeGetPixelHeight(vidmode);
if (modelist != NULL) {
CFIndex modescount = CFArrayGetCount(modelist);
int i;
for (i = 0; i < modescount; i++) {
size_t otherW, otherH, otherpixelW, otherpixelH;
float otherrefresh;
Uint32 otherformat;
bool otherGUI;
CGDisplayModeRef othermode = (CGDisplayModeRef)CFArrayGetValueAtIndex(modelist, i);
uint32_t otherioflags = CGDisplayModeGetIOFlags(othermode);
if (CFEqual(vidmode, othermode)) {
continue;
}
if (!HasValidDisplayModeFlags(othermode)) {
continue;
}
otherW = CGDisplayModeGetWidth(othermode);
otherH = CGDisplayModeGetHeight(othermode);
otherpixelW = CGDisplayModeGetPixelWidth(othermode);
otherpixelH = CGDisplayModeGetPixelHeight(othermode);
otherrefresh = GetDisplayModeRefreshRate(othermode, link);
otherformat = GetDisplayModePixelFormat(othermode);
otherGUI = CGDisplayModeIsUsableForDesktopGUI(othermode);
if (interlaced && ((otherioflags & kDisplayModeInterlacedFlag) == 0) && width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && refreshrate == otherrefresh && format == otherformat && usableForGUI == otherGUI) {
CFRelease(modes);
return false;
}
if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && !usableForGUI && otherGUI && refreshrate == otherrefresh && format == otherformat) {
CFRelease(modes);
return false;
}
if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && usableForGUI == otherGUI && refreshrate == otherrefresh && format == otherformat) {
CFArrayAppendValue(modes, othermode);
}
}
}
SDL_zerop(mode);
data = (SDL_DisplayModeData *)SDL_malloc(sizeof(*data));
if (!data) {
CFRelease(modes);
return false;
}
data->modes = modes;
mode->format = format;
mode->w = (int)width;
mode->h = (int)height;
mode->pixel_density = (float)pixelW / width;
mode->refresh_rate = refreshrate;
mode->internal = data;
return true;
}
static char *Cocoa_GetDisplayName(CGDirectDisplayID displayID)
{
if (@available(macOS 10.15, *)) {
NSScreen *screen = GetNSScreenForDisplayID(displayID);
if (screen) {
const char *name = [screen.localizedName UTF8String];
if (name) {
return SDL_strdup(name);
}
}
}
io_service_t servicePort = CGDisplayIOServicePort(displayID);
CFDictionaryRef deviceInfo = IODisplayCreateInfoDictionary(servicePort, kIODisplayOnlyPreferredName);
NSDictionary *localizedNames = [(__bridge NSDictionary *)deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
char *displayName = NULL;
if ([localizedNames count] > 0) {
displayName = SDL_strdup([[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] UTF8String]);
}
CFRelease(deviceInfo);
return displayName;
}
static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDROutputProperties *HDR)
{
HDR->SDR_white_level = 1.0f;
HDR->HDR_headroom = 1.0f;
if (@available(macOS 10.15, *)) {
NSScreen *screen = GetNSScreenForDisplayID(displayID);
if (screen) {
if (screen.maximumExtendedDynamicRangeColorComponentValue > 1.0f) {
HDR->HDR_headroom = screen.maximumExtendedDynamicRangeColorComponentValue;
} else {
HDR->HDR_headroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue;
}
}
}
}
static bool Cocoa_GetUsableBounds(CGDirectDisplayID displayID, SDL_Rect *rect)
{
NSScreen *screen = GetNSScreenForDisplayID(displayID);
if (screen == nil) {
return false;
}
SDL_VideoDevice *device = SDL_GetVideoDevice();
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)device->internal;
const NSRect frame = [screen visibleFrame];
rect->x = (int)frame.origin.x;
rect->y = (int)(data.mainDisplayHeight - frame.origin.y - frame.size.height);
rect->w = (int)frame.size.width;
rect->h = (int)frame.size.height;
return true;
}
bool Cocoa_AddDisplay(CGDirectDisplayID display, bool send_event)
{
CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(display);
if (!moderef) {
return false;
}
SDL_DisplayData *displaydata = (SDL_DisplayData *)SDL_calloc(1, sizeof(*displaydata));
if (!displaydata) {
CGDisplayModeRelease(moderef);
return false;
}
displaydata->display = display;
CVDisplayLinkRef link = NULL;
CVDisplayLinkCreateWithCGDisplay(display, &link);
SDL_VideoDisplay viddisplay;
SDL_zero(viddisplay);
viddisplay.name = Cocoa_GetDisplayName(display);
SDL_DisplayMode mode;
if (!GetDisplayMode(moderef, true, NULL, link, &mode)) {
CVDisplayLinkRelease(link);
CGDisplayModeRelease(moderef);
SDL_free(viddisplay.name);
SDL_free(displaydata);
return false;
}
CVDisplayLinkRelease(link);
CGDisplayModeRelease(moderef);
Cocoa_GetHDRProperties(displaydata->display, &viddisplay.HDR);
Cocoa_GetUsableBounds(displaydata->display, &displaydata->usable_bounds);
viddisplay.desktop_mode = mode;
viddisplay.internal = displaydata;
const bool retval = SDL_AddVideoDisplay(&viddisplay, send_event);
SDL_free(viddisplay.name);
return retval;
}
static void Cocoa_DisplayReconfigurationCallback(CGDirectDisplayID displayid, CGDisplayChangeSummaryFlags flags, void *userInfo)
{
#if 0 #endif
SDL_VideoDevice *_this = (SDL_VideoDevice *) userInfo;
SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
if (flags & kCGDisplayDisabledFlag) {
flags |= kCGDisplayRemoveFlag; }
if (flags & kCGDisplayEnabledFlag) {
flags |= kCGDisplayAddFlag; }
if (flags & kCGDisplayMirrorFlag) {
flags |= kCGDisplayRemoveFlag; }
if (flags & kCGDisplayUnMirrorFlag) {
flags |= kCGDisplayAddFlag; }
if ((flags & kCGDisplayAddFlag) && (flags & kCGDisplayRemoveFlag)) {
if (((flags & kCGDisplayMirrorFlag) == 0) && (CGDisplayPixelsWide(displayid) > 1)) {
flags &= ~kCGDisplayRemoveFlag;
} else {
flags &= ~kCGDisplayAddFlag;
}
}
if (flags & kCGDisplayAddFlag) {
if (!display) {
if (!Cocoa_AddDisplay(displayid, true)) {
return; }
display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
SDL_assert(display != NULL);
}
}
if (flags & kCGDisplayRemoveFlag) {
if (display) {
SDL_DelVideoDisplay(display->id, true);
display = NULL;
}
}
if (flags & kCGDisplaySetModeFlag) {
if (display) {
CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(displayid);
if (moderef) {
CVDisplayLinkRef link = NULL;
CVDisplayLinkCreateWithCGDisplay(displayid, &link);
if (link) {
SDL_DisplayMode mode;
if (GetDisplayMode(moderef, true, NULL, link, &mode)) {
SDL_SetDesktopDisplayMode(display, &mode);
}
CVDisplayLinkRelease(link);
}
CGDisplayModeRelease(moderef);
}
}
}
if (flags & kCGDisplaySetMainFlag) {
if (display) {
for (int i = 0; i < _this->num_displays; i++) {
if (_this->displays[i] == display) {
if (i > 0) {
SDL_memmove(&_this->displays[1], &_this->displays[0], sizeof (*_this->displays) * i);
_this->displays[0] = display;
}
flags |= kCGDisplayMovedFlag; break;
}
}
}
}
if (flags & kCGDisplayMovedFlag) {
if (display) {
SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_MOVED, 0, 0);
}
}
if (flags & kCGDisplayDesktopShapeChangedFlag) {
SDL_UpdateDesktopBounds();
}
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
data.mainDisplayHeight = CGDisplayPixelsHigh(kCGDirectMainDisplay);
}
void Cocoa_InitModes(SDL_VideoDevice *_this)
{
@autoreleasepool {
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
CGDisplayErr result;
CGDisplayCount numDisplays = 0;
data.mainDisplayHeight = CGDisplayPixelsHigh(kCGDirectMainDisplay);
result = CGGetOnlineDisplayList(0, NULL, &numDisplays);
if (result != kCGErrorSuccess) {
CG_SetError("CGGetOnlineDisplayList()", result);
return;
}
bool isstack;
CGDirectDisplayID *displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack);
result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays);
if (result != kCGErrorSuccess) {
CG_SetError("CGGetOnlineDisplayList()", result);
SDL_small_free(displays, isstack);
return;
}
CGDisplayRegisterReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
for (int pass = 0; pass < 2; ++pass) {
for (int i = 0; i < numDisplays; ++i) {
if (pass == 0) {
if (!CGDisplayIsMain(displays[i])) {
continue;
}
} else {
if (CGDisplayIsMain(displays[i])) {
continue;
}
}
if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay) {
continue;
}
Cocoa_AddDisplay(displays[i], false);
}
}
SDL_small_free(displays, isstack);
}
}
void Cocoa_UpdateDisplays(SDL_VideoDevice *_this)
{
SDL_HDROutputProperties HDR;
int i;
for (i = 0; i < _this->num_displays; ++i) {
SDL_VideoDisplay *display = _this->displays[i];
SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
Cocoa_GetHDRProperties(displaydata->display, &HDR);
SDL_SetDisplayHDRProperties(display, &HDR);
SDL_Rect rect;
if (Cocoa_GetUsableBounds(displaydata->display, &rect) &&
SDL_memcmp(&displaydata->usable_bounds, &rect, sizeof(rect)) != 0) {
SDL_memcpy(&displaydata->usable_bounds, &rect, sizeof(rect));
SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED, 0, 0);
}
}
}
bool Cocoa_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect)
{
SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
CGRect cgrect;
cgrect = CGDisplayBounds(displaydata->display);
rect->x = (int)cgrect.origin.x;
rect->y = (int)cgrect.origin.y;
rect->w = (int)cgrect.size.width;
rect->h = (int)cgrect.size.height;
return true;
}
bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect)
{
SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
SDL_memcpy(rect, &displaydata->usable_bounds, sizeof(*rect));
return true;
}
bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display)
{
SDL_DisplayData *data = (SDL_DisplayData *)display->internal;
CVDisplayLinkRef link = NULL;
CFArrayRef modes;
CFDictionaryRef dict = NULL;
const CFStringRef dictkeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
const CFBooleanRef dictvalues[] = { kCFBooleanTrue };
CVDisplayLinkCreateWithCGDisplay(data->display, &link);
dict = CFDictionaryCreate(NULL,
(const void **)dictkeys,
(const void **)dictvalues,
1,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
modes = CGDisplayCopyAllDisplayModes(data->display, dict);
if (dict) {
CFRelease(dict);
}
if (modes) {
CFIndex i;
const CFIndex count = CFArrayGetCount(modes);
for (i = 0; i < count; i++) {
CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i);
SDL_DisplayMode mode;
if (GetDisplayMode(moderef, false, modes, link, &mode)) {
if (!SDL_AddFullscreenDisplayMode(display, &mode)) {
CFRelease(mode.internal->modes);
SDL_free(mode.internal);
}
}
}
CFRelease(modes);
}
CVDisplayLinkRelease(link);
return true;
}
static CGError SetDisplayModeForDisplay(CGDirectDisplayID display, SDL_DisplayModeData *data)
{
CGError result = kCGErrorFailure;
for (CFIndex i = 0; i < CFArrayGetCount(data->modes); i++) {
CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(data->modes, i);
result = CGDisplaySetDisplayMode(display, moderef, NULL);
if (result == kCGErrorSuccess) {
if (i > 0) {
CFArrayExchangeValuesAtIndices(data->modes, i, 0);
}
break;
}
}
return result;
}
bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode)
{
SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
SDL_DisplayModeData *data = mode->internal;
CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken;
CGError result = kCGErrorSuccess;
b_inModeTransition = true;
if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess) {
CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE);
}
if (data == display->desktop_mode.internal) {
SetDisplayModeForDisplay(displaydata->display, data);
} else {
result = SetDisplayModeForDisplay(displaydata->display, data);
}
if (fade_token != kCGDisplayFadeReservationInvalidToken) {
CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE);
CGReleaseDisplayFadeReservation(fade_token);
}
b_inModeTransition = false;
if (result != kCGErrorSuccess) {
return CG_SetError("CGDisplaySwitchToMode()", result);
}
return true;
}
void Cocoa_QuitModes(SDL_VideoDevice *_this)
{
int i, j;
CGDisplayRemoveReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
for (i = 0; i < _this->num_displays; ++i) {
SDL_VideoDisplay *display = _this->displays[i];
SDL_DisplayModeData *mode;
if (display->current_mode->internal != display->desktop_mode.internal) {
Cocoa_SetDisplayMode(_this, display, &display->desktop_mode);
}
mode = display->desktop_mode.internal;
CFRelease(mode->modes);
for (j = 0; j < display->num_fullscreen_modes; j++) {
mode = display->fullscreen_modes[j].internal;
CFRelease(mode->modes);
}
}
}
#endif