diff options
Diffstat (limited to 'src/Ryujinx.Headless.SDL2/WindowBase.cs')
-rw-r--r-- | src/Ryujinx.Headless.SDL2/WindowBase.cs | 499 |
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 |