From eca8808649b7d763dd6b528bd26d43b7bd839cd2 Mon Sep 17 00:00:00 2001
From: Theun de Bruijn <theun@theundebruijn.com>
Date: Tue, 26 Sep 2023 07:40:16 +1000
Subject: [PATCH] Headless: Add support for Scaling Filters, Anti-aliasing and
 Exclusive Fullscreen (#5412)

* Headless: Added support for fullscreen option

* Headless: cleanup of fullscreen support

* Headless: fullscreen support : implemented proposed changes

* Headless: fullscreen support: cleanup

* Headless: exclusive fullscreen support: wip

* Headless: exclusive fullscreen support: add. windows scale interop

* Headless: exclusive fullscreen support: cleanup

* Headless: exclusive fullscreen support: cleanup

* Headless: fullscreen support: fix for OpenGL scaling

* Headless: fullscreen support: cleanup

* Headless: fullscreen support: cleanup

* Headless: fullscreen support: add. Vulkan fix

* Headless: fullscreen support: add. macOS fullscreen fix

* Headless: fullscreen support: cleanup

* Headless: fullscreen support: cleanup

* Headless: fullscreen support: cleanup

* Headless: exclusive fullscreen support: add. display selection logic

* Headless: exclusive fullscreen support: add. anti-aliasing + scaling-filter logic

* Headless: exclusive fullscreen support: upd. options to be case-insensitive

* Headless: exclusive fullscreen support: force default values for scaling + anti-aliasing options

* Headless: upd. OpenGL --fullscreen window size logic

* Headless: upd. fullscreen logic

* Headless: cleanup

* Headless: refac. DisplayId option naming

* Headless: refac. scaling + anti-aliasing option handling

* Headless: refac. namespace handling

* Headless: upd. imports ordering

* Apply suggestions from code review

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
---
 .../OpenGL/OpenGLWindow.cs                    | 10 ++--
 src/Ryujinx.Headless.SDL2/Options.cs          | 23 +++++++-
 src/Ryujinx.Headless.SDL2/Program.cs          |  7 +++
 .../Vulkan/VulkanWindow.cs                    | 12 ++++-
 src/Ryujinx.Headless.SDL2/WindowBase.cs       | 53 ++++++++++++++++---
 5 files changed, 91 insertions(+), 14 deletions(-)

diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
index a2b26bbc58..245aba7782 100644
--- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
@@ -151,11 +151,15 @@ namespace Ryujinx.Headless.SDL2.OpenGL
             GL.Clear(ClearBufferMask.ColorBufferBit);
             SwapBuffers();
 
-            if (IsFullscreen)
+            if (IsExclusiveFullscreen)
+            {
+                Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+                MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+            }
+            else if (IsFullscreen)
             {
                 // NOTE: grabbing the main display's dimensions directly as OpenGL doesn't scale along like the VulkanWindow.
-                // we might have to amend this if people run this on a non-primary display set to a different resolution.
-                if (SDL_GetDisplayBounds(0, out SDL_Rect displayBounds) < 0)
+                if (SDL_GetDisplayBounds(DisplayId, out SDL_Rect displayBounds) < 0)
                 {
                     Logger.Warning?.Print(LogClass.Application, $"Could not retrieve display bounds: {SDL_GetError()}");
 
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
index e44cedec96..a1adcd0242 100644
--- a/src/Ryujinx.Headless.SDL2/Options.cs
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -14,9 +14,21 @@ namespace Ryujinx.Headless.SDL2
         [Option("profile", Required = false, HelpText = "Set the user profile to launch the game with.")]
         public string UserProfile { get; set; }
 
-        [Option("fullscreen", Required = false, HelpText = "Launch the game in fullscreen mode.")]
+        [Option("display-id", Required = false, Default = 0, HelpText = "Set the display to use - especially helpful for fullscreen mode. [0-n]")]
+        public int DisplayId { get; set; }
+
+        [Option("fullscreen", Required = false, Default = false, HelpText = "Launch the game in fullscreen mode.")]
         public bool IsFullscreen { get; set; }
 
+        [Option("exclusive-fullscreen", Required = false, Default = false, HelpText = "Launch the game in exclusive fullscreen mode.")]
+        public bool IsExclusiveFullscreen { get; set; }
+
+        [Option("exclusive-fullscreen-width", Required = false, Default = 1920, HelpText = "Set horizontal resolution for exclusive fullscreen mode.")]
+        public int ExclusiveFullscreenWidth { get; set; }
+
+        [Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
+        public int ExclusiveFullscreenHeight { get; set; }
+
         // Input
 
         [Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")]
@@ -196,6 +208,15 @@ namespace Ryujinx.Headless.SDL2
         [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
         public string PreferredGPUVendor { get; set; }
 
+        [Option("anti-aliasing", Required = false, Default = AntiAliasing.None, HelpText = "Set the type of anti aliasing being used. [None|Fxaa|SmaaLow|SmaaMedium|SmaaHigh|SmaaUltra]")]
+        public AntiAliasing AntiAliasing { get; set; }
+
+        [Option("scaling-filter", Required = false, Default = ScalingFilter.Bilinear, HelpText = "Set the scaling filter. [Bilinear|Nearest|Fsr]")]
+        public ScalingFilter ScalingFilter { get; set; }
+
+        [Option("scaling-filter-level", Required = false, Default = 0, HelpText = "Set the scaling filter intensity (currently only applies to FSR). [0-100]")]
+        public int ScalingFilterLevel { get; set; }
+
         // Hacks
 
         [Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")]
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index 98cc5abf4a..e90d5595ee 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -596,6 +596,13 @@ namespace Ryujinx.Headless.SDL2
             _window = window;
 
             _window.IsFullscreen = options.IsFullscreen;
+            _window.DisplayId = options.DisplayId;
+            _window.IsExclusiveFullscreen = options.IsExclusiveFullscreen;
+            _window.ExclusiveFullscreenWidth = options.ExclusiveFullscreenWidth;
+            _window.ExclusiveFullscreenHeight = options.ExclusiveFullscreenHeight;
+            _window.AntiAliasing = options.AntiAliasing;
+            _window.ScalingFilter = options.ScalingFilter;
+            _window.ScalingFilterLevel = options.ScalingFilterLevel;
 
             _emulationContext = InitializeEmulationContext(window, renderer, options);
 
diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
index f2f337a531..4a04b10a32 100644
--- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
@@ -29,8 +29,16 @@ namespace Ryujinx.Headless.SDL2.Vulkan
 
         protected override void InitializeRenderer()
         {
-            Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
-            MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+            if (IsExclusiveFullscreen)
+            {
+                Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+                MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+            }
+            else
+            {
+                Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
+                MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+            }
         }
 
         private static void BasicInvoke(Action action)
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
index 0b91da5c6c..1b9556057a 100644
--- a/src/Ryujinx.Headless.SDL2/WindowBase.cs
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -20,6 +20,8 @@ using System.IO;
 using System.Runtime.InteropServices;
 using System.Threading;
 using static SDL2.SDL;
+using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
+using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
 using Switch = Ryujinx.HLE.Switch;
 
 namespace Ryujinx.Headless.SDL2
@@ -28,8 +30,9 @@ namespace Ryujinx.Headless.SDL2
     {
         protected const int DefaultWidth = 1280;
         protected const int DefaultHeight = 720;
-        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 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 SDL_WindowFlags FullscreenFlag = 0;
 
         private static readonly ConcurrentQueue<Action> _mainThreadActions = new();
 
@@ -54,7 +57,14 @@ namespace Ryujinx.Headless.SDL2
         public IHostUiTheme HostUiTheme { get; }
         public int Width { get; private set; }
         public int Height { get; private set; }
+        public int DisplayId { get; set; }
         public bool IsFullscreen { get; set; }
+        public bool IsExclusiveFullscreen { get; set; }
+        public int ExclusiveFullscreenWidth { get; set; }
+        public int ExclusiveFullscreenHeight { get; set; }
+        public AntiAliasing AntiAliasing { get; set; }
+        public ScalingFilter ScalingFilter { get; set; }
+        public int ScalingFilterLevel { get; set; }
 
         protected SDL2MouseDriver MouseDriver;
         private readonly InputManager _inputManager;
@@ -158,9 +168,24 @@ namespace Ryujinx.Headless.SDL2
             string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
             string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
 
-            SDL_WindowFlags fullscreenFlag = IsFullscreen ? SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
+            Width = DefaultWidth;
+            Height = DefaultHeight;
 
-            WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | fullscreenFlag | GetWindowFlags());
+            if (IsExclusiveFullscreen)
+            {
+                Width = ExclusiveFullscreenWidth;
+                Height = ExclusiveFullscreenHeight;
+
+                DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
+                FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
+            }
+            else if (IsFullscreen)
+            {
+                DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
+                FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
+            }
+
+            WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
 
             if (WindowHandle == IntPtr.Zero)
             {
@@ -175,9 +200,6 @@ namespace Ryujinx.Headless.SDL2
 
             _windowId = SDL_GetWindowID(WindowHandle);
             SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
-
-            Width = DefaultWidth;
-            Height = DefaultHeight;
         }
 
         private void HandleWindowEvent(SDL_Event evnt)
@@ -189,8 +211,8 @@ namespace Ryujinx.Headless.SDL2
                     case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
                         // Unlike on Windows, this event fires on macOS when triggering fullscreen mode.
                         // And promptly crashes the process because `Renderer?.window.SetSize` is undefined.
-                        // As we don't need this to fire in either case we can test for isFullscreen.
-                        if (!IsFullscreen)
+                        // As we don't need this to fire in either case we can test for fullscreen.
+                        if (!IsFullscreen && !IsExclusiveFullscreen)
                         {
                             Width = evnt.window.data1;
                             Height = evnt.window.data2;
@@ -225,6 +247,17 @@ namespace Ryujinx.Headless.SDL2
             return Renderer.GetHardwareInfo().GpuVendor;
         }
 
+        private void SetAntiAliasing()
+        {
+            Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)AntiAliasing);
+        }
+
+        private void SetScalingFilter()
+        {
+            Renderer?.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ScalingFilter);
+            Renderer?.Window.SetScalingFilterLevel(ScalingFilterLevel);
+        }
+
         public void Render()
         {
             InitializeWindowRenderer();
@@ -233,6 +266,10 @@ namespace Ryujinx.Headless.SDL2
 
             InitializeRenderer();
 
+            SetAntiAliasing();
+
+            SetScalingFilter();
+
             _gpuVendorName = GetGpuVendorName();
 
             Device.Gpu.Renderer.RunLoop(() =>