/ src / Ryujinx.Gtk3 / UI / RendererWidgetBase.cs
RendererWidgetBase.cs
  1  using Gdk;
  2  using Gtk;
  3  using Ryujinx.Common;
  4  using Ryujinx.Common.Configuration;
  5  using Ryujinx.Common.Logging;
  6  using Ryujinx.Common.Utilities;
  7  using Ryujinx.Graphics.GAL;
  8  using Ryujinx.Graphics.GAL.Multithreading;
  9  using Ryujinx.Graphics.Gpu;
 10  using Ryujinx.Input;
 11  using Ryujinx.Input.GTK3;
 12  using Ryujinx.Input.HLE;
 13  using Ryujinx.UI.Common.Configuration;
 14  using Ryujinx.UI.Common.Helper;
 15  using Ryujinx.UI.Widgets;
 16  using SkiaSharp;
 17  using System;
 18  using System.Diagnostics;
 19  using System.IO;
 20  using System.Runtime.InteropServices;
 21  using System.Threading;
 22  using System.Threading.Tasks;
 23  using Key = Ryujinx.Input.Key;
 24  using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter;
 25  using Switch = Ryujinx.HLE.Switch;
 26  
 27  namespace Ryujinx.UI
 28  {
 29      public abstract class RendererWidgetBase : DrawingArea
 30      {
 31          private const int SwitchPanelWidth = 1280;
 32          private const int SwitchPanelHeight = 720;
 33          private const int TargetFps = 60;
 34          private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
 35          private const float VolumeDelta = 0.05f;
 36  
 37          public ManualResetEvent WaitEvent { get; set; }
 38          public NpadManager NpadManager { get; }
 39          public TouchScreenManager TouchScreenManager { get; }
 40          public Switch Device { get; private set; }
 41          public IRenderer Renderer { get; private set; }
 42  
 43          public bool ScreenshotRequested { get; set; }
 44          protected int WindowWidth { get; private set; }
 45          protected int WindowHeight { get; private set; }
 46  
 47          public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
 48  
 49          private bool _isActive;
 50          private bool _isStopped;
 51  
 52          private bool _toggleFullscreen;
 53          private bool _toggleDockedMode;
 54  
 55          private readonly long _ticksPerFrame;
 56  
 57          private long _ticks = 0;
 58          private float _newVolume;
 59  
 60          private readonly Stopwatch _chrono;
 61  
 62          private KeyboardHotkeyState _prevHotkeyState;
 63  
 64          private readonly ManualResetEvent _exitEvent;
 65          private readonly ManualResetEvent _gpuDoneEvent;
 66  
 67          private readonly CancellationTokenSource _gpuCancellationTokenSource;
 68  
 69          // Hide Cursor
 70          const int CursorHideIdleTime = 5; // seconds
 71          private static readonly Cursor _invisibleCursor = new(Display.Default, CursorType.BlankCursor);
 72          private long _lastCursorMoveTime;
 73          private HideCursorMode _hideCursorMode;
 74          private readonly InputManager _inputManager;
 75          private readonly IKeyboard _keyboardInterface;
 76          private readonly GraphicsDebugLevel _glLogLevel;
 77          private string _gpuBackendName;
 78          private string _gpuDriverName;
 79          private bool _isMouseInClient;
 80  
 81          public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel)
 82          {
 83              var mouseDriver = new GTK3MouseDriver(this);
 84  
 85              _inputManager = inputManager;
 86              _inputManager.SetMouseDriver(mouseDriver);
 87              NpadManager = _inputManager.CreateNpadManager();
 88              TouchScreenManager = _inputManager.CreateTouchScreenManager();
 89              _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
 90  
 91              WaitEvent = new ManualResetEvent(false);
 92  
 93              _glLogLevel = glLogLevel;
 94  
 95              Destroyed += Renderer_Destroyed;
 96  
 97              _chrono = new Stopwatch();
 98  
 99              _ticksPerFrame = Stopwatch.Frequency / TargetFps;
100  
101              AddEvents((int)(EventMask.ButtonPressMask
102                            | EventMask.ButtonReleaseMask
103                            | EventMask.PointerMotionMask
104                            | EventMask.ScrollMask
105                            | EventMask.EnterNotifyMask
106                            | EventMask.LeaveNotifyMask
107                            | EventMask.KeyPressMask
108                            | EventMask.KeyReleaseMask));
109  
110              _exitEvent = new ManualResetEvent(false);
111              _gpuDoneEvent = new ManualResetEvent(false);
112  
113              _gpuCancellationTokenSource = new CancellationTokenSource();
114  
115              _hideCursorMode = ConfigurationState.Instance.HideCursor;
116              _lastCursorMoveTime = Stopwatch.GetTimestamp();
117  
118              ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged;
119              ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing;
120              ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
121              ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
122          }
123  
124          private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
125          {
126              Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
127              Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
128          }
129  
130          private void UpdateScalingFilter(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.ScalingFilter> e)
131          {
132              Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
133              Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
134          }
135  
136          public abstract void InitializeRenderer();
137  
138          public abstract void SwapBuffers();
139  
140          protected abstract string GetGpuBackendName();
141  
142          private string GetGpuDriverName()
143          {
144              return Renderer.GetHardwareInfo().GpuDriver;
145          }
146  
147          private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state)
148          {
149              Application.Invoke(delegate
150              {
151                  _hideCursorMode = state.NewValue;
152  
153                  switch (_hideCursorMode)
154                  {
155                      case HideCursorMode.Never:
156                          Window.Cursor = null;
157                          break;
158                      case HideCursorMode.OnIdle:
159                          _lastCursorMoveTime = Stopwatch.GetTimestamp();
160                          break;
161                      case HideCursorMode.Always:
162                          Window.Cursor = _invisibleCursor;
163                          break;
164                      default:
165                          throw new ArgumentOutOfRangeException(nameof(state));
166                  }
167              });
168          }
169  
170          private void Renderer_Destroyed(object sender, EventArgs e)
171          {
172              ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged;
173              ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing;
174              ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
175              ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
176  
177              NpadManager.Dispose();
178              Dispose();
179          }
180  
181          private void UpdateAnriAliasing(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.AntiAliasing> e)
182          {
183              Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
184          }
185  
186          protected override bool OnMotionNotifyEvent(EventMotion evnt)
187          {
188              if (_hideCursorMode == HideCursorMode.OnIdle)
189              {
190                  _lastCursorMoveTime = Stopwatch.GetTimestamp();
191              }
192  
193              if (ConfigurationState.Instance.Hid.EnableMouse)
194              {
195                  Window.Cursor = _invisibleCursor;
196              }
197  
198              _isMouseInClient = true;
199  
200              return false;
201          }
202  
203          protected override bool OnEnterNotifyEvent(EventCrossing evnt)
204          {
205              Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null;
206  
207              _isMouseInClient = true;
208  
209              return base.OnEnterNotifyEvent(evnt);
210          }
211  
212          protected override bool OnLeaveNotifyEvent(EventCrossing evnt)
213          {
214              Window.Cursor = null;
215  
216              _isMouseInClient = false;
217  
218              return base.OnLeaveNotifyEvent(evnt);
219          }
220  
221          protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight)
222          {
223              Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
224  
225              // If the monitor is at least 1080p, use the Switch panel size as minimal size.
226              if (monitor.Geometry.Height >= 1080)
227              {
228                  minimumHeight = SwitchPanelHeight;
229              }
230              // Otherwise, we default minimal size to 480p 16:9.
231              else
232              {
233                  minimumHeight = 480;
234              }
235  
236              naturalHeight = minimumHeight;
237          }
238  
239          protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth)
240          {
241              Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
242  
243              // If the monitor is at least 1080p, use the Switch panel size as minimal size.
244              if (monitor.Geometry.Height >= 1080)
245              {
246                  minimumWidth = SwitchPanelWidth;
247              }
248              // Otherwise, we default minimal size to 480p 16:9.
249              else
250              {
251                  minimumWidth = 854;
252              }
253  
254              naturalWidth = minimumWidth;
255          }
256  
257          protected override bool OnConfigureEvent(EventConfigure evnt)
258          {
259              bool result = base.OnConfigureEvent(evnt);
260  
261              Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
262  
263              WindowWidth = evnt.Width * monitor.ScaleFactor;
264              WindowHeight = evnt.Height * monitor.ScaleFactor;
265  
266              Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
267  
268              return result;
269          }
270  
271          private void HandleScreenState(KeyboardStateSnapshot keyboard)
272          {
273              bool toggleFullscreen = keyboard.IsPressed(Key.F11)
274                                  || ((keyboard.IsPressed(Key.AltLeft)
275                                  || keyboard.IsPressed(Key.AltRight))
276                                  && keyboard.IsPressed(Key.Enter))
277                                  || keyboard.IsPressed(Key.Escape);
278  
279              bool fullScreenToggled = ParentWindow.State.HasFlag(WindowState.Fullscreen);
280  
281              if (toggleFullscreen != _toggleFullscreen)
282              {
283                  if (toggleFullscreen)
284                  {
285                      if (fullScreenToggled)
286                      {
287                          ParentWindow.Unfullscreen();
288                          (Toplevel as MainWindow)?.ToggleExtraWidgets(true);
289                      }
290                      else
291                      {
292                          if (keyboard.IsPressed(Key.Escape))
293                          {
294                              if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
295                              {
296                                  Exit();
297                              }
298                          }
299                          else
300                          {
301                              ParentWindow.Fullscreen();
302                              (Toplevel as MainWindow)?.ToggleExtraWidgets(false);
303                          }
304                      }
305                  }
306              }
307  
308              _toggleFullscreen = toggleFullscreen;
309  
310              bool toggleDockedMode = keyboard.IsPressed(Key.F9);
311  
312              if (toggleDockedMode != _toggleDockedMode)
313              {
314                  if (toggleDockedMode)
315                  {
316                      ConfigurationState.Instance.System.EnableDockedMode.Value =
317                          !ConfigurationState.Instance.System.EnableDockedMode.Value;
318                  }
319              }
320  
321              _toggleDockedMode = toggleDockedMode;
322  
323              if (_isMouseInClient)
324              {
325                  if (ConfigurationState.Instance.Hid.EnableMouse.Value)
326                  {
327                      Window.Cursor = _invisibleCursor;
328                  }
329                  else
330                  {
331                      switch (_hideCursorMode)
332                      {
333                          case HideCursorMode.OnIdle:
334                              long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
335                              Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
336                              break;
337                          case HideCursorMode.Always:
338                              Window.Cursor = _invisibleCursor;
339                              break;
340                          case HideCursorMode.Never:
341                              Window.Cursor = null;
342                              break;
343                      }
344                  }
345              }
346          }
347  
348          public void Initialize(Switch device)
349          {
350              Device = device;
351  
352              IRenderer renderer = Device.Gpu.Renderer;
353  
354              if (renderer is ThreadedRenderer tr)
355              {
356                  renderer = tr.BaseRenderer;
357              }
358  
359              Renderer = renderer;
360              Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
361  
362              if (Renderer != null)
363              {
364                  Renderer.ScreenCaptured += Renderer_ScreenCaptured;
365              }
366  
367              NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
368              TouchScreenManager.Initialize(device);
369          }
370  
371          private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
372          {
373              if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
374              {
375                  Task.Run(() =>
376                  {
377                      lock (this)
378                      {
379                          string applicationName = Device.Processes.ActiveApplication.Name;
380                          string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName);
381                          DateTime currentTime = DateTime.Now;
382  
383                          string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
384  
385                          string directory = AppDataManager.Mode switch
386                          {
387                              AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => System.IO.Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
388                              _ => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"),
389                          };
390  
391                          string path = System.IO.Path.Combine(directory, filename);
392  
393                          try
394                          {
395                              Directory.CreateDirectory(directory);
396                          }
397                          catch (Exception ex)
398                          {
399                              Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
400  
401                              return;
402                          }
403  
404                          var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
405                          using var image = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
406  
407                          Marshal.Copy(e.Data, 0, image.GetPixels(), e.Data.Length);
408                          using var surface = SKSurface.Create(image.Info);
409                          var canvas = surface.Canvas;
410  
411                          if (e.FlipX || e.FlipY)
412                          {
413                              canvas.Clear(SKColors.Transparent);
414  
415                              float scaleX = e.FlipX ? -1 : 1;
416                              float scaleY = e.FlipY ? -1 : 1;
417  
418                              var matrix = SKMatrix.CreateScale(scaleX, scaleY, image.Width / 2f, image.Height / 2f);
419  
420                              canvas.SetMatrix(matrix);
421                          }
422                          canvas.DrawBitmap(image, new SKPoint());
423  
424                          surface.Flush();
425                          using var snapshot = surface.Snapshot();
426                          using var encoded = snapshot.Encode(SKEncodedImageFormat.Png, 80);
427                          using var file = File.OpenWrite(path);
428                          encoded.SaveTo(file);
429  
430                          image.Dispose();
431  
432                          Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
433                      }
434                  });
435              }
436              else
437              {
438                  Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
439              }
440          }
441  
442          public void Render()
443          {
444              Gtk.Window parent = Toplevel as Gtk.Window;
445              parent.Present();
446  
447              InitializeRenderer();
448  
449              Device.Gpu.Renderer.Initialize(_glLogLevel);
450  
451              Renderer.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
452              Renderer.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
453              Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
454  
455              _gpuBackendName = GetGpuBackendName();
456              _gpuDriverName = GetGpuDriverName();
457  
458              Device.Gpu.Renderer.RunLoop(() =>
459              {
460                  Device.Gpu.SetGpuThread();
461                  Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
462  
463                  Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
464  
465                  (Toplevel as MainWindow)?.ActivatePauseMenu();
466  
467                  while (_isActive)
468                  {
469                      if (_isStopped)
470                      {
471                          return;
472                      }
473  
474                      _ticks += _chrono.ElapsedTicks;
475  
476                      _chrono.Restart();
477  
478                      if (Device.WaitFifo())
479                      {
480                          Device.Statistics.RecordFifoStart();
481                          Device.ProcessFrame();
482                          Device.Statistics.RecordFifoEnd();
483                      }
484  
485                      while (Device.ConsumeFrameAvailable())
486                      {
487                          Device.PresentFrame(SwapBuffers);
488                      }
489  
490                      if (_ticks >= _ticksPerFrame)
491                      {
492                          string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
493                          float scale = GraphicsConfig.ResScale;
494                          if (scale != 1)
495                          {
496                              dockedMode += $" ({scale}x)";
497                          }
498  
499                          StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
500                              Device.EnableDeviceVsync,
501                              Device.GetVolume(),
502                              _gpuBackendName,
503                              dockedMode,
504                              ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
505                              $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
506                              $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
507                              $"GPU: {_gpuDriverName}"));
508  
509                          _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
510                      }
511                  }
512  
513                  // Make sure all commands in the run loop are fully executed before leaving the loop.
514                  if (Device.Gpu.Renderer is ThreadedRenderer threaded)
515                  {
516                      threaded.FlushThreadedCommands();
517                  }
518  
519                  _gpuDoneEvent.Set();
520              });
521          }
522  
523          public void Start()
524          {
525              _chrono.Restart();
526  
527              _isActive = true;
528  
529              Gtk.Window parent = Toplevel as Gtk.Window;
530  
531              Application.Invoke(delegate
532              {
533                  parent.Present();
534  
535                  var activeProcess = Device.Processes.ActiveApplication;
536  
537                  parent.Title = TitleHelper.ActiveApplicationTitle(activeProcess, Program.Version);
538              });
539  
540              Thread renderLoopThread = new(Render)
541              {
542                  Name = "GUI.RenderLoop",
543              };
544              renderLoopThread.Start();
545  
546              Thread nvidiaStutterWorkaround = null;
547              if (Renderer is Graphics.OpenGL.OpenGLRenderer)
548              {
549                  nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
550                  {
551                      Name = "GUI.NvidiaStutterWorkaround",
552                  };
553                  nvidiaStutterWorkaround.Start();
554              }
555  
556              MainLoop();
557  
558              // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
559              // We only need to wait for all commands submitted during the main gpu loop to be processed.
560              _gpuDoneEvent.WaitOne();
561              _gpuDoneEvent.Dispose();
562              nvidiaStutterWorkaround?.Join();
563  
564              Exit();
565          }
566  
567          public void Exit()
568          {
569              TouchScreenManager?.Dispose();
570              NpadManager?.Dispose();
571  
572              if (_isStopped)
573              {
574                  return;
575              }
576  
577              _gpuCancellationTokenSource.Cancel();
578  
579              _isStopped = true;
580  
581              if (_isActive)
582              {
583                  _isActive = false;
584  
585                  _exitEvent.WaitOne();
586                  _exitEvent.Dispose();
587              }
588          }
589  
590          private void NvidiaStutterWorkaround()
591          {
592              while (_isActive)
593              {
594                  // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
595                  // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
596                  // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
597                  // This creates a new thread every second or so.
598                  // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
599                  // This is a little over budget on a frame time of 16ms, so creates a large stutter.
600                  // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
601  
602                  // TODO: This should be removed when the issue with the GateThread is resolved.
603  
604                  ThreadPool.QueueUserWorkItem((state) => { });
605                  Thread.Sleep(300);
606              }
607          }
608  
609          public void MainLoop()
610          {
611              while (_isActive)
612              {
613                  UpdateFrame();
614  
615                  // Polling becomes expensive if it's not slept
616                  Thread.Sleep(1);
617              }
618  
619              _exitEvent.Set();
620          }
621  
622          private bool UpdateFrame()
623          {
624              if (!_isActive)
625              {
626                  return true;
627              }
628  
629              if (_isStopped)
630              {
631                  return false;
632              }
633  
634              if ((Toplevel as MainWindow).IsFocused)
635              {
636                  Application.Invoke(delegate
637                  {
638                      KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
639  
640                      HandleScreenState(keyboard);
641  
642                      if (keyboard.IsPressed(Key.Delete))
643                      {
644                          if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
645                          {
646                              Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
647                          }
648                      }
649                  });
650              }
651  
652              NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
653  
654              if ((Toplevel as MainWindow).IsFocused)
655              {
656                  KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
657  
658                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
659                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
660                  {
661                      Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
662                  }
663  
664                  if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) &&
665                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested)
666                  {
667                      ScreenshotRequested = false;
668  
669                      Renderer.Screenshot();
670                  }
671  
672                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI) &&
673                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI))
674                  {
675                      (Toplevel as MainWindow).ToggleExtraWidgets(true);
676                  }
677  
678                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) &&
679                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause))
680                  {
681                      (Toplevel as MainWindow)?.TogglePause();
682                  }
683  
684                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) &&
685                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute))
686                  {
687                      if (Device.IsAudioMuted())
688                      {
689                          Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
690                      }
691                      else
692                      {
693                          Device.SetVolume(0);
694                      }
695                  }
696  
697                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) &&
698                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp))
699                  {
700                      GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
701                  }
702  
703                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) &&
704                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown))
705                  {
706                      GraphicsConfig.ResScale =
707                      (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
708                  }
709  
710                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp) &&
711                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp))
712                  {
713                      _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
714                      Device.SetVolume(_newVolume);
715                  }
716  
717                  if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown) &&
718                      !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown))
719                  {
720                      _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
721                      Device.SetVolume(_newVolume);
722                  }
723  
724                  _prevHotkeyState = currentHotkeyState;
725              }
726  
727              // Touchscreen
728              bool hasTouch = false;
729  
730              // Get screen touch position
731              if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse)
732              {
733                  hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
734              }
735  
736              if (!hasTouch)
737              {
738                  TouchScreenManager.Update(false);
739              }
740  
741              Device.Hid.DebugPad.Update();
742  
743              return true;
744          }
745  
746          [Flags]
747          private enum KeyboardHotkeyState
748          {
749              None = 0,
750              ToggleVSync = 1 << 0,
751              Screenshot = 1 << 1,
752              ShowUI = 1 << 2,
753              Pause = 1 << 3,
754              ToggleMute = 1 << 4,
755              ResScaleUp = 1 << 5,
756              ResScaleDown = 1 << 6,
757              VolumeUp = 1 << 7,
758              VolumeDown = 1 << 8,
759          }
760  
761          private KeyboardHotkeyState GetHotkeyState()
762          {
763              KeyboardHotkeyState state = KeyboardHotkeyState.None;
764  
765              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
766              {
767                  state |= KeyboardHotkeyState.ToggleVSync;
768              }
769  
770              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
771              {
772                  state |= KeyboardHotkeyState.Screenshot;
773              }
774  
775              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI))
776              {
777                  state |= KeyboardHotkeyState.ShowUI;
778              }
779  
780              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
781              {
782                  state |= KeyboardHotkeyState.Pause;
783              }
784  
785              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
786              {
787                  state |= KeyboardHotkeyState.ToggleMute;
788              }
789  
790              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
791              {
792                  state |= KeyboardHotkeyState.ResScaleUp;
793              }
794  
795              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
796              {
797                  state |= KeyboardHotkeyState.ResScaleDown;
798              }
799  
800              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
801              {
802                  state |= KeyboardHotkeyState.VolumeUp;
803              }
804  
805              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
806              {
807                  state |= KeyboardHotkeyState.VolumeDown;
808              }
809  
810              return state;
811          }
812      }
813  }