AppHost.cs 46 KB

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