From e211c3f00a847f50b286349918e5c51967862e93 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Tue, 6 Dec 2022 22:00:25 +0000 Subject: [PATCH] UI: Add Metal surface creation for MoltenVK (#3980) * Initial implementation of metal surface across UIs * Fix SDL2 on windows * Update Ryujinx/Ryujinx.csproj Co-authored-by: Mary-nyan * Address Feedback Co-authored-by: Mary-nyan --- Ryujinx.Ava/AppHost.cs | 2 +- Ryujinx.Ava/Helper/MetalHelper.cs | 127 +++++++++++++++++ Ryujinx.Ava/Program.cs | 9 +- Ryujinx.Ava/Ryujinx.Ava.csproj | 6 +- Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs | 33 ++++- .../Ui/Controls/VulkanEmbeddedWindow.cs | 5 + .../Ui/ViewModels/SettingsViewModel.cs | 2 + Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs | 6 + Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml | 2 +- .../Configuration/AppDataManager.cs | 9 +- .../VulkanInitialization.cs | 12 +- .../Resources/Logo_Ryujinx.png | Bin 0 -> 52972 bytes .../SoftwareKeyboardRendererBase.cs | 8 +- Ryujinx.HLE/Ryujinx.HLE.csproj | 2 + Ryujinx.Headless.SDL2/Program.cs | 20 +++ .../Ryujinx.Headless.SDL2.csproj | 3 +- Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs | 28 +++- Ryujinx.Headless.SDL2/WindowBase.cs | 34 +++-- Ryujinx.Memory/MemoryManagerUnixHelper.cs | 3 +- Ryujinx/Modules/Updater/UpdateDialog.cs | 2 +- Ryujinx/Program.cs | 36 +++++ Ryujinx/Ryujinx.csproj | 15 +- Ryujinx/Ui/Helper/MetalHelper.cs | 134 ++++++++++++++++++ Ryujinx/Ui/MainWindow.cs | 7 +- Ryujinx/Ui/VKRenderer.cs | 15 +- Ryujinx/Ui/Widgets/ProfileDialog.cs | 2 +- Ryujinx/Ui/Windows/CheatWindow.cs | 2 +- Ryujinx/Ui/Windows/ControllerWindow.cs | 17 ++- Ryujinx/Ui/Windows/DlcWindow.cs | 2 +- Ryujinx/Ui/Windows/SettingsWindow.cs | 13 +- Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 2 +- 31 files changed, 495 insertions(+), 63 deletions(-) create mode 100644 Ryujinx.Ava/Helper/MetalHelper.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png create mode 100644 Ryujinx/Ui/Helper/MetalHelper.cs diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs index a016ebd5a..0cb3bd138 100644 --- a/Ryujinx.Ava/AppHost.cs +++ b/Ryujinx.Ava/AppHost.cs @@ -125,7 +125,7 @@ namespace Ryujinx.Ava _inputManager = inputManager; _accountManager = accountManager; _userChannelPersistence = userChannelPersistence; - _renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" }; + _renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" }; _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle; _lastCursorMoveTime = Stopwatch.GetTimestamp(); _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel; diff --git a/Ryujinx.Ava/Helper/MetalHelper.cs b/Ryujinx.Ava/Helper/MetalHelper.cs new file mode 100644 index 000000000..ae07ce69b --- /dev/null +++ b/Ryujinx.Ava/Helper/MetalHelper.cs @@ -0,0 +1,127 @@ +using System; +using System.Runtime.Versioning; +using System.Runtime.InteropServices; +using Avalonia; + +namespace Ryujinx.Ava.Ui.Helper +{ + public delegate void UpdateBoundsCallbackDelegate(Rect rect); + + [SupportedOSPlatform("macos")] + static class MetalHelper + { + private const string LibObjCImport = "/usr/lib/libobjc.A.dylib"; + + private struct Selector + { + public readonly IntPtr NativePtr; + + public unsafe Selector(string value) + { + int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); + byte* data = stackalloc byte[size]; + + fixed (char* pValue = value) + { + System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); + } + + NativePtr = sel_registerName(data); + } + + public static implicit operator Selector(string value) => new Selector(value); + } + + private static unsafe IntPtr GetClass(string value) + { + int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); + byte* data = stackalloc byte[size]; + + fixed (char* pValue = value) + { + System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); + } + + return objc_getClass(data); + } + + private struct NSPoint + { + public double X; + public double Y; + + public NSPoint(double x, double y) + { + X = x; + Y = y; + } + } + + private struct NSRect + { + public NSPoint Pos; + public NSPoint Size; + + public NSRect(double x, double y, double width, double height) + { + Pos = new NSPoint(x, y); + Size = new NSPoint(width, height); + } + } + + public static IntPtr GetMetalLayer(out IntPtr nsView, out UpdateBoundsCallbackDelegate updateBounds) + { + // Create a new CAMetalLayer. + IntPtr layerClass = GetClass("CAMetalLayer"); + IntPtr metalLayer = IntPtr_objc_msgSend(layerClass, "alloc"); + objc_msgSend(metalLayer, "init"); + + // Create a child NSView to render into. + IntPtr nsViewClass = GetClass("NSView"); + IntPtr child = IntPtr_objc_msgSend(nsViewClass, "alloc"); + objc_msgSend(child, "init", new NSRect(0, 0, 0, 0)); + + // Make its renderer our metal layer. + objc_msgSend(child, "setWantsLayer:", (byte)1); + objc_msgSend(child, "setLayer:", metalLayer); + objc_msgSend(metalLayer, "setContentsScale:", Program.DesktopScaleFactor); + + // Ensure the scale factor is up to date. + updateBounds = (Rect rect) => { + objc_msgSend(metalLayer, "setContentsScale:", Program.DesktopScaleFactor); + }; + + nsView = child; + return metalLayer; + } + + public static void DestroyMetalLayer(IntPtr nsView, IntPtr metalLayer) + { + // TODO + } + + [DllImport(LibObjCImport)] + private static unsafe extern IntPtr sel_registerName(byte* data); + + [DllImport(LibObjCImport)] + private static unsafe extern IntPtr objc_getClass(byte* data); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, byte value); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, NSRect point); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, double value); + + [DllImport(LibObjCImport, EntryPoint = "objc_msgSend")] + private static extern IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 23b9ef8ea..d929331db 100644 --- a/Ryujinx.Ava/Program.cs +++ b/Ryujinx.Ava/Program.cs @@ -22,10 +22,11 @@ namespace Ryujinx.Ava { internal class Program { - public static double WindowScaleFactor { get; set; } - public static string Version { get; private set; } - public static string ConfigurationPath { get; private set; } - public static bool PreviewerDetached { get; private set; } + public static double WindowScaleFactor { get; set; } + public static double DesktopScaleFactor { get; set; } = 1.0; + public static string Version { get; private set; } + public static string ConfigurationPath { get; private set; } + public static bool PreviewerDetached { get; private set; } [DllImport("user32.dll", SetLastError = true)] public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type); diff --git a/Ryujinx.Ava/Ryujinx.Ava.csproj b/Ryujinx.Ava/Ryujinx.Ava.csproj index 6d963a403..24bdf22de 100644 --- a/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -31,8 +31,10 @@ - - + + + + diff --git a/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs b/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs index 7acbefca5..6ef159821 100644 --- a/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs +++ b/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs @@ -2,11 +2,10 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Platform; +using Ryujinx.Ava.Ui.Helper; using SPB.Graphics; using SPB.Platform; using SPB.Platform.GLX; -using SPB.Platform.X11; -using SPB.Windowing; using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -23,6 +22,10 @@ namespace Ryujinx.Ava.Ui.Controls protected GLXWindow X11Window { get; set; } protected IntPtr WindowHandle { get; set; } protected IntPtr X11Display { get; set; } + protected IntPtr NsView { get; set; } + protected IntPtr MetalLayer { get; set; } + + private UpdateBoundsCallbackDelegate _updateBoundsCallback; public event EventHandler WindowCreated; public event EventHandler SizeChanged; @@ -58,6 +61,7 @@ namespace Ryujinx.Ava.Ui.Controls private void StateChanged(Rect rect) { SizeChanged?.Invoke(this, rect.Size); + _updateBoundsCallback?.Invoke(rect); } protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) @@ -70,6 +74,11 @@ namespace Ryujinx.Ava.Ui.Controls { return CreateWin32(parent); } + else if (OperatingSystem.IsMacOS()) + { + return CreateMacOs(parent); + } + return base.CreateNativeControlCore(parent); } @@ -85,6 +94,10 @@ namespace Ryujinx.Ava.Ui.Controls { DestroyWin32(control); } + else if (OperatingSystem.IsMacOS()) + { + DestroyMacOS(); + } else { base.DestroyNativeControlCore(control); @@ -187,6 +200,16 @@ namespace Ryujinx.Ava.Ui.Controls return DefWindowProc(hWnd, msg, (IntPtr)wParam, (IntPtr)lParam); } + [SupportedOSPlatform("macos")] + IPlatformHandle CreateMacOs(IPlatformHandle parent) + { + MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback); + + NsView = nsView; + + return new PlatformHandle(nsView, "NSView"); + } + void DestroyLinux() { X11Window?.Dispose(); @@ -198,5 +221,11 @@ namespace Ryujinx.Ava.Ui.Controls DestroyWindow(handle.Handle); UnregisterClass(_className, GetModuleHandle(null)); } + + [SupportedOSPlatform("macos")] + void DestroyMacOS() + { + MetalHelper.DestroyMetalLayer(NsView, MetalLayer); + } } } \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs b/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs index 236a0a166..b9c5f75f5 100644 --- a/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs +++ b/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs @@ -3,6 +3,7 @@ using Ryujinx.Ava.Ui.Controls; using Silk.NET.Vulkan; using SPB.Graphics.Vulkan; using SPB.Platform.GLX; +using SPB.Platform.Metal; using SPB.Platform.Win32; using SPB.Platform.X11; using SPB.Windowing; @@ -37,6 +38,10 @@ namespace Ryujinx.Ava.Ui { _window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle)); } + else if (OperatingSystem.IsMacOS()) + { + _window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer)); + } else { throw new PlatformNotSupportedException(); diff --git a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs index bd4a55e8f..c752697b9 100644 --- a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs @@ -108,6 +108,8 @@ namespace Ryujinx.Ava.Ui.ViewModels } } + public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); + public bool DirectoryChanged { get => _directoryChanged; diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs index a9da8d7d0..33c35c7e0 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs @@ -154,6 +154,12 @@ namespace Ryujinx.Ava.Ui.Windows } } + protected override void HandleScalingChanged(double scale) + { + Program.DesktopScaleFactor = scale; + base.HandleScalingChanged(scale); + } + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) diff --git a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml index c8c9f59a8..bd3dd613e 100644 --- a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml @@ -540,7 +540,7 @@ - + diff --git a/Ryujinx.Common/Configuration/AppDataManager.cs b/Ryujinx.Common/Configuration/AppDataManager.cs index 1d217f587..42b76453b 100644 --- a/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/Ryujinx.Common/Configuration/AppDataManager.cs @@ -45,7 +45,14 @@ namespace Ryujinx.Common.Configuration public static void Initialize(string baseDirPath) { - string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir); + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + if (appDataPath.Length == 0) + { + appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + + string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir); string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir); if (Directory.Exists(portablePath)) diff --git a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 7813bb816..942970c27 100644 --- a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; @@ -21,6 +21,7 @@ namespace Ryujinx.Graphics.Vulkan { ExtConditionalRendering.ExtensionName, ExtExtendedDynamicState.ExtensionName, + ExtTransformFeedback.ExtensionName, KhrDrawIndirectCount.ExtensionName, KhrPushDescriptor.ExtensionName, "VK_EXT_custom_border_color", @@ -36,8 +37,7 @@ namespace Ryujinx.Graphics.Vulkan public static string[] RequiredExtensions { get; } = new string[] { - KhrSwapchain.ExtensionName, - ExtTransformFeedback.ExtensionName + KhrSwapchain.ExtensionName }; private static string[] _excludedMessages = new string[] @@ -382,12 +382,12 @@ namespace Ryujinx.Graphics.Vulkan DepthClamp = true, DualSrcBlend = true, FragmentStoresAndAtomics = true, - GeometryShader = true, + GeometryShader = supportedFeatures.GeometryShader, ImageCubeArray = true, IndependentBlend = true, - LogicOp = true, + LogicOp = supportedFeatures.LogicOp, MultiViewport = true, - PipelineStatisticsQuery = true, + PipelineStatisticsQuery = supportedFeatures.PipelineStatisticsQuery, SamplerAnisotropy = true, ShaderClipDistance = true, ShaderFloat64 = supportedFeatures.ShaderFloat64, diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png new file mode 100644 index 0000000000000000000000000000000000000000..0e8da15e6ee20873f2ad7924f77d2952c20c4b14 GIT binary patch literal 52972 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 wX=Zr + @@ -44,6 +45,7 @@ + diff --git a/Ryujinx.Headless.SDL2/Program.cs b/Ryujinx.Headless.SDL2/Program.cs index bfc33edcf..50a90763f 100644 --- a/Ryujinx.Headless.SDL2/Program.cs +++ b/Ryujinx.Headless.SDL2/Program.cs @@ -77,6 +77,26 @@ namespace Ryujinx.Headless.SDL2 _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient); _userChannelPersistence = new UserChannelPersistence(); + if (OperatingSystem.IsMacOS()) + { + AutoResetEvent invoked = new AutoResetEvent(false); + + // MacOS must perform SDL polls from the main thread. + Ryujinx.SDL2.Common.SDL2Driver.MainThreadDispatcher = (Action action) => + { + invoked.Reset(); + + WindowBase.QueueMainThreadAction(() => + { + action(); + + invoked.Set(); + }); + + invoked.WaitOne(); + }; + } + _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); GraphicsConfig.EnableShaderCache = true; diff --git a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index 15286ea3a..83ae87eb8 100644 --- a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -12,7 +12,8 @@ - + + diff --git a/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index 6eacadc15..183233397 100644 --- a/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Input.HLE; +using Ryujinx.SDL2.Common; using System; using System.Runtime.InteropServices; using static SDL2.SDL; @@ -26,15 +27,34 @@ namespace Ryujinx.Headless.SDL2.Vulkan MouseDriver.SetClientSize(DefaultWidth, DefaultHeight); } + private void BasicInvoke(Action action) + { + action(); + } + public unsafe IntPtr CreateWindowSurface(IntPtr instance) { - if (SDL_Vulkan_CreateSurface(WindowHandle, instance, out ulong surfaceHandle) == SDL_bool.SDL_FALSE) + ulong surfaceHandle = 0; + + Action createSurface = () => { - string errorMessage = $"SDL_Vulkan_CreateSurface failed with error \"{SDL_GetError()}\""; + if (SDL_Vulkan_CreateSurface(WindowHandle, instance, out surfaceHandle) == SDL_bool.SDL_FALSE) + { + string errorMessage = $"SDL_Vulkan_CreateSurface failed with error \"{SDL_GetError()}\""; - Logger.Error?.Print(LogClass.Application, errorMessage); + Logger.Error?.Print(LogClass.Application, errorMessage); - throw new Exception(errorMessage); + throw new Exception(errorMessage); + } + }; + + if (SDL2Driver.MainThreadDispatcher != null) + { + SDL2Driver.MainThreadDispatcher(createSurface); + } + else + { + createSurface(); } return (IntPtr)surfaceHandle; diff --git a/Ryujinx.Headless.SDL2/WindowBase.cs b/Ryujinx.Headless.SDL2/WindowBase.cs index 9aa17936f..88b0d5733 100644 --- a/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/Ryujinx.Headless.SDL2/WindowBase.cs @@ -11,6 +11,7 @@ using Ryujinx.Input; using Ryujinx.Input.HLE; using Ryujinx.SDL2.Common; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading; @@ -26,6 +27,13 @@ namespace Ryujinx.Headless.SDL2 private const SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN; private const int TargetFps = 60; + private static ConcurrentQueue MainThreadActions = new ConcurrentQueue(); + + public static void QueueMainThreadAction(Action action) + { + MainThreadActions.Enqueue(action); + } + public NpadManager NpadManager { get; } public TouchScreenManager TouchScreenManager { get; } public Switch Device { get; private set; } @@ -168,6 +176,14 @@ namespace Ryujinx.Headless.SDL2 public void Render() { + InitializeWindowRenderer(); + + Device.Gpu.Renderer.Initialize(_glLogLevel); + + InitializeRenderer(); + + _gpuVendorName = GetGpuVendorName(); + Device.Gpu.Renderer.RunLoop(() => { Device.Gpu.SetGpuThread(); @@ -241,6 +257,14 @@ namespace Ryujinx.Headless.SDL2 _exitEvent.Dispose(); } + public void ProcessMainThreadQueue() + { + while (MainThreadActions.TryDequeue(out Action action)) + { + action(); + } + } + public void MainLoop() { while (_isActive) @@ -249,6 +273,8 @@ namespace Ryujinx.Headless.SDL2 SDL_PumpEvents(); + ProcessMainThreadQueue(); + // Polling becomes expensive if it's not slept Thread.Sleep(1); } @@ -315,14 +341,6 @@ namespace Ryujinx.Headless.SDL2 InitializeWindow(); - InitializeWindowRenderer(); - - Device.Gpu.Renderer.Initialize(_glLogLevel); - - InitializeRenderer(); - - _gpuVendorName = GetGpuVendorName(); - Thread renderLoopThread = new Thread(Render) { Name = "GUI.RenderLoop" diff --git a/Ryujinx.Memory/MemoryManagerUnixHelper.cs b/Ryujinx.Memory/MemoryManagerUnixHelper.cs index 8e6e79352..dd31c328b 100644 --- a/Ryujinx.Memory/MemoryManagerUnixHelper.cs +++ b/Ryujinx.Memory/MemoryManagerUnixHelper.cs @@ -153,7 +153,8 @@ namespace Ryujinx.Memory if (OperatingSystem.IsMacOSVersionAtLeast(10, 14)) { - result |= MAP_JIT_DARWIN; + // Only to be used with the Hardened Runtime. + // result |= MAP_JIT_DARWIN; } return result; diff --git a/Ryujinx/Modules/Updater/UpdateDialog.cs b/Ryujinx/Modules/Updater/UpdateDialog.cs index cb71fafc9..a1556713a 100644 --- a/Ryujinx/Modules/Updater/UpdateDialog.cs +++ b/Ryujinx/Modules/Updater/UpdateDialog.cs @@ -25,7 +25,7 @@ namespace Ryujinx.Modules public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Modules.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { } - private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetObject("UpdateDialog").Handle) + private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetRawOwnedObject("UpdateDialog")) { builder.Autoconnect(this); diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 162bd89d5..3baddca3f 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -16,6 +16,7 @@ using Ryujinx.Ui.Widgets; using SixLabors.ImageSharp.Formats.Jpeg; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -40,6 +41,12 @@ namespace Ryujinx [DllImport("user32.dll", SetLastError = true)] public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type); + [DllImport("libc", SetLastError = true)] + static extern int setenv(string name, string value, int overwrite); + + [DllImport("libc")] + static extern IntPtr getenv(string name); + private const uint MB_ICONWARNING = 0x30; static Program() @@ -97,6 +104,35 @@ namespace Ryujinx XInitThreads(); } + if (OperatingSystem.IsMacOS()) + { + string baseDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); + string resourcesDataDir; + + if (Path.GetFileName(baseDirectory) == "MacOS") + { + resourcesDataDir = Path.Combine(Directory.GetParent(baseDirectory).FullName, "Resources"); + } + else + { + resourcesDataDir = baseDirectory; + } + + void SetEnvironmentVariableNoCaching(string key, string value) + { + int res = setenv(key, value, 1); + Debug.Assert(res != -1); + } + + // On macOS, GTK3 needs XDG_DATA_DIRS to be set, otherwise it will try searching for "gschemas.compiled" in system directories. + SetEnvironmentVariableNoCaching("XDG_DATA_DIRS", Path.Combine(resourcesDataDir, "share")); + + // On macOS, GTK3 needs GDK_PIXBUF_MODULE_FILE to be set, otherwise it will try searching for "loaders.cache" in system directories. + SetEnvironmentVariableNoCaching("GDK_PIXBUF_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gdk-pixbuf-2.0", "2.10.0", "loaders.cache")); + + SetEnvironmentVariableNoCaching("GTK_IM_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gtk-3.0", "3.0.0", "immodules.cache")); + } + string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}"); diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 31f130c4a..ba50c109d 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -19,10 +19,13 @@ - - - - + + + + + + + @@ -62,10 +65,6 @@ Ryujinx.ico - - $(DefineConstants);MACOS_BUILD - - diff --git a/Ryujinx/Ui/Helper/MetalHelper.cs b/Ryujinx/Ui/Helper/MetalHelper.cs new file mode 100644 index 000000000..62ca29301 --- /dev/null +++ b/Ryujinx/Ui/Helper/MetalHelper.cs @@ -0,0 +1,134 @@ +using Gdk; +using System; +using System.Runtime.Versioning; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ui.Helper +{ + public delegate void UpdateBoundsCallbackDelegate(Window window); + + [SupportedOSPlatform("macos")] + static class MetalHelper + { + private const string LibObjCImport = "/usr/lib/libobjc.A.dylib"; + + private struct Selector + { + public readonly IntPtr NativePtr; + + public unsafe Selector(string value) + { + int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); + byte* data = stackalloc byte[size]; + + fixed (char* pValue = value) + { + System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); + } + + NativePtr = sel_registerName(data); + } + + public static implicit operator Selector(string value) => new Selector(value); + } + + private static unsafe IntPtr GetClass(string value) + { + int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); + byte* data = stackalloc byte[size]; + + fixed (char* pValue = value) + { + System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); + } + + return objc_getClass(data); + } + + private struct NSPoint + { + public double X; + public double Y; + + public NSPoint(double x, double y) + { + X = x; + Y = y; + } + } + + private struct NSRect + { + public NSPoint Pos; + public NSPoint Size; + + public NSRect(double x, double y, double width, double height) + { + Pos = new NSPoint(x, y); + Size = new NSPoint(width, height); + } + } + + public static IntPtr GetMetalLayer(Display display, Window window, out IntPtr nsView, out UpdateBoundsCallbackDelegate updateBounds) + { + nsView = gdk_quartz_window_get_nsview(window.Handle); + + // Create a new CAMetalLayer. + IntPtr layerClass = GetClass("CAMetalLayer"); + IntPtr metalLayer = IntPtr_objc_msgSend(layerClass, "alloc"); + objc_msgSend(metalLayer, "init"); + + // Create a child NSView to render into. + IntPtr nsViewClass = GetClass("NSView"); + IntPtr child = IntPtr_objc_msgSend(nsViewClass, "alloc"); + objc_msgSend(child, "init", new NSRect()); + + // Add it as a child. + objc_msgSend(nsView, "addSubview:", child); + + // Make its renderer our metal layer. + objc_msgSend(child, "setWantsLayer:", (byte)1); + objc_msgSend(child, "setLayer:", metalLayer); + objc_msgSend(metalLayer, "setContentsScale:", (double)display.GetMonitorAtWindow(window).ScaleFactor); + + // Set the frame position/location. + updateBounds = (Window window) => { + window.GetPosition(out int x, out int y); + int width = window.Width; + int height = window.Height; + objc_msgSend(child, "setFrame:", new NSRect(x, y, width, height)); + }; + + updateBounds(window); + + return metalLayer; + } + + [DllImport(LibObjCImport)] + private static unsafe extern IntPtr sel_registerName(byte* data); + + [DllImport(LibObjCImport)] + private static unsafe extern IntPtr objc_getClass(byte* data); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, byte value); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, NSRect point); + + [DllImport(LibObjCImport)] + private static extern void objc_msgSend(IntPtr receiver, Selector selector, double value); + + [DllImport(LibObjCImport, EntryPoint = "objc_msgSend")] + private static extern IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); + + [DllImport("libgdk-3.0.dylib")] + private static extern IntPtr gdk_quartz_window_get_nsview(IntPtr gdkWindow); + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 5216c7747..3a5b7723d 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -142,7 +142,7 @@ namespace Ryujinx.Ui public MainWindow() : this(new Builder("Ryujinx.Ui.MainWindow.glade")) { } - private MainWindow(Builder builder) : base(builder.GetObject("_mainWin").Handle) + private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("_mainWin")) { builder.Autoconnect(this); @@ -846,9 +846,7 @@ namespace Ryujinx.Ui _deviceExitStatus.Reset(); Translator.IsReadyForTranslation.Reset(); -#if MACOS_BUILD - CreateGameWindow(); -#else + Thread windowThread = new Thread(() => { CreateGameWindow(); @@ -858,7 +856,6 @@ namespace Ryujinx.Ui }; windowThread.Start(); -#endif _gameLoaded = true; _actionMenu.Sensitive = true; diff --git a/Ryujinx/Ui/VKRenderer.cs b/Ryujinx/Ui/VKRenderer.cs index 7e02c689d..63d0d0a62 100644 --- a/Ryujinx/Ui/VKRenderer.cs +++ b/Ryujinx/Ui/VKRenderer.cs @@ -1,9 +1,11 @@ using Gdk; using Ryujinx.Common.Configuration; using Ryujinx.Input.HLE; +using Ryujinx.Ui.Helper; using SPB.Graphics.Vulkan; using SPB.Platform.Win32; using SPB.Platform.X11; +using SPB.Platform.Metal; using SPB.Windowing; using System; using System.Runtime.InteropServices; @@ -13,6 +15,7 @@ namespace Ryujinx.Ui public class VKRenderer : RendererWidgetBase { public NativeWindowBase NativeWindow { get; private set; } + private UpdateBoundsCallbackDelegate _updateBoundsCallback; public VKRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel) { } @@ -31,6 +34,12 @@ namespace Ryujinx.Ui return new SimpleX11Window(new NativeHandle(displayHandle), new NativeHandle(windowHandle)); } + else if (OperatingSystem.IsMacOS()) + { + IntPtr metalLayer = MetalHelper.GetMetalLayer(Display, Window, out IntPtr nsView, out _updateBoundsCallback); + + return new SimpleMetalWindow(new NativeHandle(nsView), new NativeHandle(metalLayer)); + } throw new NotImplementedException(); } @@ -53,7 +62,11 @@ namespace Ryujinx.Ui WaitEvent.Set(); } - return base.OnConfigureEvent(evnt); + bool result = base.OnConfigureEvent(evnt); + + _updateBoundsCallback?.Invoke(Window); + + return result; } public unsafe IntPtr CreateWindowSurface(IntPtr instance) diff --git a/Ryujinx/Ui/Widgets/ProfileDialog.cs b/Ryujinx/Ui/Widgets/ProfileDialog.cs index 8748737c7..96b44d240 100644 --- a/Ryujinx/Ui/Widgets/ProfileDialog.cs +++ b/Ryujinx/Ui/Widgets/ProfileDialog.cs @@ -18,7 +18,7 @@ namespace Ryujinx.Ui.Widgets public ProfileDialog() : this(new Builder("Ryujinx.Ui.Widgets.ProfileDialog.glade")) { } - private ProfileDialog(Builder builder) : base(builder.GetObject("_profileDialog").Handle) + private ProfileDialog(Builder builder) : base(builder.GetRawOwnedObject("_profileDialog")) { builder.Autoconnect(this); Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); diff --git a/Ryujinx/Ui/Windows/CheatWindow.cs b/Ryujinx/Ui/Windows/CheatWindow.cs index a9dccd34f..917603b29 100644 --- a/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/Ryujinx/Ui/Windows/CheatWindow.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Ui.Windows public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName) { } - private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : base(builder.GetObject("_cheatWindow").Handle) + private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs index d043d0238..002f8fe22 100644 --- a/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -119,7 +119,7 @@ namespace Ryujinx.Ui.Windows public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { } - private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetObject("_controllerWin").Handle) + private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin")) { _mainWindow = mainWindow; _selectedGamepad = null; @@ -379,13 +379,16 @@ namespace Ryujinx.Ui.Windows break; } - _controllerImage.Pixbuf = _controllerType.ActiveId switch + if (!OperatingSystem.IsMacOS()) { - "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_ProCon.svg", 400, 400), - "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConLeft.svg", 400, 500), - "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConRight.svg", 400, 500), - _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConPair.svg", 400, 500), - }; + _controllerImage.Pixbuf = _controllerType.ActiveId switch + { + "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_ProCon.svg", 400, 400), + "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConLeft.svg", 400, 500), + "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConRight.svg", 400, 500), + _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConPair.svg", 400, 500), + }; + } } private void ClearValues() diff --git a/Ryujinx/Ui/Windows/DlcWindow.cs b/Ryujinx/Ui/Windows/DlcWindow.cs index 1a47ae414..0a97ac2a2 100644 --- a/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/Ryujinx/Ui/Windows/DlcWindow.cs @@ -34,7 +34,7 @@ namespace Ryujinx.Ui.Windows public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_dlcWindow").Handle) + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) { builder.Autoconnect(this); diff --git a/Ryujinx/Ui/Windows/SettingsWindow.cs b/Ryujinx/Ui/Windows/SettingsWindow.cs index 901973188..220bb82ae 100644 --- a/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -113,7 +113,7 @@ namespace Ryujinx.Ui.Windows public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Ui.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { } - private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetObject("_settingsWin").Handle) + private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin")) { Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); @@ -422,7 +422,7 @@ namespace Ryujinx.Ui.Windows Task.Run(() => { openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported; - soundIoIsSupported = SoundIoHardwareDeviceDriver.IsSupported; + soundIoIsSupported = !OperatingSystem.IsMacOS() && SoundIoHardwareDeviceDriver.IsSupported; sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported; }); @@ -438,6 +438,15 @@ namespace Ryujinx.Ui.Windows _ => throw new ArgumentOutOfRangeException() }; }); + + if (OperatingSystem.IsMacOS()) + { + var store = (_graphicsBackend.Model as ListStore); + store.GetIter(out TreeIter openglIter, new TreePath(new int[] {1})); + store.Remove(ref openglIter); + + _graphicsBackend.Model = store; + } } private void UpdatePreferredGpuComboBox() diff --git a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 94bf9e709..2618168cd 100644 --- a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Ui.Windows public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_titleUpdateWindow").Handle) + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent;