From 67fc9ea81bb1f399e1488e91546842a36d96f9c6 Mon Sep 17 00:00:00 2001 From: jcm Date: Sun, 5 May 2024 10:11:35 -0500 Subject: [PATCH] ruby: Various Mac driver settings fix-ups (#1486) 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 --- desktop-ui/settings/drivers.cpp | 12 ++++++- desktop-ui/settings/settings.cpp | 1 + desktop-ui/settings/settings.hpp | 4 +++ ruby/video/metal/metal.cpp | 62 ++++++++++++++++++++------------ ruby/video/metal/metal.hpp | 1 + ruby/video/video.cpp | 53 ++++++++++++++++----------- ruby/video/video.hpp | 7 ++++ 7 files changed, 97 insertions(+), 43 deletions(-) diff --git a/desktop-ui/settings/drivers.cpp b/desktop-ui/settings/drivers.cpp index 370bff6db..33f3690f3 100644 --- a/desktop-ui/settings/drivers.cpp +++ b/desktop-ui/settings/drivers.cpp @@ -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); @@ -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()); @@ -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(); diff --git a/desktop-ui/settings/settings.cpp b/desktop-ui/settings/settings.cpp index b0293d3fc..844820823 100644 --- a/desktop-ui/settings/settings.cpp +++ b/desktop-ui/settings/settings.cpp @@ -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); diff --git a/desktop-ui/settings/settings.hpp b/desktop-ui/settings/settings.hpp index ea12038ca..6651527b6 100644 --- a/desktop-ui/settings/settings.hpp +++ b/desktop-ui/settings/settings.hpp @@ -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; @@ -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}; diff --git a/ruby/video/metal/metal.cpp b/ruby/video/metal/metal.cpp index 94250321a..a264ce942 100644 --- a/ruby/video/metal/metal.cpp +++ b/ruby/video/metal/metal.cpp @@ -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; @@ -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; } @@ -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; @@ -562,15 +579,16 @@ private: 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; } diff --git a/ruby/video/metal/metal.hpp b/ruby/video/metal/metal.hpp index a89aff229..37d46e455 100644 --- a/ruby/video/metal/metal.hpp +++ b/ruby/video/metal/metal.hpp @@ -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); diff --git a/ruby/video/video.cpp b/ruby/video/video.cpp index 6ecb62264..2c24fdde8 100644 --- a/ruby/video/video.cpp +++ b/ruby/video/video.cpp @@ -76,6 +76,14 @@ auto Video::setThreadedRenderer(bool threadedRenderer) -> bool { return true; } +auto Video::setNativeFullScreen(bool nativeFullScreen) -> bool { + lock_guard 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 lock(mutex); if(instance->flush == flush) return true; @@ -300,28 +308,33 @@ auto Video::hasMonitors() -> vector { 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); } } diff --git a/ruby/video/video.hpp b/ruby/video/video.hpp index d4c6c40c9..73e922883 100644 --- a/ruby/video/video.hpp +++ b/ruby/video/video.hpp @@ -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 { return {"ARGB24"}; } virtual auto hasShader() -> bool { return false; } @@ -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; } @@ -52,6 +54,7 @@ protected: bool blocking = false; bool forceSRGB = false; bool threadedRenderer = true; + bool nativeFullScreen = false; bool flush = false; string format = "ARGB24"; string shader = "None"; @@ -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; @@ -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 { return instance->hasFormats(); } auto hasShader() -> bool { return instance->hasShader(); } @@ -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; } @@ -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;