WindowBase.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. using ARMeilleure.Translation;
  2. using Ryujinx.Common.Configuration;
  3. using Ryujinx.Common.Configuration.Hid;
  4. using Ryujinx.Common.Logging;
  5. using Ryujinx.Graphics.GAL;
  6. using Ryujinx.Graphics.GAL.Multithreading;
  7. using Ryujinx.Graphics.Gpu;
  8. using Ryujinx.Graphics.OpenGL;
  9. using Ryujinx.HLE.HOS.Applets;
  10. using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
  11. using Ryujinx.HLE.Ui;
  12. using Ryujinx.Input;
  13. using Ryujinx.Input.HLE;
  14. using Ryujinx.SDL2.Common;
  15. using System;
  16. using System.Collections.Concurrent;
  17. using System.Collections.Generic;
  18. using System.Diagnostics;
  19. using System.IO;
  20. using System.Runtime.InteropServices;
  21. using System.Threading;
  22. using static SDL2.SDL;
  23. using Switch = Ryujinx.HLE.Switch;
  24. namespace Ryujinx.Headless.SDL2
  25. {
  26. abstract partial class WindowBase : IHostUiHandler, IDisposable
  27. {
  28. protected const int DefaultWidth = 1280;
  29. protected const int DefaultHeight = 720;
  30. 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;
  31. private const int TargetFps = 60;
  32. private static readonly ConcurrentQueue<Action> _mainThreadActions = new();
  33. [LibraryImport("SDL2")]
  34. // TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
  35. private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
  36. public static void QueueMainThreadAction(Action action)
  37. {
  38. _mainThreadActions.Enqueue(action);
  39. }
  40. public NpadManager NpadManager { get; }
  41. public TouchScreenManager TouchScreenManager { get; }
  42. public Switch Device { get; private set; }
  43. public IRenderer Renderer { get; private set; }
  44. public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
  45. protected IntPtr WindowHandle { get; set; }
  46. public IHostUiTheme HostUiTheme { get; }
  47. public int Width { get; private set; }
  48. public int Height { get; private set; }
  49. public bool IsFullscreen { get; set; }
  50. protected SDL2MouseDriver MouseDriver;
  51. private readonly InputManager _inputManager;
  52. private readonly IKeyboard _keyboardInterface;
  53. private readonly GraphicsDebugLevel _glLogLevel;
  54. private readonly Stopwatch _chrono;
  55. private readonly long _ticksPerFrame;
  56. private readonly CancellationTokenSource _gpuCancellationTokenSource;
  57. private readonly ManualResetEvent _exitEvent;
  58. private readonly ManualResetEvent _gpuDoneEvent;
  59. private long _ticks;
  60. private bool _isActive;
  61. private bool _isStopped;
  62. private uint _windowId;
  63. private string _gpuVendorName;
  64. private readonly AspectRatio _aspectRatio;
  65. private readonly bool _enableMouse;
  66. public WindowBase(
  67. InputManager inputManager,
  68. GraphicsDebugLevel glLogLevel,
  69. AspectRatio aspectRatio,
  70. bool enableMouse,
  71. HideCursorMode hideCursorMode)
  72. {
  73. MouseDriver = new SDL2MouseDriver(hideCursorMode);
  74. _inputManager = inputManager;
  75. _inputManager.SetMouseDriver(MouseDriver);
  76. NpadManager = _inputManager.CreateNpadManager();
  77. TouchScreenManager = _inputManager.CreateTouchScreenManager();
  78. _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
  79. _glLogLevel = glLogLevel;
  80. _chrono = new Stopwatch();
  81. _ticksPerFrame = Stopwatch.Frequency / TargetFps;
  82. _gpuCancellationTokenSource = new CancellationTokenSource();
  83. _exitEvent = new ManualResetEvent(false);
  84. _gpuDoneEvent = new ManualResetEvent(false);
  85. _aspectRatio = aspectRatio;
  86. _enableMouse = enableMouse;
  87. HostUiTheme = new HeadlessHostUiTheme();
  88. SDL2Driver.Instance.Initialize();
  89. }
  90. public void Initialize(Switch device, List<InputConfig> inputConfigs, bool enableKeyboard, bool enableMouse)
  91. {
  92. Device = device;
  93. IRenderer renderer = Device.Gpu.Renderer;
  94. if (renderer is ThreadedRenderer tr)
  95. {
  96. renderer = tr.BaseRenderer;
  97. }
  98. Renderer = renderer;
  99. NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse);
  100. TouchScreenManager.Initialize(device);
  101. }
  102. private void SetWindowIcon()
  103. {
  104. Stream iconStream = typeof(WindowBase).Assembly.GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
  105. byte[] iconBytes = new byte[iconStream!.Length];
  106. if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
  107. {
  108. Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
  109. iconStream.Close();
  110. return;
  111. }
  112. iconStream.Close();
  113. unsafe
  114. {
  115. fixed (byte* iconPtr = iconBytes)
  116. {
  117. IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
  118. IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
  119. SDL_SetWindowIcon(WindowHandle, iconHandle);
  120. SDL_FreeSurface(iconHandle);
  121. }
  122. }
  123. }
  124. private void InitializeWindow()
  125. {
  126. var activeProcess = Device.Processes.ActiveApplication;
  127. var nacp = activeProcess.ApplicationControlProperties;
  128. int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
  129. string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
  130. string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
  131. string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
  132. string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
  133. SDL_WindowFlags fullscreenFlag = IsFullscreen ? SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
  134. WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | fullscreenFlag | GetWindowFlags());
  135. if (WindowHandle == IntPtr.Zero)
  136. {
  137. string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
  138. Logger.Error?.Print(LogClass.Application, errorMessage);
  139. throw new Exception(errorMessage);
  140. }
  141. SetWindowIcon();
  142. _windowId = SDL_GetWindowID(WindowHandle);
  143. SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
  144. Width = DefaultWidth;
  145. Height = DefaultHeight;
  146. }
  147. private void HandleWindowEvent(SDL_Event evnt)
  148. {
  149. if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
  150. {
  151. switch (evnt.window.windowEvent)
  152. {
  153. case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
  154. // Unlike on Windows, this event fires on macOS when triggering fullscreen mode.
  155. // And promptly crashes the process because `Renderer?.window.SetSize` is undefined.
  156. // As we don't need this to fire in either case we can test for isFullscreen.
  157. if (!IsFullscreen)
  158. {
  159. Width = evnt.window.data1;
  160. Height = evnt.window.data2;
  161. Renderer?.Window.SetSize(Width, Height);
  162. MouseDriver.SetClientSize(Width, Height);
  163. }
  164. break;
  165. case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
  166. Exit();
  167. break;
  168. }
  169. }
  170. else
  171. {
  172. MouseDriver.Update(evnt);
  173. }
  174. }
  175. protected abstract void InitializeWindowRenderer();
  176. protected abstract void InitializeRenderer();
  177. protected abstract void FinalizeWindowRenderer();
  178. protected abstract void SwapBuffers();
  179. public abstract SDL_WindowFlags GetWindowFlags();
  180. private string GetGpuVendorName()
  181. {
  182. return Renderer.GetHardwareInfo().GpuVendor;
  183. }
  184. public void Render()
  185. {
  186. InitializeWindowRenderer();
  187. Device.Gpu.Renderer.Initialize(_glLogLevel);
  188. InitializeRenderer();
  189. _gpuVendorName = GetGpuVendorName();
  190. Device.Gpu.Renderer.RunLoop(() =>
  191. {
  192. Device.Gpu.SetGpuThread();
  193. Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
  194. Translator.IsReadyForTranslation.Set();
  195. while (_isActive)
  196. {
  197. if (_isStopped)
  198. {
  199. return;
  200. }
  201. _ticks += _chrono.ElapsedTicks;
  202. _chrono.Restart();
  203. if (Device.WaitFifo())
  204. {
  205. Device.Statistics.RecordFifoStart();
  206. Device.ProcessFrame();
  207. Device.Statistics.RecordFifoEnd();
  208. }
  209. while (Device.ConsumeFrameAvailable())
  210. {
  211. Device.PresentFrame(SwapBuffers);
  212. }
  213. if (_ticks >= _ticksPerFrame)
  214. {
  215. string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
  216. float scale = GraphicsConfig.ResScale;
  217. if (scale != 1)
  218. {
  219. dockedMode += $" ({scale}x)";
  220. }
  221. StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
  222. Device.EnableDeviceVsync,
  223. dockedMode,
  224. Device.Configuration.AspectRatio.ToText(),
  225. $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
  226. $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
  227. $"GPU: {_gpuVendorName}"));
  228. _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
  229. }
  230. }
  231. // Make sure all commands in the run loop are fully executed before leaving the loop.
  232. if (Device.Gpu.Renderer is ThreadedRenderer threaded)
  233. {
  234. threaded.FlushThreadedCommands();
  235. }
  236. _gpuDoneEvent.Set();
  237. });
  238. FinalizeWindowRenderer();
  239. }
  240. public void Exit()
  241. {
  242. TouchScreenManager?.Dispose();
  243. NpadManager?.Dispose();
  244. if (_isStopped)
  245. {
  246. return;
  247. }
  248. _gpuCancellationTokenSource.Cancel();
  249. _isStopped = true;
  250. _isActive = false;
  251. _exitEvent.WaitOne();
  252. _exitEvent.Dispose();
  253. }
  254. public static void ProcessMainThreadQueue()
  255. {
  256. while (_mainThreadActions.TryDequeue(out Action action))
  257. {
  258. action();
  259. }
  260. }
  261. public void MainLoop()
  262. {
  263. while (_isActive)
  264. {
  265. UpdateFrame();
  266. SDL_PumpEvents();
  267. ProcessMainThreadQueue();
  268. // Polling becomes expensive if it's not slept
  269. Thread.Sleep(1);
  270. }
  271. _exitEvent.Set();
  272. }
  273. private void NvidiaStutterWorkaround()
  274. {
  275. while (_isActive)
  276. {
  277. // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
  278. // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
  279. // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
  280. // This creates a new thread every second or so.
  281. // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
  282. // This is a little over budget on a frame time of 16ms, so creates a large stutter.
  283. // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
  284. // TODO: This should be removed when the issue with the GateThread is resolved.
  285. ThreadPool.QueueUserWorkItem(state => { });
  286. Thread.Sleep(300);
  287. }
  288. }
  289. private bool UpdateFrame()
  290. {
  291. if (!_isActive)
  292. {
  293. return true;
  294. }
  295. if (_isStopped)
  296. {
  297. return false;
  298. }
  299. NpadManager.Update();
  300. // Touchscreen
  301. bool hasTouch = false;
  302. // Get screen touch position
  303. if (!_enableMouse)
  304. {
  305. hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
  306. }
  307. if (!hasTouch)
  308. {
  309. TouchScreenManager.Update(false);
  310. }
  311. Device.Hid.DebugPad.Update();
  312. // TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
  313. MouseDriver.UpdatePosition();
  314. return true;
  315. }
  316. public void Execute()
  317. {
  318. _chrono.Restart();
  319. _isActive = true;
  320. InitializeWindow();
  321. Thread renderLoopThread = new(Render)
  322. {
  323. Name = "GUI.RenderLoop",
  324. };
  325. renderLoopThread.Start();
  326. Thread nvidiaStutterWorkaround = null;
  327. if (Renderer is OpenGLRenderer)
  328. {
  329. nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
  330. {
  331. Name = "GUI.NvidiaStutterWorkaround",
  332. };
  333. nvidiaStutterWorkaround.Start();
  334. }
  335. MainLoop();
  336. // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
  337. // We only need to wait for all commands submitted during the main gpu loop to be processed.
  338. _gpuDoneEvent.WaitOne();
  339. _gpuDoneEvent.Dispose();
  340. nvidiaStutterWorkaround?.Join();
  341. Exit();
  342. }
  343. public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
  344. {
  345. // SDL2 doesn't support input dialogs
  346. userText = "Ryujinx";
  347. return true;
  348. }
  349. public bool DisplayMessageDialog(string title, string message)
  350. {
  351. SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
  352. return true;
  353. }
  354. public bool DisplayMessageDialog(ControllerAppletUiArgs args)
  355. {
  356. string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
  357. string message = $"Application requests {playerCount} player(s) with:\n\n"
  358. + $"TYPES: {args.SupportedStyles}\n\n"
  359. + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n"
  360. + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "")
  361. + "Please reconfigure Input now and then press OK.";
  362. return DisplayMessageDialog("Controller Applet", message);
  363. }
  364. public IDynamicTextInputHandler CreateDynamicTextInputHandler()
  365. {
  366. return new HeadlessDynamicTextInputHandler();
  367. }
  368. public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
  369. {
  370. device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
  371. Exit();
  372. }
  373. public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
  374. {
  375. SDL_MessageBoxData data = new()
  376. {
  377. title = title,
  378. message = message,
  379. buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
  380. numbuttons = buttonsText.Length,
  381. window = WindowHandle,
  382. };
  383. for (int i = 0; i < buttonsText.Length; i++)
  384. {
  385. data.buttons[i] = new SDL_MessageBoxButtonData
  386. {
  387. buttonid = i,
  388. text = buttonsText[i],
  389. };
  390. }
  391. SDL_ShowMessageBox(ref data, out int _);
  392. return true;
  393. }
  394. public void Dispose()
  395. {
  396. Dispose(true);
  397. }
  398. protected virtual void Dispose(bool disposing)
  399. {
  400. if (disposing)
  401. {
  402. _isActive = false;
  403. TouchScreenManager?.Dispose();
  404. NpadManager.Dispose();
  405. SDL2Driver.Instance.UnregisterWindow(_windowId);
  406. SDL_DestroyWindow(WindowHandle);
  407. SDL2Driver.Instance.Dispose();
  408. }
  409. }
  410. }
  411. }