From 38176bfe9ab6eda2620bebc9a96f95a644671d25 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 10 Jan 2025 13:23:55 -0500 Subject: [PATCH] cocoa: Implemented display hotplugging support. Fixes #7764. --- src/video/SDL_sysvideo.h | 2 + src/video/SDL_video.c | 2 +- src/video/cocoa/SDL_cocoamodes.h | 1 + src/video/cocoa/SDL_cocoamodes.m | 225 +++++++++++++++++++++++------- src/video/cocoa/SDL_cocoawindow.m | 14 +- 5 files changed, 183 insertions(+), 61 deletions(-) diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index 5ac9db26ee..418b8239f1 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -586,6 +586,8 @@ extern bool SDL_ShouldAllowTopmost(void); extern void SDL_ToggleDragAndDropSupport(void); +extern void SDL_UpdateDesktopBounds(void); + extern SDL_TextInputType SDL_GetTextInputType(SDL_PropertiesID props); extern SDL_Capitalization SDL_GetTextInputCapitalization(SDL_PropertiesID props); extern bool SDL_GetTextInputAutocorrect(SDL_PropertiesID props); diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 6808e14a40..3b5fa4d84d 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -743,7 +743,7 @@ SDL_SystemTheme SDL_GetSystemTheme(void) } } -static void SDL_UpdateDesktopBounds(void) +void SDL_UpdateDesktopBounds(void) { SDL_Rect rect; SDL_zero(rect); diff --git a/src/video/cocoa/SDL_cocoamodes.h b/src/video/cocoa/SDL_cocoamodes.h index fd4e18998d..37f3aa5840 100644 --- a/src/video/cocoa/SDL_cocoamodes.h +++ b/src/video/cocoa/SDL_cocoamodes.h @@ -40,5 +40,6 @@ extern bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDispla extern bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display); extern bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode); extern void Cocoa_QuitModes(SDL_VideoDevice *_this); +extern SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid); #endif // SDL_cocoamodes_h_ diff --git a/src/video/cocoa/SDL_cocoamodes.m b/src/video/cocoa/SDL_cocoamodes.m index b309c3ab33..218571860d 100644 --- a/src/video/cocoa/SDL_cocoamodes.m +++ b/src/video/cocoa/SDL_cocoamodes.m @@ -23,6 +23,7 @@ #ifdef SDL_VIDEO_DRIVER_COCOA #include "SDL_cocoavideo.h" +#include "../../events/SDL_events_c.h" // We need this for IODisplayCreateInfoDictionary and kIODisplayOnlyPreferredName #include @@ -97,6 +98,17 @@ static NSScreen *GetNSScreenForDisplayID(CGDirectDisplayID displayID) 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); @@ -153,7 +165,7 @@ static Uint32 GetDisplayModePixelFormat(CGDisplayModeRef vidmode) return pixelformat; } -static bool GetDisplayMode(SDL_VideoDevice *_this, CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode) +static bool GetDisplayMode(CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode) { SDL_DisplayModeData *data; bool usableForGUI = CGDisplayModeIsUsableForDesktopGUI(vidmode); @@ -309,21 +321,169 @@ static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDROutputPro #endif } + +bool Cocoa_AddDisplay(CGDirectDisplayID display, bool send_event) +{ + CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(display); + if (!moderef) { + return false; + } + + SDL_DisplayData *displaydata = (SDL_DisplayData *)SDL_malloc(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); // this returns a strdup'ed string + + 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); + + 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 + SDL_Log("COCOA DISPLAY RECONFIG CALLBACK! display=%u", (unsigned int) displayid); + #define CHECK_DISPLAY_RECONFIG_FLAG(x) if (flags & x) { SDL_Log(" - " #x); } + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayBeginConfigurationFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMovedFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetMainFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetModeFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayAddFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayRemoveFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayEnabledFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDisabledFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMirrorFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayUnMirrorFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDesktopShapeChangedFlag); + #undef CHECK_DISPLAY_RECONFIG_FLAG + #endif + + SDL_VideoDevice *_this = (SDL_VideoDevice *) userInfo; + SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); // will be NULL for newly-added (or newly-unmirrored) displays! + + if (flags & kCGDisplayDisabledFlag) { + flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still plugged in. + } + + if (flags & kCGDisplayEnabledFlag) { + flags |= kCGDisplayAddFlag; // treat this like a display leaving, even though it's still plugged in. + } + + if (flags & kCGDisplayMirrorFlag) { + flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still actually here. + } + + if (flags & kCGDisplayUnMirrorFlag) { + flags |= kCGDisplayAddFlag; // treat this like a new display arriving, even though it was here all along. + } + + if ((flags & kCGDisplayAddFlag) && (flags & kCGDisplayRemoveFlag)) { + // both adding _and_ removing? Treat it as a remove exclusively. This can happen if a display is unmirroring because it's being disabled, etc. + flags &= ~kCGDisplayAddFlag; + } + + if (flags & kCGDisplayAddFlag) { + if (!display) { + if (!Cocoa_AddDisplay(displayid, true)) { + return; // oh well. + } + 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_SetCurrentDisplayMode(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) { + // move this display to the front of _this->displays so it's treated as primary. + SDL_memmove(&_this->displays[1], &_this->displays[0], sizeof (*_this->displays) * i); + _this->displays[0] = display; + } + flags |= kCGDisplayMovedFlag; // we don't have an SDL event atm for "this display became primary," so at least let everyone know it "moved". + break; + } + } + } + } + + if (flags & kCGDisplayMovedFlag) { + if (display) { + SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_MOVED, 0, 0); + } + } + + if (flags & kCGDisplayDesktopShapeChangedFlag) { + SDL_UpdateDesktopBounds(); + } +} + void Cocoa_InitModes(SDL_VideoDevice *_this) { @autoreleasepool { CGDisplayErr result; - CGDirectDisplayID *displays; - CGDisplayCount numDisplays; - bool isstack; - int pass, i; + CGDisplayCount numDisplays = 0; result = CGGetOnlineDisplayList(0, NULL, &numDisplays); if (result != kCGErrorSuccess) { CG_SetError("CGGetOnlineDisplayList()", result); return; } - displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack); + + bool isstack; + CGDirectDisplayID *displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack); + result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays); if (result != kCGErrorSuccess) { CG_SetError("CGGetOnlineDisplayList()", result); @@ -331,15 +491,12 @@ void Cocoa_InitModes(SDL_VideoDevice *_this) return; } - // Pick up the primary display in the first pass, then get the rest - for (pass = 0; pass < 2; ++pass) { - for (i = 0; i < numDisplays; ++i) { - SDL_VideoDisplay display; - SDL_DisplayData *displaydata; - SDL_DisplayMode mode; - CGDisplayModeRef moderef = NULL; - CVDisplayLinkRef link = NULL; + // future updates to the display graph will come through this callback. + CGDisplayRegisterReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this); + // Pick up the primary display in the first pass, then get the rest + for (int pass = 0; pass < 2; ++pass) { + for (int i = 0; i < numDisplays; ++i) { if (pass == 0) { if (!CGDisplayIsMain(displays[i])) { continue; @@ -354,41 +511,7 @@ void Cocoa_InitModes(SDL_VideoDevice *_this) continue; } - moderef = CGDisplayCopyDisplayMode(displays[i]); - - if (!moderef) { - continue; - } - - displaydata = (SDL_DisplayData *)SDL_malloc(sizeof(*displaydata)); - if (!displaydata) { - CGDisplayModeRelease(moderef); - continue; - } - displaydata->display = displays[i]; - - CVDisplayLinkCreateWithCGDisplay(displays[i], &link); - - SDL_zero(display); - // this returns a strdup'ed string - display.name = Cocoa_GetDisplayName(displays[i]); - if (!GetDisplayMode(_this, moderef, true, NULL, link, &mode)) { - CVDisplayLinkRelease(link); - CGDisplayModeRelease(moderef); - SDL_free(display.name); - SDL_free(displaydata); - continue; - } - - CVDisplayLinkRelease(link); - CGDisplayModeRelease(moderef); - - Cocoa_GetHDRProperties(displaydata->display, &display.HDR); - - display.desktop_mode = mode; - display.internal = displaydata; - SDL_AddVideoDisplay(&display, false); - SDL_free(display.name); + Cocoa_AddDisplay(displays[i], false); } } SDL_small_free(displays, isstack); @@ -486,7 +609,7 @@ bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display) CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i); SDL_DisplayMode mode; - if (GetDisplayMode(_this, moderef, false, modes, link, &mode)) { + if (GetDisplayMode(moderef, false, modes, link, &mode)) { if (!SDL_AddFullscreenDisplayMode(display, &mode)) { CFRelease(mode.internal->modes); SDL_free(mode.internal); @@ -559,6 +682,8 @@ 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; diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m index cfc1d23159..9ae7b16350 100644 --- a/src/video/cocoa/SDL_cocoawindow.m +++ b/src/video/cocoa/SDL_cocoawindow.m @@ -2934,17 +2934,11 @@ SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *wind screen = data.nswindow.screen; if (screen != nil) { - CGDirectDisplayID displayid; - int i; - // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc - displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue]; - - for (i = 0; i < _this->num_displays; i++) { - SDL_DisplayData *displaydata = _this->displays[i]->internal; - if (displaydata != NULL && displaydata->display == displayid) { - return _this->displays[i]->id; - } + CGDirectDisplayID displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue]; + SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); + if (display) { + return display->id; } }