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