AppHost.cs 47 KB

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