WindowBase.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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.Reflection;
  21. using System.Runtime.InteropServices;
  22. using System.Threading;
  23. using static SDL2.SDL;
  24. using Switch = Ryujinx.HLE.Switch;
  25. namespace Ryujinx.Headless.SDL2
  26. {
  27. abstract partial class WindowBase : IHostUiHandler, IDisposable
  28. {
  29. protected const int DefaultWidth = 1280;
  30. protected const int DefaultHeight = 720;
  31. 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;
  32. private const int TargetFps = 60;
  33. private static readonly ConcurrentQueue<Action> _mainThreadActions = new();
  34. [LibraryImport("SDL2")]
  35. // TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
  36. private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
  37. public static void QueueMainThreadAction(Action action)
  38. {
  39. _mainThreadActions.Enqueue(action);
  40. }
  41. public NpadManager NpadManager { get; }
  42. public TouchScreenManager TouchScreenManager { get; }
  43. public Switch Device { get; private set; }
  44. public IRenderer Renderer { get; private set; }
  45. public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
  46. protected IntPtr WindowHandle { get; set; }
  47. public IHostUiTheme HostUiTheme { get; }
  48. public int Width { get; private set; }
  49. public int Height { get; private 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 = Assembly.GetExecutingAssembly().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. WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags());
  134. if (WindowHandle == IntPtr.Zero)
  135. {
  136. string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
  137. Logger.Error?.Print(LogClass.Application, errorMessage);
  138. throw new Exception(errorMessage);
  139. }
  140. SetWindowIcon();
  141. _windowId = SDL_GetWindowID(WindowHandle);
  142. SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
  143. Width = DefaultWidth;
  144. Height = DefaultHeight;
  145. }
  146. private void HandleWindowEvent(SDL_Event evnt)
  147. {
  148. if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
  149. {
  150. switch (evnt.window.windowEvent)
  151. {
  152. case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
  153. Width = evnt.window.data1;
  154. Height = evnt.window.data2;
  155. Renderer?.Window.SetSize(Width, Height);
  156. MouseDriver.SetClientSize(Width, Height);
  157. break;
  158. case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
  159. Exit();
  160. break;
  161. }
  162. }
  163. else
  164. {
  165. MouseDriver.Update(evnt);
  166. }
  167. }
  168. protected abstract void InitializeWindowRenderer();
  169. protected abstract void InitializeRenderer();
  170. protected abstract void FinalizeWindowRenderer();
  171. protected abstract void SwapBuffers();
  172. public abstract SDL_WindowFlags GetWindowFlags();
  173. private string GetGpuVendorName()
  174. {
  175. return Renderer.GetHardwareInfo().GpuVendor;
  176. }
  177. public void Render()
  178. {
  179. InitializeWindowRenderer();
  180. Device.Gpu.Renderer.Initialize(_glLogLevel);
  181. InitializeRenderer();
  182. _gpuVendorName = GetGpuVendorName();
  183. Device.Gpu.Renderer.RunLoop(() =>
  184. {
  185. Device.Gpu.SetGpuThread();
  186. Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
  187. Translator.IsReadyForTranslation.Set();
  188. while (_isActive)
  189. {
  190. if (_isStopped)
  191. {
  192. return;
  193. }
  194. _ticks += _chrono.ElapsedTicks;
  195. _chrono.Restart();
  196. if (Device.WaitFifo())
  197. {
  198. Device.Statistics.RecordFifoStart();
  199. Device.ProcessFrame();
  200. Device.Statistics.RecordFifoEnd();
  201. }
  202. while (Device.ConsumeFrameAvailable())
  203. {
  204. Device.PresentFrame(SwapBuffers);
  205. }
  206. if (_ticks >= _ticksPerFrame)
  207. {
  208. string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
  209. float scale = GraphicsConfig.ResScale;
  210. if (scale != 1)
  211. {
  212. dockedMode += $" ({scale}x)";
  213. }
  214. StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
  215. Device.EnableDeviceVsync,
  216. dockedMode,
  217. Device.Configuration.AspectRatio.ToText(),
  218. $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
  219. $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
  220. $"GPU: {_gpuVendorName}"));
  221. _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
  222. }
  223. }
  224. // Make sure all commands in the run loop are fully executed before leaving the loop.
  225. if (Device.Gpu.Renderer is ThreadedRenderer threaded)
  226. {
  227. threaded.FlushThreadedCommands();
  228. }
  229. _gpuDoneEvent.Set();
  230. });
  231. FinalizeWindowRenderer();
  232. }
  233. public void Exit()
  234. {
  235. TouchScreenManager?.Dispose();
  236. NpadManager?.Dispose();
  237. if (_isStopped)
  238. {
  239. return;
  240. }
  241. _gpuCancellationTokenSource.Cancel();
  242. _isStopped = true;
  243. _isActive = false;
  244. _exitEvent.WaitOne();
  245. _exitEvent.Dispose();
  246. }
  247. public static void ProcessMainThreadQueue()
  248. {
  249. while (_mainThreadActions.TryDequeue(out Action action))
  250. {
  251. action();
  252. }
  253. }
  254. public void MainLoop()
  255. {
  256. while (_isActive)
  257. {
  258. UpdateFrame();
  259. SDL_PumpEvents();
  260. ProcessMainThreadQueue();
  261. // Polling becomes expensive if it's not slept
  262. Thread.Sleep(1);
  263. }
  264. _exitEvent.Set();
  265. }
  266. private void NvidiaStutterWorkaround()
  267. {
  268. while (_isActive)
  269. {
  270. // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
  271. // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
  272. // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
  273. // This creates a new thread every second or so.
  274. // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
  275. // This is a little over budget on a frame time of 16ms, so creates a large stutter.
  276. // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
  277. // TODO: This should be removed when the issue with the GateThread is resolved.
  278. ThreadPool.QueueUserWorkItem(state => { });
  279. Thread.Sleep(300);
  280. }
  281. }
  282. private bool UpdateFrame()
  283. {
  284. if (!_isActive)
  285. {
  286. return true;
  287. }
  288. if (_isStopped)
  289. {
  290. return false;
  291. }
  292. NpadManager.Update();
  293. // Touchscreen
  294. bool hasTouch = false;
  295. // Get screen touch position
  296. if (!_enableMouse)
  297. {
  298. hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
  299. }
  300. if (!hasTouch)
  301. {
  302. TouchScreenManager.Update(false);
  303. }
  304. Device.Hid.DebugPad.Update();
  305. // TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
  306. MouseDriver.UpdatePosition();
  307. return true;
  308. }
  309. public void Execute()
  310. {
  311. _chrono.Restart();
  312. _isActive = true;
  313. InitializeWindow();
  314. Thread renderLoopThread = new(Render)
  315. {
  316. Name = "GUI.RenderLoop",
  317. };
  318. renderLoopThread.Start();
  319. Thread nvidiaStutterWorkaround = null;
  320. if (Renderer is OpenGLRenderer)
  321. {
  322. nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
  323. {
  324. Name = "GUI.NvidiaStutterWorkaround",
  325. };
  326. nvidiaStutterWorkaround.Start();
  327. }
  328. MainLoop();
  329. // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
  330. // We only need to wait for all commands submitted during the main gpu loop to be processed.
  331. _gpuDoneEvent.WaitOne();
  332. _gpuDoneEvent.Dispose();
  333. nvidiaStutterWorkaround?.Join();
  334. Exit();
  335. }
  336. public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
  337. {
  338. // SDL2 doesn't support input dialogs
  339. userText = "Ryujinx";
  340. return true;
  341. }
  342. public bool DisplayMessageDialog(string title, string message)
  343. {
  344. SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
  345. return true;
  346. }
  347. public bool DisplayMessageDialog(ControllerAppletUiArgs args)
  348. {
  349. string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
  350. string message = $"Application requests {playerCount} player(s) with:\n\n"
  351. + $"TYPES: {args.SupportedStyles}\n\n"
  352. + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n"
  353. + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "")
  354. + "Please reconfigure Input now and then press OK.";
  355. return DisplayMessageDialog("Controller Applet", message);
  356. }
  357. public IDynamicTextInputHandler CreateDynamicTextInputHandler()
  358. {
  359. return new HeadlessDynamicTextInputHandler();
  360. }
  361. public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
  362. {
  363. device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
  364. Exit();
  365. }
  366. public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
  367. {
  368. SDL_MessageBoxData data = new()
  369. {
  370. title = title,
  371. message = message,
  372. buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
  373. numbuttons = buttonsText.Length,
  374. window = WindowHandle,
  375. };
  376. for (int i = 0; i < buttonsText.Length; i++)
  377. {
  378. data.buttons[i] = new SDL_MessageBoxButtonData
  379. {
  380. buttonid = i,
  381. text = buttonsText[i],
  382. };
  383. }
  384. SDL_ShowMessageBox(ref data, out int _);
  385. return true;
  386. }
  387. public void Dispose()
  388. {
  389. Dispose(true);
  390. }
  391. protected virtual void Dispose(bool disposing)
  392. {
  393. if (disposing)
  394. {
  395. _isActive = false;
  396. TouchScreenManager?.Dispose();
  397. NpadManager.Dispose();
  398. SDL2Driver.Instance.UnregisterWindow(_windowId);
  399. SDL_DestroyWindow(WindowHandle);
  400. SDL2Driver.Instance.Dispose();
  401. }
  402. }
  403. }
  404. }