AppHost.cs 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. using ARMeilleure.Translation;
  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.Logging;
  23. using Ryujinx.Common.SystemInterop;
  24. using Ryujinx.Graphics.GAL;
  25. using Ryujinx.Graphics.GAL.Multithreading;
  26. using Ryujinx.Graphics.Gpu;
  27. using Ryujinx.Graphics.OpenGL;
  28. using Ryujinx.Graphics.Vulkan;
  29. using Ryujinx.HLE.FileSystem;
  30. using Ryujinx.HLE.HOS;
  31. using Ryujinx.HLE.HOS.Services.Account.Acc;
  32. using Ryujinx.HLE.HOS.SystemState;
  33. using Ryujinx.Input;
  34. using Ryujinx.Input.HLE;
  35. using Ryujinx.Ui.Common;
  36. using Ryujinx.Ui.Common.Configuration;
  37. using Ryujinx.Ui.Common.Helper;
  38. using SixLabors.ImageSharp;
  39. using SixLabors.ImageSharp.Formats.Png;
  40. using SixLabors.ImageSharp.PixelFormats;
  41. using SixLabors.ImageSharp.Processing;
  42. using SPB.Graphics.Vulkan;
  43. using System;
  44. using System.Collections.Generic;
  45. using System.Diagnostics;
  46. using System.IO;
  47. using System.Threading;
  48. using System.Threading.Tasks;
  49. using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
  50. using Image = SixLabors.ImageSharp.Image;
  51. using InputManager = Ryujinx.Input.HLE.InputManager;
  52. using Key = Ryujinx.Input.Key;
  53. using MouseButton = Ryujinx.Input.MouseButton;
  54. using Size = Avalonia.Size;
  55. using Switch = Ryujinx.HLE.Switch;
  56. namespace Ryujinx.Ava
  57. {
  58. internal class AppHost
  59. {
  60. private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
  61. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
  62. private const int TargetFps = 60;
  63. private const float VolumeDelta = 0.05f;
  64. private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
  65. private readonly IntPtr InvisibleCursorWin;
  66. private readonly IntPtr DefaultCursorWin;
  67. private readonly long _ticksPerFrame;
  68. private readonly Stopwatch _chrono;
  69. private long _ticks;
  70. private readonly AccountManager _accountManager;
  71. private readonly UserChannelPersistence _userChannelPersistence;
  72. private readonly InputManager _inputManager;
  73. private readonly MainWindowViewModel _viewModel;
  74. private readonly IKeyboard _keyboardInterface;
  75. private readonly TopLevel _topLevel;
  76. public RendererHost _rendererHost;
  77. private readonly GraphicsDebugLevel _glLogLevel;
  78. private float _newVolume;
  79. private KeyboardHotkeyState _prevHotkeyState;
  80. private long _lastCursorMoveTime;
  81. private bool _isCursorInRenderer;
  82. private bool _isStopped;
  83. private bool _isActive;
  84. private bool _renderingStarted;
  85. private IRenderer _renderer;
  86. private readonly Thread _renderingThread;
  87. private readonly CancellationTokenSource _gpuCancellationTokenSource;
  88. private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
  89. private bool _dialogShown;
  90. private readonly bool _isFirmwareTitle;
  91. private readonly object _lockObject = new();
  92. public event EventHandler AppExit;
  93. public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
  94. public VirtualFileSystem VirtualFileSystem { get; }
  95. public ContentManager ContentManager { get; }
  96. public NpadManager NpadManager { get; }
  97. public TouchScreenManager TouchScreenManager { get; }
  98. public Switch Device { get; set; }
  99. public int Width { get; private set; }
  100. public int Height { get; private set; }
  101. public string ApplicationPath { get; private set; }
  102. public bool ScreenshotRequested { get; set; }
  103. public AppHost(
  104. RendererHost renderer,
  105. InputManager inputManager,
  106. string applicationPath,
  107. VirtualFileSystem virtualFileSystem,
  108. ContentManager contentManager,
  109. AccountManager accountManager,
  110. UserChannelPersistence userChannelPersistence,
  111. MainWindowViewModel viewmodel,
  112. TopLevel topLevel)
  113. {
  114. _viewModel = viewmodel;
  115. _inputManager = inputManager;
  116. _accountManager = accountManager;
  117. _userChannelPersistence = userChannelPersistence;
  118. _renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
  119. _lastCursorMoveTime = Stopwatch.GetTimestamp();
  120. _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
  121. _topLevel = topLevel;
  122. _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
  123. _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
  124. NpadManager = _inputManager.CreateNpadManager();
  125. TouchScreenManager = _inputManager.CreateTouchScreenManager();
  126. ApplicationPath = applicationPath;
  127. VirtualFileSystem = virtualFileSystem;
  128. ContentManager = contentManager;
  129. _rendererHost = renderer;
  130. _chrono = new Stopwatch();
  131. _ticksPerFrame = Stopwatch.Frequency / TargetFps;
  132. if (ApplicationPath.StartsWith("@SystemContent"))
  133. {
  134. ApplicationPath = _viewModel.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
  135. _isFirmwareTitle = true;
  136. }
  137. ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
  138. _topLevel.PointerMoved += TopLevel_PointerMoved;
  139. if (OperatingSystem.IsWindows())
  140. {
  141. InvisibleCursorWin = CreateEmptyCursor();
  142. DefaultCursorWin = CreateArrowCursor();
  143. }
  144. ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
  145. ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
  146. ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
  147. ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
  148. ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
  149. ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
  150. ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAntiAliasing;
  151. ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
  152. ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
  153. ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
  154. _gpuCancellationTokenSource = new CancellationTokenSource();
  155. }
  156. private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
  157. {
  158. if (sender is MainWindow window)
  159. {
  160. _lastCursorMoveTime = Stopwatch.GetTimestamp();
  161. if (_rendererHost.EmbeddedWindow.TransformedBounds != null)
  162. {
  163. var point = e.GetCurrentPoint(window).Position;
  164. var bounds = _rendererHost.EmbeddedWindow.TransformedBounds.Value.Clip;
  165. _isCursorInRenderer = point.X >= bounds.X &&
  166. point.X <= bounds.Width + bounds.X &&
  167. point.Y >= bounds.Y &&
  168. point.Y <= bounds.Height + bounds.Y;
  169. }
  170. }
  171. }
  172. private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
  173. {
  174. _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
  175. _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
  176. }
  177. private void UpdateScalingFilter(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.ScalingFilter> e)
  178. {
  179. _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
  180. _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
  181. }
  182. private void ShowCursor()
  183. {
  184. Dispatcher.UIThread.Post(() =>
  185. {
  186. _viewModel.Cursor = Cursor.Default;
  187. if (OperatingSystem.IsWindows())
  188. {
  189. SetCursor(DefaultCursorWin);
  190. }
  191. });
  192. }
  193. private void HideCursor()
  194. {
  195. Dispatcher.UIThread.Post(() =>
  196. {
  197. _viewModel.Cursor = InvisibleCursor;
  198. if (OperatingSystem.IsWindows())
  199. {
  200. SetCursor(InvisibleCursorWin);
  201. }
  202. });
  203. }
  204. private void SetRendererWindowSize(Size size)
  205. {
  206. if (_renderer != null)
  207. {
  208. double scale = _topLevel.PlatformImpl.RenderScaling;
  209. _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
  210. }
  211. }
  212. private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
  213. {
  214. if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
  215. {
  216. Task.Run(() =>
  217. {
  218. lock (_lockObject)
  219. {
  220. DateTime currentTime = DateTime.Now;
  221. string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
  222. string directory = AppDataManager.Mode switch
  223. {
  224. AppDataManager.LaunchMode.Portable => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
  225. _ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx")
  226. };
  227. string path = Path.Combine(directory, filename);
  228. try
  229. {
  230. Directory.CreateDirectory(directory);
  231. }
  232. catch (Exception ex)
  233. {
  234. Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
  235. return;
  236. }
  237. Image image = e.IsBgra ? Image.LoadPixelData<Bgra32>(e.Data, e.Width, e.Height)
  238. : Image.LoadPixelData<Rgba32>(e.Data, e.Width, e.Height);
  239. if (e.FlipX)
  240. {
  241. image.Mutate(x => x.Flip(FlipMode.Horizontal));
  242. }
  243. if (e.FlipY)
  244. {
  245. image.Mutate(x => x.Flip(FlipMode.Vertical));
  246. }
  247. image.SaveAsPng(path, new PngEncoder()
  248. {
  249. ColorType = PngColorType.Rgb
  250. });
  251. image.Dispose();
  252. Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
  253. }
  254. });
  255. }
  256. else
  257. {
  258. Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
  259. }
  260. }
  261. public void Start()
  262. {
  263. if (OperatingSystem.IsWindows())
  264. {
  265. _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
  266. }
  267. DisplaySleep.Prevent();
  268. NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
  269. TouchScreenManager.Initialize(Device);
  270. _viewModel.IsGameRunning = true;
  271. var activeProcess = Device.Processes.ActiveApplication;
  272. string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}";
  273. string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}";
  274. string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})";
  275. string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
  276. Dispatcher.UIThread.InvokeAsync(() =>
  277. {
  278. _viewModel.Title = $"Ryujinx {Program.Version} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
  279. });
  280. _viewModel.SetUIProgressHandlers(Device);
  281. _rendererHost.SizeChanged += Window_SizeChanged;
  282. _isActive = true;
  283. _renderingThread.Start();
  284. _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
  285. MainLoop();
  286. Exit();
  287. }
  288. private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
  289. {
  290. if (Device != null)
  291. {
  292. Device.Configuration.IgnoreMissingServices = args.NewValue;
  293. }
  294. }
  295. private void UpdateAspectRatioState(object sender, ReactiveEventArgs<AspectRatio> args)
  296. {
  297. if (Device != null)
  298. {
  299. Device.Configuration.AspectRatio = args.NewValue;
  300. }
  301. }
  302. private void UpdateAntiAliasing(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.AntiAliasing> e)
  303. {
  304. _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
  305. }
  306. private void UpdateDockedModeState(object sender, ReactiveEventArgs<bool> e)
  307. {
  308. Device?.System.ChangeDockedModeState(e.NewValue);
  309. }
  310. private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
  311. {
  312. Device?.SetVolume(e.NewValue);
  313. Dispatcher.UIThread.Post(() =>
  314. {
  315. _viewModel.Volume = e.NewValue;
  316. });
  317. }
  318. private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
  319. {
  320. Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
  321. }
  322. public void Stop()
  323. {
  324. _isActive = false;
  325. }
  326. private void Exit()
  327. {
  328. (_keyboardInterface as AvaloniaKeyboard)?.Clear();
  329. if (_isStopped)
  330. {
  331. return;
  332. }
  333. _isStopped = true;
  334. _isActive = false;
  335. }
  336. public void DisposeContext()
  337. {
  338. Dispose();
  339. _isActive = false;
  340. if (_renderingThread.IsAlive)
  341. {
  342. _renderingThread.Join();
  343. }
  344. DisplaySleep.Restore();
  345. NpadManager.Dispose();
  346. TouchScreenManager.Dispose();
  347. Device.Dispose();
  348. DisposeGpu();
  349. AppExit?.Invoke(this, EventArgs.Empty);
  350. }
  351. private void Dispose()
  352. {
  353. if (Device.Processes != null)
  354. {
  355. _viewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
  356. }
  357. ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
  358. ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
  359. ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
  360. ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
  361. ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
  362. ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
  363. ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing;
  364. _topLevel.PointerMoved -= TopLevel_PointerMoved;
  365. _gpuCancellationTokenSource.Cancel();
  366. _gpuCancellationTokenSource.Dispose();
  367. _chrono.Stop();
  368. }
  369. public void DisposeGpu()
  370. {
  371. if (OperatingSystem.IsWindows())
  372. {
  373. _windowsMultimediaTimerResolution?.Dispose();
  374. _windowsMultimediaTimerResolution = null;
  375. }
  376. (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent();
  377. Device.DisposeGpu();
  378. (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
  379. }
  380. private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
  381. {
  382. if (state.NewValue)
  383. {
  384. _lastCursorMoveTime = Stopwatch.GetTimestamp();
  385. }
  386. }
  387. public async Task<bool> LoadGuestApplication()
  388. {
  389. InitializeSwitchInstance();
  390. MainWindow.UpdateGraphicsConfig();
  391. SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
  392. if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  393. {
  394. if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
  395. {
  396. {
  397. if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
  398. {
  399. if (userError == UserError.NoFirmware)
  400. {
  401. UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
  402. LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
  403. LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
  404. LocaleManager.Instance[LocaleKeys.InputDialogYes],
  405. LocaleManager.Instance[LocaleKeys.InputDialogNo],
  406. "");
  407. if (result != UserResult.Yes)
  408. {
  409. await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
  410. Device.Dispose();
  411. return false;
  412. }
  413. }
  414. if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
  415. {
  416. await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
  417. Device.Dispose();
  418. return false;
  419. }
  420. // Tell the user that we installed a firmware for them.
  421. if (userError == UserError.NoFirmware)
  422. {
  423. firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
  424. _viewModel.RefreshFirmwareStatus();
  425. await ContentDialogHelper.CreateInfoDialog(
  426. LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
  427. LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
  428. LocaleManager.Instance[LocaleKeys.InputDialogOk],
  429. "",
  430. LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
  431. }
  432. }
  433. else
  434. {
  435. await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
  436. Device.Dispose();
  437. return false;
  438. }
  439. }
  440. }
  441. }
  442. Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
  443. if (_isFirmwareTitle)
  444. {
  445. Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
  446. if (!Device.LoadNca(ApplicationPath))
  447. {
  448. Device.Dispose();
  449. return false;
  450. }
  451. }
  452. else if (Directory.Exists(ApplicationPath))
  453. {
  454. string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage");
  455. if (romFsFiles.Length == 0)
  456. {
  457. romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
  458. }
  459. if (romFsFiles.Length > 0)
  460. {
  461. Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
  462. if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
  463. {
  464. Device.Dispose();
  465. return false;
  466. }
  467. }
  468. else
  469. {
  470. Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
  471. if (!Device.LoadCart(ApplicationPath))
  472. {
  473. Device.Dispose();
  474. return false;
  475. }
  476. }
  477. }
  478. else if (File.Exists(ApplicationPath))
  479. {
  480. switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
  481. {
  482. case ".xci":
  483. {
  484. Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
  485. if (!Device.LoadXci(ApplicationPath))
  486. {
  487. Device.Dispose();
  488. return false;
  489. }
  490. break;
  491. }
  492. case ".nca":
  493. {
  494. Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
  495. if (!Device.LoadNca(ApplicationPath))
  496. {
  497. Device.Dispose();
  498. return false;
  499. }
  500. break;
  501. }
  502. case ".nsp":
  503. case ".pfs0":
  504. {
  505. Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
  506. if (!Device.LoadNsp(ApplicationPath))
  507. {
  508. Device.Dispose();
  509. return false;
  510. }
  511. break;
  512. }
  513. default:
  514. {
  515. Logger.Info?.Print(LogClass.Application, "Loading as homebrew.");
  516. try
  517. {
  518. if (!Device.LoadProgram(ApplicationPath))
  519. {
  520. Device.Dispose();
  521. return false;
  522. }
  523. }
  524. catch (ArgumentOutOfRangeException)
  525. {
  526. Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
  527. Device.Dispose();
  528. return false;
  529. }
  530. break;
  531. }
  532. }
  533. }
  534. else
  535. {
  536. Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
  537. Device.Dispose();
  538. return false;
  539. }
  540. DiscordIntegrationModule.SwitchToPlayingState(Device.Processes.ActiveApplication.ProgramIdText, Device.Processes.ActiveApplication.Name);
  541. _viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
  542. {
  543. appMetadata.LastPlayed = DateTime.UtcNow.ToString();
  544. });
  545. return true;
  546. }
  547. internal void Resume()
  548. {
  549. Device?.System.TogglePauseEmulation(false);
  550. _viewModel.IsPaused = false;
  551. }
  552. internal void Pause()
  553. {
  554. Device?.System.TogglePauseEmulation(true);
  555. _viewModel.IsPaused = true;
  556. }
  557. private void InitializeSwitchInstance()
  558. {
  559. // Initialize KeySet.
  560. VirtualFileSystem.ReloadKeySet();
  561. // Initialize Renderer.
  562. IRenderer renderer;
  563. if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
  564. {
  565. renderer = new VulkanRenderer(
  566. (_rendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
  567. VulkanHelper.GetRequiredInstanceExtensions,
  568. ConfigurationState.Instance.Graphics.PreferredGpu.Value);
  569. }
  570. else
  571. {
  572. renderer = new OpenGLRenderer();
  573. }
  574. BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
  575. var isGALthreaded = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
  576. if (isGALthreaded)
  577. {
  578. renderer = new ThreadedRenderer(renderer);
  579. }
  580. Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {isGALthreaded}");
  581. // Initialize Configuration.
  582. var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value ? HLE.MemoryConfiguration.MemoryConfiguration6GiB : HLE.MemoryConfiguration.MemoryConfiguration4GiB;
  583. HLE.HLEConfiguration configuration = new(VirtualFileSystem,
  584. _viewModel.LibHacHorizonManager,
  585. ContentManager,
  586. _accountManager,
  587. _userChannelPersistence,
  588. renderer,
  589. InitializeAudio(),
  590. memoryConfiguration,
  591. _viewModel.UiHandler,
  592. (SystemLanguage)ConfigurationState.Instance.System.Language.Value,
  593. (RegionCode)ConfigurationState.Instance.System.Region.Value,
  594. ConfigurationState.Instance.Graphics.EnableVsync,
  595. ConfigurationState.Instance.System.EnableDockedMode,
  596. ConfigurationState.Instance.System.EnablePtc,
  597. ConfigurationState.Instance.System.EnableInternetAccess,
  598. ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
  599. ConfigurationState.Instance.System.FsGlobalAccessLogMode,
  600. ConfigurationState.Instance.System.SystemTimeOffset,
  601. ConfigurationState.Instance.System.TimeZone,
  602. ConfigurationState.Instance.System.MemoryManagerMode,
  603. ConfigurationState.Instance.System.IgnoreMissingServices,
  604. ConfigurationState.Instance.Graphics.AspectRatio,
  605. ConfigurationState.Instance.System.AudioVolume,
  606. ConfigurationState.Instance.System.UseHypervisor,
  607. ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
  608. Device = new Switch(configuration);
  609. }
  610. private static IHardwareDeviceDriver InitializeAudio()
  611. {
  612. var availableBackends = new List<AudioBackend>()
  613. {
  614. AudioBackend.SDL2,
  615. AudioBackend.SoundIo,
  616. AudioBackend.OpenAl,
  617. AudioBackend.Dummy
  618. };
  619. AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
  620. for (int i = 0; i < availableBackends.Count; i++)
  621. {
  622. if (availableBackends[i] == preferredBackend)
  623. {
  624. availableBackends.RemoveAt(i);
  625. availableBackends.Insert(0, preferredBackend);
  626. break;
  627. }
  628. }
  629. static IHardwareDeviceDriver InitializeAudioBackend<T>(AudioBackend backend, AudioBackend nextBackend) where T : IHardwareDeviceDriver, new()
  630. {
  631. if (T.IsSupported)
  632. {
  633. return new T();
  634. }
  635. else
  636. {
  637. Logger.Warning?.Print(LogClass.Audio, $"{backend} is not supported, falling back to {nextBackend}.");
  638. return null;
  639. }
  640. }
  641. IHardwareDeviceDriver deviceDriver = null;
  642. for (int i = 0; i < availableBackends.Count; i++)
  643. {
  644. AudioBackend currentBackend = availableBackends[i];
  645. AudioBackend nextBackend = i + 1 < availableBackends.Count ? availableBackends[i + 1] : AudioBackend.Dummy;
  646. deviceDriver = currentBackend switch
  647. {
  648. AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
  649. AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
  650. AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
  651. _ => new DummyHardwareDeviceDriver()
  652. };
  653. if (deviceDriver != null)
  654. {
  655. ConfigurationState.Instance.System.AudioBackend.Value = currentBackend;
  656. break;
  657. }
  658. }
  659. MainWindowViewModel.SaveConfig();
  660. return deviceDriver;
  661. }
  662. private void Window_SizeChanged(object sender, Size e)
  663. {
  664. Width = (int)e.Width;
  665. Height = (int)e.Height;
  666. SetRendererWindowSize(e);
  667. }
  668. private void MainLoop()
  669. {
  670. while (_isActive)
  671. {
  672. UpdateFrame();
  673. // Polling becomes expensive if it's not slept.
  674. Thread.Sleep(1);
  675. }
  676. }
  677. private void RenderLoop()
  678. {
  679. Dispatcher.UIThread.InvokeAsync(() =>
  680. {
  681. if (_viewModel.StartGamesInFullscreen)
  682. {
  683. _viewModel.WindowState = WindowState.FullScreen;
  684. }
  685. if (_viewModel.WindowState == WindowState.FullScreen)
  686. {
  687. _viewModel.ShowMenuAndStatusBar = false;
  688. }
  689. });
  690. _renderer = Device.Gpu.Renderer is ThreadedRenderer tr ? tr.BaseRenderer : Device.Gpu.Renderer;
  691. _renderer.ScreenCaptured += Renderer_ScreenCaptured;
  692. (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
  693. Device.Gpu.Renderer.Initialize(_glLogLevel);
  694. _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
  695. _renderer?.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
  696. _renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
  697. Width = (int)_rendererHost.Bounds.Width;
  698. Height = (int)_rendererHost.Bounds.Height;
  699. _renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
  700. _chrono.Start();
  701. Device.Gpu.Renderer.RunLoop(() =>
  702. {
  703. Device.Gpu.SetGpuThread();
  704. Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
  705. Translator.IsReadyForTranslation.Set();
  706. _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
  707. while (_isActive)
  708. {
  709. _ticks += _chrono.ElapsedTicks;
  710. _chrono.Restart();
  711. if (Device.WaitFifo())
  712. {
  713. Device.Statistics.RecordFifoStart();
  714. Device.ProcessFrame();
  715. Device.Statistics.RecordFifoEnd();
  716. }
  717. while (Device.ConsumeFrameAvailable())
  718. {
  719. if (!_renderingStarted)
  720. {
  721. _renderingStarted = true;
  722. _viewModel.SwitchToRenderer(false);
  723. }
  724. Device.PresentFrame(() => (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
  725. }
  726. if (_ticks >= _ticksPerFrame)
  727. {
  728. UpdateStatus();
  729. }
  730. }
  731. });
  732. (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
  733. }
  734. public void UpdateStatus()
  735. {
  736. // 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.
  737. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
  738. if (GraphicsConfig.ResScale != 1)
  739. {
  740. dockedMode += $" ({GraphicsConfig.ResScale}x)";
  741. }
  742. StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
  743. Device.EnableDeviceVsync,
  744. LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
  745. ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL",
  746. dockedMode,
  747. ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
  748. LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
  749. $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
  750. $"GPU: {_renderer.GetHardwareInfo().GpuVendor}"));
  751. }
  752. public async Task ShowExitPrompt()
  753. {
  754. bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
  755. if (!shouldExit)
  756. {
  757. if (_dialogShown)
  758. {
  759. return;
  760. }
  761. _dialogShown = true;
  762. shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
  763. _dialogShown = false;
  764. }
  765. if (shouldExit)
  766. {
  767. Stop();
  768. }
  769. }
  770. private bool UpdateFrame()
  771. {
  772. if (!_isActive)
  773. {
  774. return false;
  775. }
  776. NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
  777. if (_viewModel.IsActive)
  778. {
  779. if (ConfigurationState.Instance.Hid.EnableMouse)
  780. {
  781. if (_isCursorInRenderer)
  782. {
  783. HideCursor();
  784. }
  785. else
  786. {
  787. ShowCursor();
  788. }
  789. }
  790. else
  791. {
  792. if (ConfigurationState.Instance.HideCursorOnIdle)
  793. {
  794. if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
  795. {
  796. HideCursor();
  797. }
  798. else
  799. {
  800. ShowCursor();
  801. }
  802. }
  803. }
  804. Dispatcher.UIThread.Post(() =>
  805. {
  806. if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
  807. {
  808. Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
  809. }
  810. });
  811. KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
  812. if (currentHotkeyState != _prevHotkeyState)
  813. {
  814. switch (currentHotkeyState)
  815. {
  816. case KeyboardHotkeyState.ToggleVSync:
  817. Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
  818. break;
  819. case KeyboardHotkeyState.Screenshot:
  820. ScreenshotRequested = true;
  821. break;
  822. case KeyboardHotkeyState.ShowUi:
  823. _viewModel.ShowMenuAndStatusBar = true;
  824. break;
  825. case KeyboardHotkeyState.Pause:
  826. if (_viewModel.IsPaused)
  827. {
  828. Resume();
  829. }
  830. else
  831. {
  832. Pause();
  833. }
  834. break;
  835. case KeyboardHotkeyState.ToggleMute:
  836. if (Device.IsAudioMuted())
  837. {
  838. Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
  839. }
  840. else
  841. {
  842. Device.SetVolume(0);
  843. }
  844. _viewModel.Volume = Device.GetVolume();
  845. break;
  846. case KeyboardHotkeyState.ResScaleUp:
  847. GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
  848. break;
  849. case KeyboardHotkeyState.ResScaleDown:
  850. GraphicsConfig.ResScale =
  851. (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
  852. break;
  853. case KeyboardHotkeyState.VolumeUp:
  854. _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
  855. Device.SetVolume(_newVolume);
  856. _viewModel.Volume = Device.GetVolume();
  857. break;
  858. case KeyboardHotkeyState.VolumeDown:
  859. _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
  860. Device.SetVolume(_newVolume);
  861. _viewModel.Volume = Device.GetVolume();
  862. break;
  863. case KeyboardHotkeyState.None:
  864. (_keyboardInterface as AvaloniaKeyboard).Clear();
  865. break;
  866. }
  867. }
  868. _prevHotkeyState = currentHotkeyState;
  869. if (ScreenshotRequested)
  870. {
  871. ScreenshotRequested = false;
  872. _renderer.Screenshot();
  873. }
  874. }
  875. // Touchscreen.
  876. bool hasTouch = false;
  877. if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
  878. {
  879. hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
  880. }
  881. if (!hasTouch)
  882. {
  883. Device.Hid.Touchscreen.Update();
  884. }
  885. Device.Hid.DebugPad.Update();
  886. return true;
  887. }
  888. private KeyboardHotkeyState GetHotkeyState()
  889. {
  890. KeyboardHotkeyState state = KeyboardHotkeyState.None;
  891. if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
  892. {
  893. state = KeyboardHotkeyState.ToggleVSync;
  894. }
  895. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
  896. {
  897. state = KeyboardHotkeyState.Screenshot;
  898. }
  899. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi))
  900. {
  901. state = KeyboardHotkeyState.ShowUi;
  902. }
  903. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
  904. {
  905. state = KeyboardHotkeyState.Pause;
  906. }
  907. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
  908. {
  909. state = KeyboardHotkeyState.ToggleMute;
  910. }
  911. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
  912. {
  913. state = KeyboardHotkeyState.ResScaleUp;
  914. }
  915. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
  916. {
  917. state = KeyboardHotkeyState.ResScaleDown;
  918. }
  919. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
  920. {
  921. state = KeyboardHotkeyState.VolumeUp;
  922. }
  923. else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
  924. {
  925. state = KeyboardHotkeyState.VolumeDown;
  926. }
  927. return state;
  928. }
  929. }
  930. }