Skip to content

Commit 8d16395

Browse files
committed
apple: fix display server resolution/refresh rates on macos/ios
1 parent 64ff381 commit 8d16395

5 files changed

Lines changed: 408 additions & 33 deletions

File tree

gfx/display_servers/dispserv_apple.m

Lines changed: 289 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
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
8890
static 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: %ux%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 %ux%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 %ux%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

106217
static 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:@"%zux%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+
233489
const 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

Comments
 (0)