2424#include " ../video_driver.h"
2525#include " ../../ui/drivers/cocoa/apple_platform.h"
2626#include " ../../ui/drivers/cocoa/cocoa_common.h"
27+ #include " ../../configuration.h"
2728
2829#ifdef OSX
2930#import < AppKit/AppKit.h>
@@ -85,78 +86,282 @@ static bool apple_display_server_set_window_decorations(void *data, bool on)
8586}
8687#endif
8788
89+ #if defined(OSX) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
8890static bool apple_display_server_set_resolution (void *data,
8991 unsigned width, unsigned height, int int_hz, float hz,
9092 int center, int monitor_index, int xoffset, int padjust)
9193{
92- #if (defined(OSX) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000)
94+ CocoaView *view = [CocoaView get ];
95+ if (@available (macOS 14.0 , *))
96+ {
97+ if (!view || !view.displayLink )
98+ {
99+ RARCH_WARN (" [Video] CocoaView not ready, skipping refresh rate change to %.3f Hz\n " , hz);
100+ return false ;
101+ }
102+ }
103+ else
104+ {
105+ RARCH_WARN (" [Video] displayLink not supported on this macOS version, skipping refresh rate change to %.3f Hz\n " , hz);
106+ return false ;
107+ }
108+
109+ /* macOS: Support resolution changes in addition to refresh rate */
110+ if (width > 0 && height > 0 )
111+ {
112+ CGDirectDisplayID mainDisplayID = CGMainDisplayID ();
113+ CFArrayRef displayModes = CGDisplayCopyAllDisplayModes (mainDisplayID, NULL );
114+ CGDisplayModeRef bestMode = NULL ;
115+
116+ RARCH_LOG (" [Video] Looking for display mode: %u x%u @ %.3f Hz\n " , width, height, hz);
117+
118+ /* Find the best matching display mode */
119+ for (CFIndex i = 0 ; i < CFArrayGetCount (displayModes); i++)
120+ {
121+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex (displayModes, i);
122+ size_t modeWidth = CGDisplayModeGetWidth (mode);
123+ size_t modeHeight = CGDisplayModeGetHeight (mode);
124+ double refreshRate = CGDisplayModeGetRefreshRate (mode);
125+
126+ /* Exact match preferred */
127+ if (modeWidth == width && modeHeight == height && fabs (refreshRate - hz) < 0.1 )
128+ {
129+ bestMode = mode;
130+ break ;
131+ }
132+ /* Fallback: match resolution, any refresh rate */
133+ else if (modeWidth == width && modeHeight == height && !bestMode)
134+ bestMode = mode;
135+ }
136+
137+ if (bestMode)
138+ {
139+ CGError result = CGDisplaySetDisplayMode (mainDisplayID, bestMode, NULL );
140+ if (result == kCGErrorSuccess )
141+ {
142+ RARCH_LOG (" [Video] Successfully changed display mode to %u x%u @ %.3f Hz\n " ,
143+ width, height, hz);
144+
145+ /* Notify the window and video context about the resolution change */
146+ NSWindow *window = ((RetroArch_OSX*)[[NSApplication sharedApplication ] delegate ]).window ;
147+ if (window)
148+ {
149+ /* Force the window to update its backing store */
150+ [[window contentView ] setNeedsDisplay: YES ];
151+
152+ /* If fullscreen, update the window frame to match new resolution */
153+ if ((window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen)
154+ {
155+ NSScreen *screen = [NSScreen mainScreen ];
156+ [window setFrame: screen.frame display: YES ];
157+ }
158+
159+ /* Notify the view about the change */
160+ CocoaView *cView = [CocoaView get ];
161+ if (cView)
162+ {
163+ [cView setNeedsDisplay: YES ];
164+ [cView setFrame: [[window contentView ] bounds ]];
165+ }
166+ }
167+ }
168+ else
169+ {
170+ RARCH_ERR (" [Video] Failed to change display mode: CGError %d \n " , result);
171+ CFRelease (displayModes);
172+ return false ;
173+ }
174+ }
175+ else
176+ {
177+ RARCH_WARN (" [Video] No matching display mode found for %u x%u @ %.3f Hz\n " ,
178+ width, height, hz);
179+ CFRelease (displayModes);
180+ return false ;
181+ }
182+
183+ CFRelease (displayModes);
184+ }
185+ else
186+ RARCH_DBG (" [Video] Setting refresh rate to %.3f Hz (no resolution change)\n " , hz);
187+
188+ /* Set refresh rate for display link */
93189 if (@available (macOS 14 , *))
94- [CocoaView get ].displayLink .preferredFrameRateRange = CAFrameRateRangeMake (hz * 0.9 , hz * 1.2 , hz);
190+ view.displayLink .preferredFrameRateRange = CAFrameRateRangeMake (hz * 0.9 , hz * 1.2 , hz);
191+ return true ;
192+ }
95193#elif defined(IOS)
194+ static bool apple_display_server_set_resolution (void *data,
195+ unsigned width, unsigned height, int int_hz, float hz,
196+ int center, int monitor_index, int xoffset, int padjust)
197+ {
198+ CocoaView *view = [CocoaView get ];
199+ if (!view || !view.displayLink )
200+ {
201+ RARCH_WARN (" [Video] CocoaView not ready, skipping refresh rate change to %.3f Hz\n " , hz);
202+ return false ;
203+ }
204+
205+ /* iOS: Only refresh rate changes */
206+ RARCH_DBG (" [Video] Setting refresh rate to %.3f Hz\n " , hz);
96207#if (TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000) || (TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= 150000)
97208 if (@available (iOS 15 , tvOS 15 , *))
98- [CocoaView get ] .displayLink .preferredFrameRateRange = CAFrameRateRangeMake (hz * 0.9 , hz * 1.2 , hz);
209+ view .displayLink .preferredFrameRateRange = CAFrameRateRangeMake (hz * 0.9 , hz * 1.2 , hz);
99210 else
100211#endif
101- [CocoaView get ].displayLink .preferredFramesPerSecond = hz;
102- #endif
103- return true ;
212+ view.displayLink .preferredFramesPerSecond = hz;
213+ return true ;
104214}
215+ #endif
105216
106217static void *apple_display_server_get_resolution_list (
107218 void *data, unsigned *len)
108219{
109220 unsigned j = 0 ;
110221 struct video_display_config *conf = NULL ;
111-
112- unsigned width, height;
113- NSMutableSet *rates = [NSMutableSet set ];
114222 double currentRate;
115223
116224#ifdef OSX
117- NSRect bounds = [CocoaView get ].bounds ;
118- float scale = cocoa_screen_get_backing_scale_factor ();
119- width = bounds.size .width * scale;
120- height = bounds.size .height * scale;
121-
122225 CGDirectDisplayID mainDisplayID = CGMainDisplayID ();
123226 CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode (mainDisplayID);
124227 currentRate = CGDisplayModeGetRefreshRate (currentMode);
125- CFRelease (currentMode);
228+
229+ /* Use pixel dimensions when available (macOS 10.8+), otherwise fall back to logical dimensions */
230+ size_t currentWidth, currentHeight;
231+ if (@available (macOS 10.8 , *))
232+ {
233+ currentWidth = CGDisplayModeGetPixelWidth (currentMode);
234+ currentHeight = CGDisplayModeGetPixelHeight (currentMode);
235+ }
236+ else
237+ {
238+ currentWidth = CGDisplayModeGetWidth (currentMode);
239+ currentHeight = CGDisplayModeGetHeight (currentMode);
240+ }
241+
126242 CFArrayRef displayModes = CGDisplayCopyAllDisplayModes (mainDisplayID, NULL );
243+ NSMutableSet *resolutions = [NSMutableSet set ];
244+
245+ for (CFIndex i = 0 ; i < CFArrayGetCount (displayModes); i++)
246+ {
247+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex (displayModes, i);
248+ size_t modeWidth, modeHeight;
249+ if (@available (macOS 10.8 , *))
250+ {
251+ modeWidth = CGDisplayModeGetPixelWidth (mode);
252+ modeHeight = CGDisplayModeGetPixelHeight (mode);
253+ }
254+ else
255+ {
256+ modeWidth = CGDisplayModeGetWidth (mode);
257+ modeHeight = CGDisplayModeGetHeight (mode);
258+ }
259+ double refreshRate = CGDisplayModeGetRefreshRate (mode);
260+
261+ if (refreshRate > 0 )
262+ {
263+ NSString *resolution = [NSString stringWithFormat: @" %zu x%zu " , modeWidth, modeHeight];
264+ [resolutions addObject: resolution];
265+ }
266+ }
267+
268+ /* Build config array with all available resolution/refresh rate combinations */
269+ NSMutableArray *configArray = [NSMutableArray array ];
270+
127271 for (CFIndex i = 0 ; i < CFArrayGetCount (displayModes); i++)
128272 {
129273 CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex (displayModes, i);
274+ size_t modeWidth, modeHeight;
275+ if (@available (macOS 10.8 , *))
276+ {
277+ modeWidth = CGDisplayModeGetPixelWidth (mode);
278+ modeHeight = CGDisplayModeGetPixelHeight (mode);
279+ }
280+ else
281+ {
282+ modeWidth = CGDisplayModeGetWidth (mode);
283+ modeHeight = CGDisplayModeGetHeight (mode);
284+ }
130285 double refreshRate = CGDisplayModeGetRefreshRate (mode);
286+
131287 if (refreshRate > 0 )
132- [rates addObject: @(refreshRate)];
288+ {
289+ struct video_display_config config;
290+ config.width = (unsigned )modeWidth;
291+ config.height = (unsigned )modeHeight;
292+ config.bpp = 32 ;
293+ config.refreshrate = (unsigned )refreshRate;
294+ config.refreshrate_float = (float )refreshRate;
295+ config.interlaced = false ;
296+ config.dblscan = false ;
297+ config.idx = (unsigned )[configArray count ];
298+ config.current = (modeWidth == currentWidth && modeHeight == currentHeight && fabs (refreshRate - currentRate) < 0.1 );
299+
300+ [configArray addObject: [NSValue valueWithBytes: &config objCType: @encode (struct video_display_config)]];
301+ }
302+ }
303+
304+ /* Set length and allocate config array for macOS */
305+ *len = (unsigned )[configArray count ];
306+ if (!(conf = (struct video_display_config*)calloc (*len, sizeof (struct video_display_config))))
307+ return NULL ;
308+
309+ for (j = 0 ; j < *len; j++)
310+ {
311+ NSValue *configValue = configArray[j];
312+ [configValue getValue: &conf[j]];
133313 }
314+
134315 CFRelease (displayModes);
316+ CFRelease (currentMode);
317+ RARCH_LOG (" Found %u display modes on macOS\n " , *len);
318+ return conf;
135319#else
136- CGRect bounds = [CocoaView get ].view .bounds ;
137- float scale = cocoa_screen_get_native_scale ();
138- width = bounds.size .width * scale;
139- height = bounds.size .height * scale;
320+ /* iOS/tvOS: Only enumerate refresh rates for current resolution */
321+ unsigned width, height;
322+ NSMutableSet *rates = [NSMutableSet set ];
140323
324+ /* Use nativeBounds to get physical screen resolution
325+ * (works correctly in multitasking/Split View modes) */
141326 UIScreen *mainScreen = [UIScreen mainScreen ];
327+ CGRect nativeBounds = mainScreen.nativeBounds ;
328+ width = (unsigned )nativeBounds.size .width ;
329+ height = (unsigned )nativeBounds.size .height ;
142330#if (TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000) || (TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= 150000)
143331 if (@available (iOS 15 , tvOS 15 , *))
144332 currentRate = [CocoaView get ].displayLink .preferredFrameRateRange .preferred ;
145333 else
146334#endif
147335 currentRate = [CocoaView get ].displayLink .preferredFramesPerSecond ;
336+
337+ /* Detect ProMotion displays and available refresh rates */
148338#if !TARGET_OS_TV
149- if (@available (iOS 15 , *))
150- [rates addObjectsFromArray: @[@(24 ), @(30 ), @(40 ), @(48 ), @(60 ), @(120 )]];
151- else
152- #endif
339+ if (@available (iOS 10.3 , *))
153340 {
154- if (@available (iOS 10.3 , tvOS 10.2 , *))
155- [rates addObject: @(mainScreen.maximumFramesPerSecond)];
341+ NSInteger maxFPS = mainScreen.maximumFramesPerSecond ;
342+
343+ /* ProMotion displays (120Hz) */
344+ if (maxFPS >= 120 )
345+ {
346+ [rates addObjectsFromArray: @[@(24 ), @(30 ), @(40 ), @(48 ), @(60 ), @(80 ), @(120 )]];
347+ }
348+ /* iPad Pro 10.5" and 11" 2nd gen (120Hz) */
349+ else if (maxFPS > 60 )
350+ {
351+ [rates addObjectsFromArray: @[@(24 ), @(30 ), @(48 ), @(60 ), @(maxFPS)]];
352+ }
353+ /* Standard 60Hz displays */
156354 else
157- [rates addObject: @(60 )];
355+ {
356+ [rates addObjectsFromArray: @[@(30 ), @(60 )]];
357+ }
158358 }
359+ else
159360#endif
361+ {
362+ /* Fallback for older iOS versions */
363+ [rates addObject: @(60 )];
364+ }
160365
161366 NSArray *sorted = [[rates allObjects ] sortedArrayUsingSelector: @selector (compare: )];
162367 *len = (unsigned )[sorted count ];
@@ -179,6 +384,7 @@ static bool apple_display_server_set_resolution(void *data,
179384 conf[j].current = ([rate doubleValue ] == currentRate);
180385 }
181386 return conf;
387+ #endif
182388}
183389
184390#if TARGET_OS_IOS
@@ -230,9 +436,59 @@ static enum rotation apple_display_server_get_screen_orientation(void *data)
230436}
231437#endif
232438
439+ typedef struct
440+ {
441+ #ifdef OSX
442+ CGDisplayModeRef original_mode;
443+ CGDirectDisplayID display_id;
444+ #endif
445+ } apple_display_server_t ;
446+
447+ static void *apple_display_server_init (void )
448+ {
449+ apple_display_server_t *apple = (apple_display_server_t *)calloc (1 , sizeof (*apple));
450+ if (!apple)
451+ return NULL ;
452+
453+ #ifdef OSX
454+ /* Store original display mode for restoration */
455+ apple->display_id = CGMainDisplayID ();
456+ apple->original_mode = CGDisplayCopyDisplayMode (apple->display_id );
457+ RARCH_LOG (" [Video] Stored original display mode for restoration\n " );
458+ #endif
459+
460+ return apple;
461+ }
462+
463+ static void apple_display_server_destroy (void *data)
464+ {
465+ apple_display_server_t *apple = (apple_display_server_t *)data;
466+ if (!apple)
467+ return ;
468+
469+ #ifdef OSX
470+ /* Restore original display mode */
471+ if (apple->original_mode )
472+ {
473+ CGError result = CGDisplaySetDisplayMode (apple->display_id , apple->original_mode , NULL );
474+ if (result == kCGErrorSuccess )
475+ {
476+ RARCH_LOG (" [Video] Restored original display mode\n " );
477+ }
478+ else
479+ {
480+ RARCH_ERR (" [Video] Failed to restore original display mode: CGError %d \n " , result);
481+ }
482+ CFRelease (apple->original_mode );
483+ }
484+ #endif
485+
486+ free (apple);
487+ }
488+
233489const video_display_server_t dispserv_apple = {
234- NULL , /* init */
235- NULL , /* destroy */
490+ apple_display_server_init,
491+ apple_display_server_destroy,
236492#ifdef OSX
237493 apple_display_server_set_window_opacity,
238494 apple_display_server_set_window_progress,
@@ -242,7 +498,11 @@ static enum rotation apple_display_server_get_screen_orientation(void *data)
242498 NULL , /* set_window_progress */
243499 NULL , /* set_window_decorations */
244500#endif
501+ #if !defined(OSX) || __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
245502 apple_display_server_set_resolution,
503+ #else
504+ NULL ,
505+ #endif
246506 apple_display_server_get_resolution_list,
247507 NULL , /* get_output_options */
248508#if TARGET_OS_IOS
0 commit comments