WindowBase.cs 19 KB

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