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 }