aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Headless.SDL2/WindowBase.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Headless.SDL2/WindowBase.cs')
-rw-r--r--src/Ryujinx.Headless.SDL2/WindowBase.cs499
1 files changed, 499 insertions, 0 deletions
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
new file mode 100644
index 00000000..e3371042
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -0,0 +1,499 @@
+using ARMeilleure.Translation;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
+using Ryujinx.HLE.Ui;
+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.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading;
+using static SDL2.SDL;
+using Switch = Ryujinx.HLE.Switch;
+
+namespace Ryujinx.Headless.SDL2
+{
+ abstract partial class WindowBase : IHostUiHandler, IDisposable
+ {
+ 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 static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();
+
+ [LibraryImport("SDL2")]
+ // TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
+ private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
+
+ public static void QueueMainThreadAction(Action action)
+ {
+ MainThreadActions.Enqueue(action);
+ }
+
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+ public Switch Device { get; private set; }
+ public IRenderer Renderer { get; private set; }
+
+ public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
+
+ protected IntPtr WindowHandle { get; set; }
+
+ public IHostUiTheme HostUiTheme { get; }
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+
+ protected SDL2MouseDriver MouseDriver;
+ private InputManager _inputManager;
+ private IKeyboard _keyboardInterface;
+ private GraphicsDebugLevel _glLogLevel;
+ private readonly Stopwatch _chrono;
+ private readonly long _ticksPerFrame;
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+ private readonly ManualResetEvent _exitEvent;
+
+ private long _ticks;
+ private bool _isActive;
+ private bool _isStopped;
+ private uint _windowId;
+
+ private string _gpuVendorName;
+
+ private AspectRatio _aspectRatio;
+ private bool _enableMouse;
+
+ public WindowBase(
+ InputManager inputManager,
+ GraphicsDebugLevel glLogLevel,
+ AspectRatio aspectRatio,
+ bool enableMouse,
+ HideCursor hideCursor)
+ {
+ MouseDriver = new SDL2MouseDriver(hideCursor);
+ _inputManager = inputManager;
+ _inputManager.SetMouseDriver(MouseDriver);
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+ _glLogLevel = glLogLevel;
+ _chrono = new Stopwatch();
+ _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+ _exitEvent = new ManualResetEvent(false);
+ _aspectRatio = aspectRatio;
+ _enableMouse = enableMouse;
+ HostUiTheme = new HeadlessHostUiTheme();
+
+ SDL2Driver.Instance.Initialize();
+ }
+
+ public void Initialize(Switch device, List<InputConfig> inputConfigs, bool enableKeyboard, bool enableMouse)
+ {
+ Device = device;
+
+ IRenderer renderer = Device.Gpu.Renderer;
+
+ if (renderer is ThreadedRenderer tr)
+ {
+ renderer = tr.BaseRenderer;
+ }
+
+ Renderer = renderer;
+
+ NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse);
+ TouchScreenManager.Initialize(device);
+ }
+
+ private void SetWindowIcon()
+ {
+ Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
+ byte[] iconBytes = new byte[iconStream!.Length];
+
+ if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
+ {
+ Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
+ iconStream.Close();
+
+ return;
+ }
+
+ iconStream.Close();
+
+ unsafe
+ {
+ fixed (byte* iconPtr = iconBytes)
+ {
+ IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
+ IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
+
+ SDL_SetWindowIcon(WindowHandle, iconHandle);
+ SDL_FreeSurface(iconHandle);
+ }
+ }
+ }
+
+ private void InitializeWindow()
+ {
+ var activeProcess = Device.Processes.ActiveApplication;
+ var nacp = activeProcess.ApplicationControlProperties;
+ int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
+
+ string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
+ string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
+ string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
+ string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
+
+ WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags());
+
+ if (WindowHandle == IntPtr.Zero)
+ {
+ string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+
+ SetWindowIcon();
+
+ _windowId = SDL_GetWindowID(WindowHandle);
+ SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
+
+ Width = DefaultWidth;
+ Height = DefaultHeight;
+ }
+
+ private void HandleWindowEvent(SDL_Event evnt)
+ {
+ if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
+ {
+ switch (evnt.window.windowEvent)
+ {
+ case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
+ Width = evnt.window.data1;
+ Height = evnt.window.data2;
+ Renderer?.Window.SetSize(Width, Height);
+ MouseDriver.SetClientSize(Width, Height);
+ break;
+
+ case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
+ Exit();
+ break;
+
+ default:
+ break;
+ }
+ }
+ else
+ {
+ MouseDriver.Update(evnt);
+ }
+ }
+
+ protected abstract void InitializeWindowRenderer();
+
+ protected abstract void InitializeRenderer();
+
+ protected abstract void FinalizeWindowRenderer();
+
+ protected abstract void SwapBuffers();
+
+ public abstract SDL_WindowFlags GetWindowFlags();
+
+ private string GetGpuVendorName()
+ {
+ return Renderer.GetHardwareInfo().GpuVendor;
+ }
+
+ public void Render()
+ {
+ InitializeWindowRenderer();
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ InitializeRenderer();
+
+ _gpuVendorName = GetGpuVendorName();
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+ Translator.IsReadyForTranslation.Set();
+
+ while (_isActive)
+ {
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _ticks += _chrono.ElapsedTicks;
+
+ _chrono.Restart();
+
+ if (Device.WaitFifo())
+ {
+ Device.Statistics.RecordFifoStart();
+ Device.ProcessFrame();
+ Device.Statistics.RecordFifoEnd();
+ }
+
+ while (Device.ConsumeFrameAvailable())
+ {
+ Device.PresentFrame(SwapBuffers);
+ }
+
+ if (_ticks >= _ticksPerFrame)
+ {
+ string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
+ float scale = Graphics.Gpu.GraphicsConfig.ResScale;
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ dockedMode,
+ Device.Configuration.AspectRatio.ToText(),
+ $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
+ $"GPU: {_gpuVendorName}"));
+
+ _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
+ }
+ }
+ });
+
+ FinalizeWindowRenderer();
+ }
+
+ public void Exit()
+ {
+ TouchScreenManager?.Dispose();
+ NpadManager?.Dispose();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _gpuCancellationTokenSource.Cancel();
+
+ _isStopped = true;
+ _isActive = false;
+
+ _exitEvent.WaitOne();
+ _exitEvent.Dispose();
+ }
+
+ public void ProcessMainThreadQueue()
+ {
+ while (MainThreadActions.TryDequeue(out Action action))
+ {
+ action();
+ }
+ }
+
+ public void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ SDL_PumpEvents();
+
+ ProcessMainThreadQueue();
+
+ // Polling becomes expensive if it's not slept
+ Thread.Sleep(1);
+ }
+
+ _exitEvent.Set();
+ }
+
+ private void NVStutterWorkaround()
+ {
+ while (_isActive)
+ {
+ // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
+ // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
+ // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
+ // This creates a new thread every second or so.
+ // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
+ // This is a little over budget on a frame time of 16ms, so creates a large stutter.
+ // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
+
+ // TODO: This should be removed when the issue with the GateThread is resolved.
+
+ ThreadPool.QueueUserWorkItem((state) => { });
+ Thread.Sleep(300);
+ }
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return true;
+ }
+
+ if (_isStopped)
+ {
+ return false;
+ }
+
+ NpadManager.Update();
+
+ // Touchscreen
+ bool hasTouch = false;
+
+ // Get screen touch position
+ if (!_enableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ TouchScreenManager.Update(false);
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ // TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
+ MouseDriver.UpdatePosition();
+
+ return true;
+ }
+
+ public void Execute()
+ {
+ _chrono.Restart();
+ _isActive = true;
+
+ InitializeWindow();
+
+ Thread renderLoopThread = new Thread(Render)
+ {
+ Name = "GUI.RenderLoop"
+ };
+ renderLoopThread.Start();
+
+ Thread nvStutterWorkaround = null;
+ if (Renderer is Graphics.OpenGL.OpenGLRenderer)
+ {
+ nvStutterWorkaround = new Thread(NVStutterWorkaround)
+ {
+ Name = "GUI.NVStutterWorkaround"
+ };
+ nvStutterWorkaround.Start();
+ }
+
+ MainLoop();
+
+ renderLoopThread.Join();
+ nvStutterWorkaround?.Join();
+
+ Exit();
+ }
+
+ public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
+ {
+ // SDL2 doesn't support input dialogs
+ userText = "Ryujinx";
+
+ return true;
+ }
+
+ public bool DisplayMessageDialog(string title, string message)
+ {
+ SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
+
+ return true;
+ }
+
+ public bool DisplayMessageDialog(ControllerAppletUiArgs args)
+ {
+ string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
+
+ string message = $"Application requests {playerCount} player(s) with:\n\n"
+ + $"TYPES: {args.SupportedStyles}\n\n"
+ + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n"
+ + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "")
+ + "Please reconfigure Input now and then press OK.";
+
+ return DisplayMessageDialog("Controller Applet", message);
+ }
+
+ public IDynamicTextInputHandler CreateDynamicTextInputHandler()
+ {
+ return new HeadlessDynamicTextInputHandler();
+ }
+
+ public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
+ {
+ device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
+
+ Exit();
+ }
+
+ public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
+ {
+ SDL_MessageBoxData data = new SDL_MessageBoxData
+ {
+ title = title,
+ message = message,
+ buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
+ numbuttons = buttonsText.Length,
+ window = WindowHandle
+ };
+
+ for (int i = 0; i < buttonsText.Length; i++)
+ {
+ data.buttons[i] = new SDL_MessageBoxButtonData
+ {
+ buttonid = i,
+ text = buttonsText[i]
+ };
+ }
+
+ SDL_ShowMessageBox(ref data, out int _);
+
+ return true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _isActive = false;
+ TouchScreenManager?.Dispose();
+ NpadManager.Dispose();
+
+ SDL2Driver.Instance.UnregisterWindow(_windowId);
+
+ SDL_DestroyWindow(WindowHandle);
+
+ SDL2Driver.Instance.Dispose();
+ }
+ }
+ }
+} \ No newline at end of file