/ src / Ryujinx / AppHost.cs
AppHost.cs
   1  using Avalonia;
   2  using Avalonia.Controls;
   3  using Avalonia.Controls.ApplicationLifetimes;
   4  using Avalonia.Input;
   5  using Avalonia.Threading;
   6  using LibHac.Tools.FsSystem;
   7  using Ryujinx.Audio.Backends.Dummy;
   8  using Ryujinx.Audio.Backends.OpenAL;
   9  using Ryujinx.Audio.Backends.SDL2;
  10  using Ryujinx.Audio.Backends.SoundIo;
  11  using Ryujinx.Audio.Integration;
  12  using Ryujinx.Ava.Common;
  13  using Ryujinx.Ava.Common.Locale;
  14  using Ryujinx.Ava.Input;
  15  using Ryujinx.Ava.UI.Helpers;
  16  using Ryujinx.Ava.UI.Models;
  17  using Ryujinx.Ava.UI.Renderer;
  18  using Ryujinx.Ava.UI.ViewModels;
  19  using Ryujinx.Ava.UI.Windows;
  20  using Ryujinx.Common;
  21  using Ryujinx.Common.Configuration;
  22  using Ryujinx.Common.Configuration.Multiplayer;
  23  using Ryujinx.Common.Logging;
  24  using Ryujinx.Common.SystemInterop;
  25  using Ryujinx.Common.Utilities;
  26  using Ryujinx.Graphics.GAL;
  27  using Ryujinx.Graphics.GAL.Multithreading;
  28  using Ryujinx.Graphics.Gpu;
  29  using Ryujinx.Graphics.OpenGL;
  30  using Ryujinx.Graphics.Vulkan;
  31  using Ryujinx.HLE;
  32  using Ryujinx.HLE.FileSystem;
  33  using Ryujinx.HLE.HOS;
  34  using Ryujinx.HLE.HOS.Services.Account.Acc;
  35  using Ryujinx.HLE.HOS.SystemState;
  36  using Ryujinx.Input;
  37  using Ryujinx.Input.HLE;
  38  using Ryujinx.UI.App.Common;
  39  using Ryujinx.UI.Common;
  40  using Ryujinx.UI.Common.Configuration;
  41  using Ryujinx.UI.Common.Helper;
  42  using Silk.NET.Vulkan;
  43  using SkiaSharp;
  44  using SPB.Graphics.Vulkan;
  45  using System;
  46  using System.Collections.Generic;
  47  using System.Diagnostics;
  48  using System.IO;
  49  using System.Runtime.InteropServices;
  50  using System.Threading;
  51  using System.Threading.Tasks;
  52  using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
  53  using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
  54  using InputManager = Ryujinx.Input.HLE.InputManager;
  55  using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
  56  using Key = Ryujinx.Input.Key;
  57  using MouseButton = Ryujinx.Input.MouseButton;
  58  using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
  59  using Size = Avalonia.Size;
  60  using Switch = Ryujinx.HLE.Switch;
  61  
  62  namespace Ryujinx.Ava
  63  {
  64      internal class AppHost
  65      {
  66          private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
  67          private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
  68          private const int TargetFps = 60;
  69          private const float VolumeDelta = 0.05f;
  70  
  71          private static readonly Cursor _invisibleCursor = new(StandardCursorType.None);
  72          private readonly IntPtr _invisibleCursorWin;
  73          private readonly IntPtr _defaultCursorWin;
  74  
  75          private readonly long _ticksPerFrame;
  76          private readonly Stopwatch _chrono;
  77          private long _ticks;
  78  
  79          private readonly AccountManager _accountManager;
  80          private readonly UserChannelPersistence _userChannelPersistence;
  81          private readonly InputManager _inputManager;
  82  
  83          private readonly MainWindowViewModel _viewModel;
  84          private readonly IKeyboard _keyboardInterface;
  85          private readonly TopLevel _topLevel;
  86          public RendererHost RendererHost;
  87  
  88          private readonly GraphicsDebugLevel _glLogLevel;
  89          private float _newVolume;
  90          private KeyboardHotkeyState _prevHotkeyState;
  91  
  92          private long _lastCursorMoveTime;
  93          private bool _isCursorInRenderer = true;
  94          private bool _ignoreCursorState = false;
  95  
  96          private enum CursorStates
  97          {
  98              CursorIsHidden,
  99              CursorIsVisible,
 100              ForceChangeCursor
 101          };
 102  
 103          private CursorStates _cursorState = !ConfigurationState.Instance.Hid.EnableMouse.Value ?
 104              CursorStates.CursorIsVisible : CursorStates.CursorIsHidden;
 105  
 106          private bool _isStopped;
 107          private bool _isActive;
 108          private bool _renderingStarted;
 109  
 110          private readonly ManualResetEvent _gpuDoneEvent;
 111  
 112          private IRenderer _renderer;
 113          private readonly Thread _renderingThread;
 114          private readonly CancellationTokenSource _gpuCancellationTokenSource;
 115          private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
 116  
 117          private bool _dialogShown;
 118          private readonly bool _isFirmwareTitle;
 119  
 120          private readonly object _lockObject = new();
 121  
 122          public event EventHandler AppExit;
 123          public event EventHandler<StatusInitEventArgs> StatusInitEvent;
 124          public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
 125  
 126          public VirtualFileSystem VirtualFileSystem { get; }
 127          public ContentManager ContentManager { get; }
 128          public NpadManager NpadManager { get; }
 129          public TouchScreenManager TouchScreenManager { get; }
 130          public Switch Device { get; set; }
 131  
 132          public int Width { get; private set; }
 133          public int Height { get; private set; }
 134          public string ApplicationPath { get; private set; }
 135          public ulong ApplicationId { get; private set; }
 136          public bool ScreenshotRequested { get; set; }
 137  
 138          public AppHost(
 139              RendererHost renderer,
 140              InputManager inputManager,
 141              string applicationPath,
 142              ulong applicationId,
 143              VirtualFileSystem virtualFileSystem,
 144              ContentManager contentManager,
 145              AccountManager accountManager,
 146              UserChannelPersistence userChannelPersistence,
 147              MainWindowViewModel viewmodel,
 148              TopLevel topLevel)
 149          {
 150              _viewModel = viewmodel;
 151              _inputManager = inputManager;
 152              _accountManager = accountManager;
 153              _userChannelPersistence = userChannelPersistence;
 154              _renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" };
 155              _lastCursorMoveTime = Stopwatch.GetTimestamp();
 156              _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
 157              _topLevel = topLevel;
 158  
 159              _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
 160  
 161              _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
 162  
 163              NpadManager = _inputManager.CreateNpadManager();
 164              TouchScreenManager = _inputManager.CreateTouchScreenManager();
 165              ApplicationPath = applicationPath;
 166              ApplicationId = applicationId;
 167              VirtualFileSystem = virtualFileSystem;
 168              ContentManager = contentManager;
 169  
 170              RendererHost = renderer;
 171  
 172              _chrono = new Stopwatch();
 173              _ticksPerFrame = Stopwatch.Frequency / TargetFps;
 174  
 175              if (ApplicationPath.StartsWith("@SystemContent"))
 176              {
 177                  ApplicationPath = VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
 178  
 179                  _isFirmwareTitle = true;
 180              }
 181  
 182              ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed;
 183  
 184              _topLevel.PointerMoved += TopLevel_PointerEnteredOrMoved;
 185              _topLevel.PointerEntered += TopLevel_PointerEnteredOrMoved;
 186              _topLevel.PointerExited += TopLevel_PointerExited;
 187  
 188              if (OperatingSystem.IsWindows())
 189              {
 190                  _invisibleCursorWin = CreateEmptyCursor();
 191                  _defaultCursorWin = CreateArrowCursor();
 192              }
 193  
 194              ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
 195              ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
 196              ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
 197              ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
 198              ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
 199              ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
 200              ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAntiAliasing;
 201              ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
 202              ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
 203              ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
 204  
 205              ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
 206              ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
 207              ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
 208  
 209              _gpuCancellationTokenSource = new CancellationTokenSource();
 210              _gpuDoneEvent = new ManualResetEvent(false);
 211          }
 212  
 213          private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e)
 214          {
 215              if (!_viewModel.IsActive)
 216              {
 217                  _isCursorInRenderer = false;
 218                  _ignoreCursorState = false;
 219                  return;
 220              }
 221  
 222              if (sender is MainWindow window)
 223              {
 224                  if (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle)
 225                  {
 226                      _lastCursorMoveTime = Stopwatch.GetTimestamp();
 227                  }
 228  
 229                  var point = e.GetCurrentPoint(window).Position;
 230                  var bounds = RendererHost.EmbeddedWindow.Bounds;
 231                  var windowYOffset = bounds.Y + window.MenuBarHeight;
 232                  var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1;
 233  
 234                  if (!_viewModel.ShowMenuAndStatusBar)
 235                  {
 236                      windowYOffset -= window.MenuBarHeight;
 237                      windowYLimit += window.StatusBarHeight + 1;
 238                  }
 239  
 240                  _isCursorInRenderer = point.X >= bounds.X &&
 241                      Math.Ceiling(point.X) <= (int)window.Bounds.Width &&
 242                      point.Y >= windowYOffset &&
 243                      point.Y <= windowYLimit &&
 244                      !_viewModel.IsSubMenuOpen;
 245  
 246                  _ignoreCursorState = false;
 247              }
 248          }
 249  
 250          private void TopLevel_PointerExited(object sender, PointerEventArgs e)
 251          {
 252              _isCursorInRenderer = false;
 253  
 254              if (sender is MainWindow window)
 255              {
 256                  var point = e.GetCurrentPoint(window).Position;
 257                  var bounds = RendererHost.EmbeddedWindow.Bounds;
 258                  var windowYOffset = bounds.Y + window.MenuBarHeight;
 259                  var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1;
 260  
 261                  if (!_viewModel.ShowMenuAndStatusBar)
 262                  {
 263                      windowYOffset -= window.MenuBarHeight;
 264                      windowYLimit += window.StatusBarHeight + 1;
 265                  }
 266  
 267                  _ignoreCursorState = (point.X == bounds.X ||
 268                      Math.Ceiling(point.X) == (int)window.Bounds.Width) &&
 269                      point.Y >= windowYOffset &&
 270                      point.Y <= windowYLimit;
 271              }
 272  
 273              _cursorState = CursorStates.ForceChangeCursor;
 274          }
 275  
 276          private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
 277          {
 278              _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
 279              _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
 280          }
 281  
 282          private void UpdateScalingFilter(object sender, ReactiveEventArgs<ScalingFilter> e)
 283          {
 284              _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
 285              _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
 286          }
 287  
 288          private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs<bool> e)
 289          {
 290              _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
 291          }
 292  
 293          private void ShowCursor()
 294          {
 295              Dispatcher.UIThread.Post(() =>
 296              {
 297                  _viewModel.Cursor = Cursor.Default;
 298  
 299                  if (OperatingSystem.IsWindows())
 300                  {
 301                      if (_cursorState != CursorStates.CursorIsHidden && !_ignoreCursorState)
 302                      {
 303                          SetCursor(_defaultCursorWin);
 304                      }
 305                  }
 306              });
 307  
 308              _cursorState = CursorStates.CursorIsVisible;
 309          }
 310  
 311          private void HideCursor()
 312          {
 313              Dispatcher.UIThread.Post(() =>
 314              {
 315                  _viewModel.Cursor = _invisibleCursor;
 316  
 317                  if (OperatingSystem.IsWindows())
 318                  {
 319                      SetCursor(_invisibleCursorWin);
 320                  }
 321              });
 322  
 323              _cursorState = CursorStates.CursorIsHidden;
 324          }
 325  
 326          private void SetRendererWindowSize(Size size)
 327          {
 328              if (_renderer != null)
 329              {
 330                  double scale = _topLevel.RenderScaling;
 331  
 332                  _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
 333              }
 334          }
 335  
 336          private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
 337          {
 338              if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
 339              {
 340                  Task.Run(() =>
 341                  {
 342                      lock (_lockObject)
 343                      {
 344                          string applicationName = Device.Processes.ActiveApplication.Name;
 345                          string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName);
 346                          DateTime currentTime = DateTime.Now;
 347  
 348                          string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
 349  
 350                          string directory = AppDataManager.Mode switch
 351                          {
 352                              AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
 353                              _ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"),
 354                          };
 355  
 356                          string path = Path.Combine(directory, filename);
 357  
 358                          try
 359                          {
 360                              Directory.CreateDirectory(directory);
 361                          }
 362                          catch (Exception ex)
 363                          {
 364                              Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
 365  
 366                              return;
 367                          }
 368  
 369                          var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
 370                          using SKBitmap bitmap = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
 371  
 372                          Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length);
 373  
 374                          using SKBitmap bitmapToSave = new SKBitmap(bitmap.Width, bitmap.Height);
 375                          using SKCanvas canvas = new SKCanvas(bitmapToSave);
 376  
 377                          canvas.Clear(SKColors.Black);
 378  
 379                          float scaleX = e.FlipX ? -1 : 1;
 380                          float scaleY = e.FlipY ? -1 : 1;
 381  
 382                          var matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f);
 383  
 384                          canvas.SetMatrix(matrix);
 385                          canvas.DrawBitmap(bitmap, SKPoint.Empty);
 386  
 387                          SaveBitmapAsPng(bitmapToSave, path);
 388  
 389                          Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
 390                      }
 391                  });
 392              }
 393              else
 394              {
 395                  Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
 396              }
 397          }
 398  
 399          private void SaveBitmapAsPng(SKBitmap bitmap, string path)
 400          {
 401              using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100);
 402              using var stream = File.OpenWrite(path);
 403  
 404              data.SaveTo(stream);
 405          }
 406  
 407          public void Start()
 408          {
 409              if (OperatingSystem.IsWindows())
 410              {
 411                  _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
 412              }
 413  
 414              DisplaySleep.Prevent();
 415  
 416              NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
 417              TouchScreenManager.Initialize(Device);
 418  
 419              _viewModel.IsGameRunning = true;
 420  
 421              Dispatcher.UIThread.InvokeAsync(() =>
 422              {
 423                  _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version);
 424              });
 425  
 426              _viewModel.SetUiProgressHandlers(Device);
 427  
 428              RendererHost.BoundsChanged += Window_BoundsChanged;
 429  
 430              _isActive = true;
 431  
 432              _renderingThread.Start();
 433  
 434              _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
 435  
 436              MainLoop();
 437  
 438              Exit();
 439          }
 440  
 441          private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
 442          {
 443              if (Device != null)
 444              {
 445                  Device.Configuration.IgnoreMissingServices = args.NewValue;
 446              }
 447          }
 448  
 449          private void UpdateAspectRatioState(object sender, ReactiveEventArgs<AspectRatio> args)
 450          {
 451              if (Device != null)
 452              {
 453                  Device.Configuration.AspectRatio = args.NewValue;
 454              }
 455          }
 456  
 457          private void UpdateAntiAliasing(object sender, ReactiveEventArgs<AntiAliasing> e)
 458          {
 459              _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
 460          }
 461  
 462          private void UpdateDockedModeState(object sender, ReactiveEventArgs<bool> e)
 463          {
 464              Device?.System.ChangeDockedModeState(e.NewValue);
 465          }
 466  
 467          private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
 468          {
 469              Device?.SetVolume(e.NewValue);
 470  
 471              Dispatcher.UIThread.Post(() =>
 472              {
 473                  _viewModel.Volume = e.NewValue;
 474              });
 475          }
 476  
 477          private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs<bool> e)
 478          {
 479              Device.Configuration.EnableInternetAccess = e.NewValue;
 480          }
 481  
 482          private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
 483          {
 484              Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
 485          }
 486  
 487          private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs<MultiplayerMode> e)
 488          {
 489              Device.Configuration.MultiplayerMode = e.NewValue;
 490          }
 491  
 492          public void ToggleVSync()
 493          {
 494              Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
 495              _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
 496          }
 497  
 498          public void Stop()
 499          {
 500              _isActive = false;
 501          }
 502  
 503          private void Exit()
 504          {
 505              (_keyboardInterface as AvaloniaKeyboard)?.Clear();
 506  
 507              if (_isStopped)
 508              {
 509                  return;
 510              }
 511  
 512              _isStopped = true;
 513              _isActive = false;
 514          }
 515  
 516          public void DisposeContext()
 517          {
 518              Dispose();
 519  
 520              _isActive = false;
 521  
 522              // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
 523              // We only need to wait for all commands submitted during the main gpu loop to be processed.
 524              _gpuDoneEvent.WaitOne();
 525              _gpuDoneEvent.Dispose();
 526  
 527              DisplaySleep.Restore();
 528  
 529              NpadManager.Dispose();
 530              TouchScreenManager.Dispose();
 531              Device.Dispose();
 532  
 533              DisposeGpu();
 534  
 535              AppExit?.Invoke(this, EventArgs.Empty);
 536          }
 537  
 538          private void Dispose()
 539          {
 540              if (Device.Processes != null)
 541              {
 542                  MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
 543              }
 544  
 545              ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
 546              ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
 547              ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
 548              ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
 549              ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
 550              ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
 551              ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing;
 552              ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough;
 553  
 554              _topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved;
 555              _topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved;
 556              _topLevel.PointerExited -= TopLevel_PointerExited;
 557  
 558              _gpuCancellationTokenSource.Cancel();
 559              _gpuCancellationTokenSource.Dispose();
 560  
 561              _chrono.Stop();
 562          }
 563  
 564          public void DisposeGpu()
 565          {
 566              if (OperatingSystem.IsWindows())
 567              {
 568                  _windowsMultimediaTimerResolution?.Dispose();
 569                  _windowsMultimediaTimerResolution = null;
 570              }
 571  
 572              if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow)
 573              {
 574                  // Try to bind the OpenGL context before calling the shutdown event.
 575                  openGlWindow.MakeCurrent(false, false);
 576  
 577                  Device.DisposeGpu();
 578  
 579                  // Unbind context and destroy everything.
 580                  openGlWindow.MakeCurrent(true, false);
 581              }
 582              else
 583              {
 584                  Device.DisposeGpu();
 585              }
 586          }
 587  
 588          private void HideCursorState_Changed(object sender, ReactiveEventArgs<HideCursorMode> state)
 589          {
 590              if (state.NewValue == HideCursorMode.OnIdle)
 591              {
 592                  _lastCursorMoveTime = Stopwatch.GetTimestamp();
 593              }
 594  
 595              _cursorState = CursorStates.ForceChangeCursor;
 596          }
 597  
 598          public async Task<bool> LoadGuestApplication()
 599          {
 600              InitializeSwitchInstance();
 601              MainWindow.UpdateGraphicsConfig();
 602  
 603              SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
 604  
 605              if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
 606              {
 607                  if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
 608                  {
 609                      {
 610                          if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
 611                          {
 612                              if (userError == UserError.NoFirmware)
 613                              {
 614                                  UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
 615                                      LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
 616                                      LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
 617                                      LocaleManager.Instance[LocaleKeys.InputDialogYes],
 618                                      LocaleManager.Instance[LocaleKeys.InputDialogNo],
 619                                      "");
 620  
 621                                  if (result != UserResult.Yes)
 622                                  {
 623                                      await UserErrorDialog.ShowUserErrorDialog(userError);
 624                                      Device.Dispose();
 625  
 626                                      return false;
 627                                  }
 628                              }
 629  
 630                              if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
 631                              {
 632                                  await UserErrorDialog.ShowUserErrorDialog(userError);
 633                                  Device.Dispose();
 634  
 635                                  return false;
 636                              }
 637  
 638                              // Tell the user that we installed a firmware for them.
 639                              if (userError == UserError.NoFirmware)
 640                              {
 641                                  firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
 642  
 643                                  _viewModel.RefreshFirmwareStatus();
 644  
 645                                  await ContentDialogHelper.CreateInfoDialog(
 646                                      LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
 647                                      LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
 648                                      LocaleManager.Instance[LocaleKeys.InputDialogOk],
 649                                      "",
 650                                      LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
 651                              }
 652                          }
 653                          else
 654                          {
 655                              await UserErrorDialog.ShowUserErrorDialog(userError);
 656                              Device.Dispose();
 657  
 658                              return false;
 659                          }
 660                      }
 661                  }
 662              }
 663  
 664              Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
 665  
 666              if (_isFirmwareTitle)
 667              {
 668                  Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
 669  
 670                  if (!Device.LoadNca(ApplicationPath))
 671                  {
 672                      Device.Dispose();
 673  
 674                      return false;
 675                  }
 676              }
 677              else if (Directory.Exists(ApplicationPath))
 678              {
 679                  string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage");
 680  
 681                  if (romFsFiles.Length == 0)
 682                  {
 683                      romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
 684                  }
 685  
 686                  if (romFsFiles.Length > 0)
 687                  {
 688                      Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
 689  
 690                      if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
 691                      {
 692                          Device.Dispose();
 693  
 694                          return false;
 695                      }
 696                  }
 697                  else
 698                  {
 699                      Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
 700  
 701                      if (!Device.LoadCart(ApplicationPath))
 702                      {
 703                          Device.Dispose();
 704  
 705                          return false;
 706                      }
 707                  }
 708              }
 709              else if (File.Exists(ApplicationPath))
 710              {
 711                  switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
 712                  {
 713                      case ".xci":
 714                          {
 715                              Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
 716  
 717                              if (!Device.LoadXci(ApplicationPath, ApplicationId))
 718                              {
 719                                  Device.Dispose();
 720  
 721                                  return false;
 722                              }
 723  
 724                              break;
 725                          }
 726                      case ".nca":
 727                          {
 728                              Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
 729  
 730                              if (!Device.LoadNca(ApplicationPath))
 731                              {
 732                                  Device.Dispose();
 733  
 734                                  return false;
 735                              }
 736  
 737                              break;
 738                          }
 739                      case ".nsp":
 740                      case ".pfs0":
 741                          {
 742                              Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
 743  
 744                              if (!Device.LoadNsp(ApplicationPath, ApplicationId))
 745                              {
 746                                  Device.Dispose();
 747  
 748                                  return false;
 749                              }
 750  
 751                              break;
 752                          }
 753                      default:
 754                          {
 755                              Logger.Info?.Print(LogClass.Application, "Loading as homebrew.");
 756  
 757                              try
 758                              {
 759                                  if (!Device.LoadProgram(ApplicationPath))
 760                                  {
 761                                      Device.Dispose();
 762  
 763                                      return false;
 764                                  }
 765                              }
 766                              catch (ArgumentOutOfRangeException)
 767                              {
 768                                  Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
 769  
 770                                  Device.Dispose();
 771  
 772                                  return false;
 773                              }
 774  
 775                              break;
 776                          }
 777                  }
 778              }
 779              else
 780              {
 781                  Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
 782  
 783                  Device.Dispose();
 784  
 785                  return false;
 786              }
 787  
 788              DiscordIntegrationModule.SwitchToPlayingState(Device.Processes.ActiveApplication.ProgramIdText, Device.Processes.ActiveApplication.Name);
 789  
 790              ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
 791              {
 792                  appMetadata.UpdatePreGame();
 793              });
 794  
 795              return true;
 796          }
 797  
 798          internal void Resume()
 799          {
 800              Device?.System.TogglePauseEmulation(false);
 801  
 802              _viewModel.IsPaused = false;
 803              _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
 804              Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
 805          }
 806  
 807          internal void Pause()
 808          {
 809              Device?.System.TogglePauseEmulation(true);
 810  
 811              _viewModel.IsPaused = true;
 812              _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
 813              Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
 814          }
 815  
 816          private void InitializeSwitchInstance()
 817          {
 818              // Initialize KeySet.
 819              VirtualFileSystem.ReloadKeySet();
 820  
 821              // Initialize Renderer.
 822              IRenderer renderer;
 823  
 824              if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
 825              {
 826                  renderer = new VulkanRenderer(
 827                      Vk.GetApi(),
 828                      (RendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
 829                      VulkanHelper.GetRequiredInstanceExtensions,
 830                      ConfigurationState.Instance.Graphics.PreferredGpu.Value);
 831              }
 832              else
 833              {
 834                  renderer = new OpenGLRenderer();
 835              }
 836  
 837              BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
 838  
 839              var isGALThreaded = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
 840              if (isGALThreaded)
 841              {
 842                  renderer = new ThreadedRenderer(renderer);
 843              }
 844  
 845              Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {isGALThreaded}");
 846  
 847              // Initialize Configuration.
 848              var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value ? MemoryConfiguration.MemoryConfiguration8GiB : MemoryConfiguration.MemoryConfiguration4GiB;
 849  
 850              HLEConfiguration configuration = new(VirtualFileSystem,
 851                                                   _viewModel.LibHacHorizonManager,
 852                                                   ContentManager,
 853                                                   _accountManager,
 854                                                   _userChannelPersistence,
 855                                                   renderer,
 856                                                   InitializeAudio(),
 857                                                   memoryConfiguration,
 858                                                   _viewModel.UiHandler,
 859                                                   (SystemLanguage)ConfigurationState.Instance.System.Language.Value,
 860                                                   (RegionCode)ConfigurationState.Instance.System.Region.Value,
 861                                                   ConfigurationState.Instance.Graphics.EnableVsync,
 862                                                   ConfigurationState.Instance.System.EnableDockedMode,
 863                                                   ConfigurationState.Instance.System.EnablePtc,
 864                                                   ConfigurationState.Instance.System.EnableInternetAccess,
 865                                                   ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
 866                                                   ConfigurationState.Instance.System.FsGlobalAccessLogMode,
 867                                                   ConfigurationState.Instance.System.SystemTimeOffset,
 868                                                   ConfigurationState.Instance.System.TimeZone,
 869                                                   ConfigurationState.Instance.System.MemoryManagerMode,
 870                                                   ConfigurationState.Instance.System.IgnoreMissingServices,
 871                                                   ConfigurationState.Instance.Graphics.AspectRatio,
 872                                                   ConfigurationState.Instance.System.AudioVolume,
 873                                                   ConfigurationState.Instance.System.UseHypervisor,
 874                                                   ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
 875                                                   ConfigurationState.Instance.Multiplayer.Mode);
 876  
 877              Device = new Switch(configuration);
 878          }
 879  
 880          private static IHardwareDeviceDriver InitializeAudio()
 881          {
 882              var availableBackends = new List<AudioBackend>
 883              {
 884                  AudioBackend.SDL2,
 885                  AudioBackend.SoundIo,
 886                  AudioBackend.OpenAl,
 887                  AudioBackend.Dummy,
 888              };
 889  
 890              AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
 891  
 892              for (int i = 0; i < availableBackends.Count; i++)
 893              {
 894                  if (availableBackends[i] == preferredBackend)
 895                  {
 896                      availableBackends.RemoveAt(i);
 897                      availableBackends.Insert(0, preferredBackend);
 898                      break;
 899                  }
 900              }
 901  
 902              static IHardwareDeviceDriver InitializeAudioBackend<T>(AudioBackend backend, AudioBackend nextBackend) where T : IHardwareDeviceDriver, new()
 903              {
 904                  if (T.IsSupported)
 905                  {
 906                      return new T();
 907                  }
 908  
 909                  Logger.Warning?.Print(LogClass.Audio, $"{backend} is not supported, falling back to {nextBackend}.");
 910  
 911                  return null;
 912              }
 913  
 914              IHardwareDeviceDriver deviceDriver = null;
 915  
 916              for (int i = 0; i < availableBackends.Count; i++)
 917              {
 918                  AudioBackend currentBackend = availableBackends[i];
 919                  AudioBackend nextBackend = i + 1 < availableBackends.Count ? availableBackends[i + 1] : AudioBackend.Dummy;
 920  
 921                  deviceDriver = currentBackend switch
 922                  {
 923                      AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
 924                      AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
 925                      AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
 926                      _ => new DummyHardwareDeviceDriver(),
 927                  };
 928  
 929                  if (deviceDriver != null)
 930                  {
 931                      ConfigurationState.Instance.System.AudioBackend.Value = currentBackend;
 932                      break;
 933                  }
 934              }
 935  
 936              MainWindowViewModel.SaveConfig();
 937  
 938              return deviceDriver;
 939          }
 940  
 941          private void Window_BoundsChanged(object sender, Size e)
 942          {
 943              Width = (int)e.Width;
 944              Height = (int)e.Height;
 945  
 946              SetRendererWindowSize(e);
 947          }
 948  
 949          private void MainLoop()
 950          {
 951              while (_isActive)
 952              {
 953                  UpdateFrame();
 954  
 955                  // Polling becomes expensive if it's not slept.
 956                  Thread.Sleep(1);
 957              }
 958          }
 959  
 960          private void RenderLoop()
 961          {
 962              Dispatcher.UIThread.InvokeAsync(() =>
 963              {
 964                  if (_viewModel.StartGamesInFullscreen)
 965                  {
 966                      _viewModel.WindowState = WindowState.FullScreen;
 967                  }
 968  
 969                  if (_viewModel.WindowState == WindowState.FullScreen)
 970                  {
 971                      _viewModel.ShowMenuAndStatusBar = false;
 972                  }
 973              });
 974  
 975              _renderer = Device.Gpu.Renderer is ThreadedRenderer tr ? tr.BaseRenderer : Device.Gpu.Renderer;
 976  
 977              _renderer.ScreenCaptured += Renderer_ScreenCaptured;
 978  
 979              (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
 980  
 981              Device.Gpu.Renderer.Initialize(_glLogLevel);
 982  
 983              _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
 984              _renderer?.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
 985              _renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
 986              _renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
 987  
 988              Width = (int)RendererHost.Bounds.Width;
 989              Height = (int)RendererHost.Bounds.Height;
 990  
 991              _renderer.Window.SetSize((int)(Width * _topLevel.RenderScaling), (int)(Height * _topLevel.RenderScaling));
 992  
 993              _chrono.Start();
 994  
 995              Device.Gpu.Renderer.RunLoop(() =>
 996              {
 997                  Device.Gpu.SetGpuThread();
 998                  Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
 999  
1000                  _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
1001  
1002                  while (_isActive)
1003                  {
1004                      _ticks += _chrono.ElapsedTicks;
1005  
1006                      _chrono.Restart();
1007  
1008                      if (Device.WaitFifo())
1009                      {
1010                          Device.Statistics.RecordFifoStart();
1011                          Device.ProcessFrame();
1012                          Device.Statistics.RecordFifoEnd();
1013                      }
1014  
1015                      while (Device.ConsumeFrameAvailable())
1016                      {
1017                          if (!_renderingStarted)
1018                          {
1019                              _renderingStarted = true;
1020                              _viewModel.SwitchToRenderer(false);
1021                              InitStatus();
1022                          }
1023  
1024                          Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
1025                      }
1026  
1027                      if (_ticks >= _ticksPerFrame)
1028                      {
1029                          UpdateStatus();
1030                      }
1031                  }
1032  
1033                  // Make sure all commands in the run loop are fully executed before leaving the loop.
1034                  if (Device.Gpu.Renderer is ThreadedRenderer threaded)
1035                  {
1036                      threaded.FlushThreadedCommands();
1037                  }
1038  
1039                  _gpuDoneEvent.Set();
1040              });
1041  
1042              (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true);
1043          }
1044  
1045          public void InitStatus()
1046          {
1047              StatusInitEvent?.Invoke(this, new StatusInitEventArgs(
1048                  ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch
1049                  {
1050                      GraphicsBackend.Vulkan => "Vulkan",
1051                      GraphicsBackend.OpenGl => "OpenGL",
1052                      _ => throw new NotImplementedException()
1053                  },
1054                  $"GPU: {_renderer.GetHardwareInfo().GpuDriver}"));
1055          }
1056  
1057          public void UpdateStatus()
1058          {
1059              // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued.
1060              string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
1061  
1062              if (GraphicsConfig.ResScale != 1)
1063              {
1064                  dockedMode += $" ({GraphicsConfig.ResScale}x)";
1065              }
1066  
1067              StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
1068                  Device.EnableDeviceVsync,
1069                  LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
1070                  dockedMode,
1071                  ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
1072                  LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
1073                  $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %"));
1074          }
1075  
1076          public async Task ShowExitPrompt()
1077          {
1078              bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
1079              if (!shouldExit)
1080              {
1081                  if (_dialogShown)
1082                  {
1083                      return;
1084                  }
1085  
1086                  _dialogShown = true;
1087  
1088                  shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
1089  
1090                  _dialogShown = false;
1091              }
1092  
1093              if (shouldExit)
1094              {
1095                  Stop();
1096              }
1097          }
1098  
1099          private bool UpdateFrame()
1100          {
1101              if (!_isActive)
1102              {
1103                  return false;
1104              }
1105  
1106              NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
1107  
1108              if (_viewModel.IsActive)
1109              {
1110                  bool isCursorVisible = true;
1111  
1112                  if (_isCursorInRenderer && !_viewModel.ShowLoadProgress)
1113                  {
1114                      if (ConfigurationState.Instance.Hid.EnableMouse.Value)
1115                      {
1116                          isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never;
1117                      }
1118                      else
1119                      {
1120                          isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never ||
1121                              (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle &&
1122                              Stopwatch.GetTimestamp() - _lastCursorMoveTime < CursorHideIdleTime * Stopwatch.Frequency);
1123                      }
1124                  }
1125  
1126                  if (_cursorState != (isCursorVisible ? CursorStates.CursorIsVisible : CursorStates.CursorIsHidden))
1127                  {
1128                      if (isCursorVisible)
1129                      {
1130                          ShowCursor();
1131                      }
1132                      else
1133                      {
1134                          HideCursor();
1135                      }
1136                  }
1137  
1138                  Dispatcher.UIThread.Post(() =>
1139                  {
1140                      if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
1141                      {
1142                          Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
1143                      }
1144                  });
1145  
1146                  KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
1147  
1148                  if (currentHotkeyState != _prevHotkeyState)
1149                  {
1150                      switch (currentHotkeyState)
1151                      {
1152                          case KeyboardHotkeyState.ToggleVSync:
1153                              ToggleVSync();
1154                              break;
1155                          case KeyboardHotkeyState.Screenshot:
1156                              ScreenshotRequested = true;
1157                              break;
1158                          case KeyboardHotkeyState.ShowUI:
1159                              _viewModel.ShowMenuAndStatusBar = !_viewModel.ShowMenuAndStatusBar;
1160                              break;
1161                          case KeyboardHotkeyState.Pause:
1162                              if (_viewModel.IsPaused)
1163                              {
1164                                  Resume();
1165                              }
1166                              else
1167                              {
1168                                  Pause();
1169                              }
1170                              break;
1171                          case KeyboardHotkeyState.ToggleMute:
1172                              if (Device.IsAudioMuted())
1173                              {
1174                                  Device.SetVolume(_viewModel.VolumeBeforeMute);
1175                              }
1176                              else
1177                              {
1178                                  _viewModel.VolumeBeforeMute = Device.GetVolume();
1179                                  Device.SetVolume(0);
1180                              }
1181  
1182                              _viewModel.Volume = Device.GetVolume();
1183                              break;
1184                          case KeyboardHotkeyState.ResScaleUp:
1185                              GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
1186                              break;
1187                          case KeyboardHotkeyState.ResScaleDown:
1188                              GraphicsConfig.ResScale =
1189                              (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
1190                              break;
1191                          case KeyboardHotkeyState.VolumeUp:
1192                              _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
1193                              Device.SetVolume(_newVolume);
1194  
1195                              _viewModel.Volume = Device.GetVolume();
1196                              break;
1197                          case KeyboardHotkeyState.VolumeDown:
1198                              _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
1199                              Device.SetVolume(_newVolume);
1200  
1201                              _viewModel.Volume = Device.GetVolume();
1202                              break;
1203                          case KeyboardHotkeyState.None:
1204                              (_keyboardInterface as AvaloniaKeyboard).Clear();
1205                              break;
1206                      }
1207                  }
1208  
1209                  _prevHotkeyState = currentHotkeyState;
1210  
1211                  if (ScreenshotRequested)
1212                  {
1213                      ScreenshotRequested = false;
1214                      _renderer.Screenshot();
1215                  }
1216              }
1217  
1218              // Touchscreen.
1219              bool hasTouch = false;
1220  
1221              if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value)
1222              {
1223                  hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
1224              }
1225  
1226              if (!hasTouch)
1227              {
1228                  Device.Hid.Touchscreen.Update();
1229              }
1230  
1231              Device.Hid.DebugPad.Update();
1232  
1233              return true;
1234          }
1235  
1236          private KeyboardHotkeyState GetHotkeyState()
1237          {
1238              KeyboardHotkeyState state = KeyboardHotkeyState.None;
1239  
1240              if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
1241              {
1242                  state = KeyboardHotkeyState.ToggleVSync;
1243              }
1244              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
1245              {
1246                  state = KeyboardHotkeyState.Screenshot;
1247              }
1248              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI))
1249              {
1250                  state = KeyboardHotkeyState.ShowUI;
1251              }
1252              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
1253              {
1254                  state = KeyboardHotkeyState.Pause;
1255              }
1256              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
1257              {
1258                  state = KeyboardHotkeyState.ToggleMute;
1259              }
1260              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
1261              {
1262                  state = KeyboardHotkeyState.ResScaleUp;
1263              }
1264              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
1265              {
1266                  state = KeyboardHotkeyState.ResScaleDown;
1267              }
1268              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
1269              {
1270                  state = KeyboardHotkeyState.VolumeUp;
1271              }
1272              else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
1273              {
1274                  state = KeyboardHotkeyState.VolumeDown;
1275              }
1276  
1277              return state;
1278          }
1279      }
1280  }