Skip to content

Commit

Permalink
ruby: Various Mac driver settings fix-ups (ares-emulator#1486)
Browse files Browse the repository at this point in the history
This PR applies several minor fixes and improvements to macOS driver
settings and the driver settings pane.

#### Remove the "exclusive mode" video option on macOS.

* There is no real notion of exclusive presentation on macOS beyond what
already exists in normal fullscreen. If an application is covering the
screen and no other application or window is visible, the system
automatically switches to a "direct" presentation mode that optimizes
for single-application presentation performance. There is not a good
reason to show this option on macOS, even disabled.
* By contrast, it is possible (though not implemented by ares) to enter
exclusive mode on the audio device with CoreAudio, so leave that option
there, just disabled.
#### Add a "use native fullscreen" option on macOS.
* There are various good reasons to prefer either native platform
fullscreen behavior, or a custom borderless windowed fullscreen. Rather
than guess what the user wants, offer an option.
* If unchecked, make the window title bar enlarge the window rather than
fullscreen it, so we don't mix behaviors.
#### Implement fullscreen monitor selection behavior for Metal, and
correctly enumerate the user's monitor names.
* Fullscreen display on the selected monitor in the settings pane was
previously not implemented on macOS. This implementation only works if
"Use native fullscreen" is disabled, since the macOS fullscreen idiom
doesn't feature selecting a specific display.
* Additionally, the old function to retrieve the monitor's localized
name did not work reliably on newer macOS versions. Use the modern
property `localizedName` on NSScreen for macOS versions above 10.15, and
fall back to the old implementation otherwise.
* Implementing this meant adding a `uintptr` handle to the NSScreen
instance in the `Monitor` struct in ruby that uses a bridged cast to
interface with Objective-C. I would have preferred not to do this, but I
didn't see another good way to handle getting the `NSScreen` instance
that didn't involve a serious refactor.
#### (all platforms) Disable monitor selection if the video driver's
`hasMonitor()` is false.
* Just a minor fixup; the existing behavior is that the dropdown list
can be navigated and selected, but the selection does not persist. It
makes more sense to just disable it if the driver doesn't support it.

Co-authored-by: jcm <[email protected]>
  • Loading branch information
jcm93 and jcm authored May 5, 2024
1 parent 4f834bd commit 67fc9ea
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 43 deletions.
12 changes: 11 additions & 1 deletion desktop-ui/settings/drivers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ auto DriverSettings::construct() -> void {
program.videoFormatUpdate();
videoRefresh();
});
#if !defined(PLATFORM_MACOS)
videoExclusiveToggle.setText("Exclusive mode").onToggle([&] {
settings.video.exclusive = videoExclusiveToggle.checked();
ruby::video.setExclusive(settings.video.exclusive);
});
#endif
videoBlockingToggle.setText("Synchronize").onToggle([&] {
settings.video.blocking = videoBlockingToggle.checked();
ruby::video.setBlocking(settings.video.blocking);
Expand All @@ -48,6 +50,11 @@ auto DriverSettings::construct() -> void {
settings.video.threadedRenderer = videoThreadedRendererToggle.checked();
ruby::video.setThreadedRenderer(settings.video.threadedRenderer);
});
videoNativeFullScreenToggle.setText("Use native fullscreen").onToggle([&] {
settings.video.nativeFullScreen = videoNativeFullScreenToggle.checked();
ruby::video.setNativeFullScreen(settings.video.nativeFullScreen);
videoRefresh();
});
#endif

audioLabel.setText("Audio").setFont(Font().setBold());
Expand Down Expand Up @@ -153,13 +160,16 @@ auto DriverSettings::videoRefresh() -> void {
item.setText(format);
if(format == ruby::video.format()) item.setSelected();
}
videoMonitorList.setEnabled(videoMonitorList.itemCount() > 1);
videoMonitorList.setEnabled(videoMonitorList.itemCount() > 1 && ruby::video.hasMonitor());
videoFormatList.setEnabled(0 && videoFormatList.itemCount() > 1);
#if !defined(PLATFORM_MACOS)
videoExclusiveToggle.setChecked(ruby::video.exclusive()).setEnabled(ruby::video.hasExclusive());
#endif
videoBlockingToggle.setChecked(ruby::video.blocking()).setEnabled(ruby::video.hasBlocking());
#if defined(PLATFORM_MACOS)
videoColorSpaceToggle.setChecked(ruby::video.forceSRGB()).setEnabled(ruby::video.hasForceSRGB());
videoThreadedRendererToggle.setChecked(ruby::video.threadedRenderer()).setEnabled(ruby::video.hasThreadedRenderer());
videoNativeFullScreenToggle.setChecked(ruby::video.nativeFullScreen()).setEnabled(ruby::video.hasNativeFullScreen());
#endif
videoFlushToggle.setChecked(ruby::video.flush()).setEnabled(ruby::video.hasFlush());
VerticalLayout::resize();
Expand Down
1 change: 1 addition & 0 deletions desktop-ui/settings/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ auto Settings::process(bool load) -> void {
bind(boolean, "Video/Blocking", video.blocking);
bind(boolean, "Video/PresentSRGB", video.forceSRGB);
bind(boolean, "Video/ThreadedRenderer", video.threadedRenderer);
bind(boolean, "Video/NativeFullScreen", video.nativeFullScreen);
bind(boolean, "Video/Flush", video.flush);
bind(string, "Video/Shader", video.shader);
bind(natural, "Video/Multiplier", video.multiplier);
Expand Down
4 changes: 4 additions & 0 deletions desktop-ui/settings/settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct Settings : Markup::Node {
bool blocking = false;
bool forceSRGB = false;
bool threadedRenderer = true;
bool nativeFullScreen = false;
bool flush = false;
string shader = "None";
u32 multiplier = 2;
Expand Down Expand Up @@ -333,12 +334,15 @@ struct DriverSettings : VerticalLayout {
Label videoFormatLabel{&videoPropertyLayout, Size{0, 0}};
ComboButton videoFormatList{&videoPropertyLayout, Size{0, 0}};
HorizontalLayout videoToggleLayout{this, Size{~0, 0}};
#if !defined(PLATFORM_MACOS)
CheckLabel videoExclusiveToggle{&videoToggleLayout, Size{0, 0}};
#endif
CheckLabel videoBlockingToggle{&videoToggleLayout, Size{0, 0}};
CheckLabel videoFlushToggle{&videoToggleLayout, Size{0, 0}};
#if defined(PLATFORM_MACOS)
CheckLabel videoColorSpaceToggle{&videoToggleLayout, Size{0, 0}};
CheckLabel videoThreadedRendererToggle{&videoToggleLayout, Size{0, 0}};
CheckLabel videoNativeFullScreenToggle{&videoToggleLayout, Size{0, 0}};
#endif
//
Label audioLabel{this, Size{~0, 0}, 5};
Expand Down
62 changes: 40 additions & 22 deletions ruby/video/metal/metal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ struct VideoMetal : VideoDriver, Metal {
auto ready() -> bool override { return _ready; }

auto hasFullScreen() -> bool override { return true; }
auto hasMonitor() -> bool override { return !_nativeFullScreen; }
auto hasContext() -> bool override { return true; }
auto hasFlush() -> bool override { return true; }
auto hasBlocking() -> bool override {
if (@available(macOS 10.15.4, *)) {
return true;
Expand All @@ -41,31 +43,34 @@ struct VideoMetal : VideoDriver, Metal {
}
auto hasForceSRGB() -> bool override { return true; }
auto hasThreadedRenderer() -> bool override { return true; }
auto hasFlush() -> bool override { return true; }
auto hasNativeFullScreen() -> bool override { return true; }
auto hasShader() -> bool override { return true; }

auto setFullScreen(bool fullScreen) -> bool override {
/// This function implements non-idiomatic macOS fullscreen behavior that sets the window frame equal to the display's
/// frame size and hides the cursor. Idiomatic fullscreen is still available via the normal stoplight window controls. This
/// version of fullscreen is desirable because it allows us to render around the camera housing on newer Macs
/// (important for bezel-style shaders), has snappier entrance/exit and tabbing behavior, and functions better with
/// recording and capture software such as OBS and screen recorders. Hiding the mouse cursor is also essential to
/// rendering with appropriate frame pacing in Metal's 'direct' presentation mode.

// todo: unify with cursor auto-hide in hiro, ideally ares-wide fullscreen mode option

if (fullScreen) {
frameBeforeFullScreen = view.window.frame;
[NSApp setPresentationOptions:(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar)];
[view.window setStyleMask:NSWindowStyleMaskBorderless];
[view.window setFrame:view.window.screen.frame display:YES];
[NSCursor setHiddenUntilMouseMoves:YES];
// todo: fix/make consistent mouse cursor hide behavior

if (_nativeFullScreen) {
[view.window toggleFullScreen:nil];
} else {
[NSApp setPresentationOptions:NSApplicationPresentationDefault];
[view.window setStyleMask:NSWindowStyleMaskTitled];
[view.window setFrame:frameBeforeFullScreen display:YES];
/// This option implements non-idiomatic macOS fullscreen behavior that sets the window frame equal to the selected display's
/// frame size and hides the cursor. This version of fullscreen is desirable because it allows us to render around the camera
/// housing on newer Macs (important for bezel-style shaders), has snappier entrance/exit and tabbing behavior, and functions
/// better with recording and capture software such as OBS.
if (fullScreen) {
auto monitor = Video::monitor(self.monitor);
NSScreen *handle = (__bridge NSScreen *)(void *)monitor.nativeHandle; //eew
frameBeforeFullScreen = view.window.frame;
[NSApp setPresentationOptions:(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar)];
[view.window setStyleMask:NSWindowStyleMaskBorderless];
[view.window setFrame:handle.frame display:YES];
[NSCursor setHiddenUntilMouseMoves:YES];
} else {
[NSApp setPresentationOptions:NSApplicationPresentationDefault];
[view.window setStyleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable)];
[view.window setFrame:frameBeforeFullScreen display:YES];
}
[view.window makeFirstResponder:view];
}
[view.window makeFirstResponder:view];
return true;
}

Expand All @@ -92,6 +97,18 @@ struct VideoMetal : VideoDriver, Metal {
_threaded = threadedRenderer;
return true;
}

auto setNativeFullScreen(bool nativeFullScreen) -> bool override {
_nativeFullScreen = nativeFullScreen;
if (nativeFullScreen) {
//maximize goes fullscreen
[view.window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenPrimary];
} else {
//maximize does not go fullscreen
[view.window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenAuxiliary];
}
return true;
}

auto setFlush(bool flush) -> bool override {
_flush = flush;
Expand Down Expand Up @@ -562,15 +579,16 @@ struct VideoMetal : VideoDriver, Metal {
bool forceSRGB = self.forceSRGB;
self.setForceSRGB(forceSRGB);
view.autoresizingMask = NSViewWidthSizable|NSViewHeightSizable;

_threaded = self.threadedRenderer;
_blocking = self.blocking;
setNativeFullScreen(self.nativeFullScreen);

_libra = librashader_load_instance();
if (!_libra.instance_loaded) {
print("Metal: Failed to load librashader: shaders will be disabled\n");
}

_blocking = self.blocking;

initialized = true;
return _ready = true;
}
Expand Down
1 change: 1 addition & 0 deletions ruby/video/metal/metal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct Metal {
bool _flush = false;
bool _vrrIsSupported = false;
bool _threaded = true;
bool _nativeFullScreen = false;

NSRect frameBeforeFullScreen = NSMakeRect(0,0,0,0);

Expand Down
53 changes: 33 additions & 20 deletions ruby/video/video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ auto Video::setThreadedRenderer(bool threadedRenderer) -> bool {
return true;
}

auto Video::setNativeFullScreen(bool nativeFullScreen) -> bool {
lock_guard<recursive_mutex> lock(mutex);
if(instance->nativeFullScreen == nativeFullScreen) return true;
if(!instance->hasNativeFullScreen()) return false;
if(!instance->setNativeFullScreen(instance->nativeFullScreen = nativeFullScreen)) return false;
return true;
}

auto Video::setFlush(bool flush) -> bool {
lock_guard<recursive_mutex> lock(mutex);
if(instance->flush == flush) return true;
Expand Down Expand Up @@ -300,28 +308,33 @@ auto Video::hasMonitors() -> vector<Monitor> {
monitor.y = rectangle.origin.y;
monitor.width = rectangle.size.width;
monitor.height = rectangle.size.height;
//getting the name of the monitor on macOS: "Think Different"
auto screenDictionary = [screen deviceDescription];
auto screenID = [screenDictionary objectForKey:@"NSScreenNumber"];
auto displayID = [screenID unsignedIntValue];
CFUUIDRef displayUUID = CGDisplayCreateUUIDFromDisplayID(displayID);
io_service_t displayPort = CGDisplayGetDisplayIDFromUUID(displayUUID);
auto dictionary = IODisplayCreateInfoDictionary(displayPort, 0);
CFRetain(dictionary);
if(auto names = (CFDictionaryRef)CFDictionaryGetValue(dictionary, CFSTR(kDisplayProductName))) {
auto languageKeys = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
CFDictionaryApplyFunction(names, MonitorKeyArrayCallback, (void*)languageKeys);
auto orderLanguageKeys = CFBundleCopyPreferredLocalizationsFromArray(languageKeys);
CFRelease(languageKeys);
if(orderLanguageKeys && CFArrayGetCount(orderLanguageKeys)) {
auto languageKey = CFArrayGetValueAtIndex(orderLanguageKeys, 0);
auto localName = CFDictionaryGetValue(names, languageKey);
monitor.name = {1 + monitors.size(), ": ", [(__bridge NSString*)localName UTF8String]};
CFRelease(localName);
monitor.nativeHandle = (uintptr)screen;
if (@available(macOS 10.15, *)) {
monitor.name = {1 + monitors.size(), ": ", screen.localizedName.UTF8String};
} else {
//getting the name of the monitor on macOS: "Think Different"
auto screenDictionary = [screen deviceDescription];
auto screenID = [screenDictionary objectForKey:@"NSScreenNumber"];
auto displayID = [screenID unsignedIntValue];
CFUUIDRef displayUUID = CGDisplayCreateUUIDFromDisplayID(displayID);
io_service_t displayPort = CGDisplayGetDisplayIDFromUUID(displayUUID);
auto dictionary = IODisplayCreateInfoDictionary(displayPort, 0);
CFRetain(dictionary);
if(auto names = (CFDictionaryRef)CFDictionaryGetValue(dictionary, CFSTR(kDisplayProductName))) {
auto languageKeys = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
CFDictionaryApplyFunction(names, MonitorKeyArrayCallback, (void*)languageKeys);
auto orderLanguageKeys = CFBundleCopyPreferredLocalizationsFromArray(languageKeys);
CFRelease(languageKeys);
if(orderLanguageKeys && CFArrayGetCount(orderLanguageKeys)) {
auto languageKey = CFArrayGetValueAtIndex(orderLanguageKeys, 0);
auto localName = CFDictionaryGetValue(names, languageKey);
monitor.name = {1 + monitors.size(), ": ", [(__bridge NSString*)localName UTF8String]};
CFRelease(localName);
}
CFRelease(orderLanguageKeys);
}
CFRelease(orderLanguageKeys);
CFRelease(dictionary);
}
CFRelease(dictionary);
monitors.append(monitor);
}
}
Expand Down
7 changes: 7 additions & 0 deletions ruby/video/video.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct VideoDriver {
virtual auto hasBlocking() -> bool { return false; }
virtual auto hasForceSRGB() -> bool { return false; }
virtual auto hasThreadedRenderer() -> bool { return false; }
virtual auto hasNativeFullScreen() -> bool { return false; }
virtual auto hasFlush() -> bool { return false; }
virtual auto hasFormats() -> vector<string> { return {"ARGB24"}; }
virtual auto hasShader() -> bool { return false; }
Expand All @@ -28,6 +29,7 @@ struct VideoDriver {
virtual auto setBlocking(bool blocking) -> bool { return true; }
virtual auto setForceSRGB(bool forceSRGB) -> bool { return true; }
virtual auto setThreadedRenderer(bool threadedRenderer) -> bool { return true; }
virtual auto setNativeFullScreen(bool nativeFullScreen) -> bool { return true; }
virtual auto setFlush(bool flush) -> bool { return true; }
virtual auto setFormat(string format) -> bool { return true; }
virtual auto setShader(string shader) -> bool { return true; }
Expand All @@ -52,6 +54,7 @@ struct VideoDriver {
bool blocking = false;
bool forceSRGB = false;
bool threadedRenderer = true;
bool nativeFullScreen = false;
bool flush = false;
string format = "ARGB24";
string shader = "None";
Expand All @@ -70,6 +73,7 @@ struct Video {
s32 y = 0;
s32 width = 0;
s32 height = 0;
uintptr_t nativeHandle = 0;
};
static auto monitor(string name) -> Monitor;
static auto hasMonitors() -> vector<Monitor>;
Expand All @@ -94,6 +98,7 @@ struct Video {
auto hasBlocking() -> bool { return instance->hasBlocking(); }
auto hasForceSRGB() -> bool { return instance->hasForceSRGB(); }
auto hasThreadedRenderer() -> bool { return instance->hasThreadedRenderer(); }
auto hasNativeFullScreen() -> bool { return instance->hasNativeFullScreen(); }
auto hasFlush() -> bool { return instance->hasFlush(); }
auto hasFormats() -> vector<string> { return instance->hasFormats(); }
auto hasShader() -> bool { return instance->hasShader(); }
Expand All @@ -107,6 +112,7 @@ struct Video {
auto blocking() -> bool { return instance->blocking; }
auto forceSRGB() -> bool { return instance->forceSRGB; }
auto threadedRenderer() -> bool { return instance->threadedRenderer; }
auto nativeFullScreen() -> bool { return instance->nativeFullScreen; }
auto flush() -> bool { return instance->flush; }
auto format() -> string { return instance->format; }
auto shader() -> string { return instance->shader; }
Expand All @@ -118,6 +124,7 @@ struct Video {
auto setBlocking(bool blocking) -> bool;
auto setForceSRGB(bool forceSRGB) -> bool;
auto setThreadedRenderer(bool threadedRenderer) -> bool;
auto setNativeFullScreen(bool nativeFullScreen) -> bool;
auto setFlush(bool flush) -> bool;
auto setFormat(string format) -> bool;
auto setShader(string shader) -> bool;
Expand Down

0 comments on commit 67fc9ea

Please sign in to comment.