AppHost.cs 44 KB

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