aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/AppHost.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Ryujinx.Ava/AppHost.cs')
-rw-r--r--Ryujinx.Ava/AppHost.cs1056
1 files changed, 1056 insertions, 0 deletions
diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs
new file mode 100644
index 00000000..30d72580
--- /dev/null
+++ b/Ryujinx.Ava/AppHost.cs
@@ -0,0 +1,1056 @@
+using ARMeilleure.Translation;
+using ARMeilleure.Translation.PTC;
+using Avalonia.Input;
+using Avalonia.Threading;
+using LibHac.Tools.FsSystem;
+using Ryujinx.Audio.Backends.Dummy;
+using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Audio.Backends.SoundIo;
+using Ryujinx.Audio.Integration;
+using Ryujinx.Ava.Common;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Input;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.System;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Gpu;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.HOS.SystemState;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using Ryujinx.Ui.Common;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Common.Helper;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+using InputManager = Ryujinx.Input.HLE.InputManager;
+using Key = Ryujinx.Input.Key;
+using MouseButton = Ryujinx.Input.MouseButton;
+using Size = Avalonia.Size;
+using Switch = Ryujinx.HLE.Switch;
+using WindowState = Avalonia.Controls.WindowState;
+
+namespace Ryujinx.Ava
+{
+ public class AppHost
+ {
+ private const int CursorHideIdleTime = 8; // Hide Cursor seconds
+
+ private static readonly Cursor InvisibleCursor = new Cursor(StandardCursorType.None);
+
+ private readonly AccountManager _accountManager;
+ private UserChannelPersistence _userChannelPersistence;
+
+ private readonly InputManager _inputManager;
+
+ private readonly IKeyboard _keyboardInterface;
+
+ private readonly MainWindow _parent;
+
+ private readonly GraphicsDebugLevel _glLogLevel;
+
+ private bool _hideCursorOnIdle;
+ private bool _isStopped;
+ private bool _isActive;
+ private long _lastCursorMoveTime;
+
+ private KeyboardHotkeyState _prevHotkeyState;
+
+ private IRenderer _renderer;
+ private readonly Thread _renderingThread;
+
+ private bool _isMouseInClient;
+ private bool _renderingStarted;
+ private bool _dialogShown;
+
+ private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
+ private KeyboardStateSnapshot _lastKeyboardSnapshot;
+
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+
+ public event EventHandler AppExit;
+ public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
+
+ public RendererControl Renderer { get; }
+ public VirtualFileSystem VirtualFileSystem { get; }
+ public ContentManager ContentManager { get; }
+ public Switch Device { get; set; }
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+ public string ApplicationPath { get; private set; }
+
+ private bool _isFirmwareTitle;
+
+ public bool ScreenshotRequested { get; set; }
+
+ private object _lockObject = new();
+
+ public AppHost(
+ RendererControl renderer,
+ InputManager inputManager,
+ string applicationPath,
+ VirtualFileSystem virtualFileSystem,
+ ContentManager contentManager,
+ AccountManager accountManager,
+ UserChannelPersistence userChannelPersistence,
+ MainWindow parent)
+ {
+ _parent = parent;
+ _inputManager = inputManager;
+ _accountManager = accountManager;
+ _userChannelPersistence = userChannelPersistence;
+ _renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" };
+ _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
+ _inputManager.SetMouseDriver(new AvaloniaMouseDriver(renderer));
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+ _lastKeyboardSnapshot = _keyboardInterface.GetKeyboardStateSnapshot();
+
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ Renderer = renderer;
+ ApplicationPath = applicationPath;
+ VirtualFileSystem = virtualFileSystem;
+ ContentManager = contentManager;
+
+ if (ApplicationPath.StartsWith("@SystemContent"))
+ {
+ ApplicationPath = _parent.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
+
+ _isFirmwareTitle = true;
+ }
+
+ ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
+
+ _parent.PointerEnter += Parent_PointerEntered;
+ _parent.PointerLeave += Parent_PointerLeft;
+ _parent.PointerMoved += Parent_PointerMoved;
+
+ ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
+ ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
+ ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
+
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+ }
+
+ private void Parent_PointerMoved(object sender, PointerEventArgs e)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+
+ private void Parent_PointerLeft(object sender, PointerEventArgs e)
+ {
+ Renderer.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? InvisibleCursor : Cursor.Default;
+
+ _isMouseInClient = false;
+ }
+
+ private void Parent_PointerEntered(object sender, PointerEventArgs e)
+ {
+ _isMouseInClient = true;
+ }
+
+ private void SetRendererWindowSize(Size size)
+ {
+ if (_renderer != null)
+ {
+ double scale = Program.WindowScaleFactor;
+ _renderer.Window.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
+ }
+ }
+
+ private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
+ {
+ if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
+ {
+ Task.Run(() =>
+ {
+ lock (_lockObject)
+ {
+ var currentTime = DateTime.Now;
+ string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
+ string directory = AppDataManager.Mode switch
+ {
+ AppDataManager.LaunchMode.Portable => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
+ _ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx")
+ };
+
+ string path = Path.Combine(directory, filename);
+
+ try
+ {
+ Directory.CreateDirectory(directory);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
+
+ return;
+ }
+
+ Image image = e.IsBgra ? Image.LoadPixelData<Bgra32>(e.Data, e.Width, e.Height)
+ : Image.LoadPixelData<Rgba32>(e.Data, e.Width, e.Height);
+
+ if (e.FlipX)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Horizontal));
+ }
+
+ if (e.FlipY)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Vertical));
+ }
+
+ image.SaveAsPng(path, new PngEncoder()
+ {
+ ColorType = PngColorType.Rgb
+ });
+
+ image.Dispose();
+
+ Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
+ }
+ });
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
+ }
+ }
+
+ public void Start()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
+ }
+
+ DisplaySleep.Prevent();
+
+ NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ TouchScreenManager.Initialize(Device);
+
+ _parent.ViewModel.IsGameRunning = true;
+
+ string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName)
+ ? string.Empty
+ : $" - {Device.Application.TitleName}";
+
+ string titleVersionSection = string.IsNullOrWhiteSpace(Device.Application.DisplayVersion)
+ ? string.Empty
+ : $" v{Device.Application.DisplayVersion}";
+
+ string titleIdSection = string.IsNullOrWhiteSpace(Device.Application.TitleIdText)
+ ? string.Empty
+ : $" ({Device.Application.TitleIdText.ToUpper()})";
+
+ string titleArchSection = Device.Application.TitleIs64Bit
+ ? " (64-bit)"
+ : " (32-bit)";
+
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ _parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
+ });
+
+ _parent.ViewModel.HandleShaderProgress(Device);
+
+ Renderer.SizeChanged += Window_SizeChanged;
+
+ _isActive = true;
+
+ _renderingThread.Start();
+
+ _parent.ViewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
+
+ MainLoop();
+
+ Exit();
+ }
+
+ private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
+ {
+ if (Device != null)
+ {
+ Device.Configuration.IgnoreMissingServices = args.NewValue;
+ }
+ }
+
+ private void UpdateAspectRatioState(object sender, ReactiveEventArgs<AspectRatio> args)
+ {
+ if (Device != null)
+ {
+ Device.Configuration.AspectRatio = args.NewValue;
+ }
+ }
+
+ private void UpdateDockedModeState(object sender, ReactiveEventArgs<bool> e)
+ {
+ Device?.System.ChangeDockedModeState(e.NewValue);
+ }
+
+ private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
+ {
+ Device?.SetVolume(e.NewValue);
+ Dispatcher.UIThread.Post(() =>
+ {
+ var value = e.NewValue;
+ _parent.ViewModel.Volume = e.NewValue;
+ });
+ }
+
+ public void Stop()
+ {
+ _isActive = false;
+ }
+
+ private void Exit()
+ {
+ (_keyboardInterface as AvaloniaKeyboard)?.Clear();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _isStopped = true;
+ _isActive = false;
+ }
+
+ public void DisposeContext()
+ {
+ Dispose();
+
+ _isActive = false;
+
+ _renderingThread.Join();
+
+ DisplaySleep.Restore();
+
+ Ptc.Close();
+ PtcProfiler.Stop();
+ NpadManager.Dispose();
+ TouchScreenManager.Dispose();
+ Device.Dispose();
+
+ DisposeGpu();
+
+ AppExit?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Dispose()
+ {
+ if (Device.Application != null)
+ {
+ _parent.UpdateGameMetadata(Device.Application.TitleIdText);
+ }
+
+ ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
+ ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
+
+ _gpuCancellationTokenSource.Cancel();
+ _gpuCancellationTokenSource.Dispose();
+ }
+
+ public void DisposeGpu()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution?.Dispose();
+ _windowsMultimediaTimerResolution = null;
+ }
+
+ Renderer?.MakeCurrent();
+
+ Device.DisposeGpu();
+
+ Renderer?.DestroyBackgroundContext();
+ Renderer?.MakeCurrent(null);
+ }
+
+ private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
+ {
+ Dispatcher.UIThread.InvokeAsync(delegate
+ {
+ _hideCursorOnIdle = state.NewValue;
+
+ if (_hideCursorOnIdle)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+ else
+ {
+ _parent.Cursor = Cursor.Default;
+ }
+ });
+ }
+
+ public async Task<bool> LoadGuestApplication()
+ {
+ InitializeSwitchInstance();
+
+ MainWindow.UpdateGraphicsConfig();
+
+ SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
+
+ if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
+ {
+ if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
+ {
+ if (userError == UserError.NoFirmware)
+ {
+ string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedMessage"], firmwareVersion.VersionString);
+
+ UserResult result = await ContentDialogHelper.CreateConfirmationDialog(_parent,
+ LocaleManager.Instance["DialogFirmwareNoFirmwareInstalledMessage"], message, LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
+
+ if (result != UserResult.Yes)
+ {
+ Dispatcher.UIThread.Post(async () => await
+ UserErrorDialog.ShowUserErrorDialog(userError, _parent));
+ Device.Dispose();
+
+ return false;
+ }
+ }
+
+ if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
+ {
+ Dispatcher.UIThread.Post(async () => await
+ UserErrorDialog.ShowUserErrorDialog(userError, _parent));
+ Device.Dispose();
+
+ return false;
+ }
+
+ // Tell the user that we installed a firmware for them.
+ if (userError == UserError.NoFirmware)
+ {
+ firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
+
+ _parent.RefreshFirmwareStatus();
+
+ string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedSuccessMessage"], firmwareVersion.VersionString);
+
+ await ContentDialogHelper.CreateInfoDialog(_parent,
+ string.Format(LocaleManager.Instance["DialogFirmwareInstalledMessage"], firmwareVersion.VersionString),
+ message,
+ LocaleManager.Instance["InputDialogOk"],
+ "",
+ LocaleManager.Instance["RyujinxInfo"]);
+ }
+ }
+ else
+ {
+ Dispatcher.UIThread.Post(async () => await
+ UserErrorDialog.ShowUserErrorDialog(userError, _parent));
+
+ Device.Dispose();
+
+ return false;
+ }
+ }
+
+ Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
+
+ if (_isFirmwareTitle)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
+
+ Device.LoadNca(ApplicationPath);
+ }
+ else if (Directory.Exists(ApplicationPath))
+ {
+ string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage");
+
+ if (romFsFiles.Length == 0)
+ {
+ romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
+ }
+
+ if (romFsFiles.Length > 0)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
+
+ Device.LoadCart(ApplicationPath, romFsFiles[0]);
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
+
+ Device.LoadCart(ApplicationPath);
+ }
+ }
+ else if (File.Exists(ApplicationPath))
+ {
+ switch (System.IO.Path.GetExtension(ApplicationPath).ToLowerInvariant())
+ {
+ case ".xci":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
+
+ Device.LoadXci(ApplicationPath);
+
+ break;
+ }
+ case ".nca":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
+
+ Device.LoadNca(ApplicationPath);
+
+ break;
+ }
+ case ".nsp":
+ case ".pfs0":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
+
+ Device.LoadNsp(ApplicationPath);
+
+ break;
+ }
+ default:
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as homebrew.");
+
+ try
+ {
+ Device.LoadProgram(ApplicationPath);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
+
+ Dispose();
+
+ return false;
+ }
+
+ break;
+ }
+ }
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
+
+ Dispose();
+
+ return false;
+ }
+
+ DiscordIntegrationModule.SwitchToPlayingState(Device.Application.TitleIdText, Device.Application.TitleName);
+
+ _parent.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata =>
+ {
+ appMetadata.LastPlayed = DateTime.UtcNow.ToString();
+ });
+
+ return true;
+ }
+
+ internal void Resume()
+ {
+ Device?.System.TogglePauseEmulation(false);
+ _parent.ViewModel.IsPaused = false;
+ }
+
+ internal void Pause()
+ {
+ Device?.System.TogglePauseEmulation(true);
+ _parent.ViewModel.IsPaused = true;
+ }
+
+ private void InitializeSwitchInstance()
+ {
+ VirtualFileSystem.ReloadKeySet();
+
+ IRenderer renderer = new Renderer();
+ IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver();
+
+ BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
+
+ var isGALthreaded = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
+
+ if (isGALthreaded)
+ {
+ renderer = new ThreadedRenderer(renderer);
+ }
+
+ Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {isGALthreaded}");
+
+ if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SDL2)
+ {
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL.");
+
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SoundIO.");
+
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+ else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo)
+ {
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, trying to fall back to SDL2.");
+
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL.");
+
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+ else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.OpenAl)
+ {
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SDL2.");
+
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to SoundIO.");
+
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
+ MainWindow.SaveConfig();
+
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+
+ var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value
+ ? HLE.MemoryConfiguration.MemoryConfiguration6GB
+ : HLE.MemoryConfiguration.MemoryConfiguration4GB;
+
+ IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
+
+ HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(VirtualFileSystem,
+ _parent.LibHacHorizonManager,
+ ContentManager,
+ _accountManager,
+ _userChannelPersistence,
+ renderer,
+ deviceDriver,
+ memoryConfiguration,
+ _parent.UiHandler,
+ (SystemLanguage)ConfigurationState.Instance.System.Language.Value,
+ (RegionCode)ConfigurationState.Instance.System.Region.Value,
+ ConfigurationState.Instance.Graphics.EnableVsync,
+ ConfigurationState.Instance.System.EnableDockedMode,
+ ConfigurationState.Instance.System.EnablePtc,
+ ConfigurationState.Instance.System.EnableInternetAccess,
+ fsIntegrityCheckLevel,
+ ConfigurationState.Instance.System.FsGlobalAccessLogMode,
+ ConfigurationState.Instance.System.SystemTimeOffset,
+ ConfigurationState.Instance.System.TimeZone,
+ ConfigurationState.Instance.System.MemoryManagerMode,
+ ConfigurationState.Instance.System.IgnoreMissingServices,
+ ConfigurationState.Instance.Graphics.AspectRatio,
+ ConfigurationState.Instance.System.AudioVolume);
+
+ Device = new Switch(configuration);
+ }
+
+ private void Window_SizeChanged(object sender, Size e)
+ {
+ Width = (int)e.Width;
+ Height = (int)e.Height;
+
+ SetRendererWindowSize(e);
+ }
+
+ private void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ // Polling becomes expensive if it's not slept
+ Thread.Sleep(1);
+ }
+ }
+
+ private unsafe void RenderLoop()
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (_parent.ViewModel.StartGamesInFullscreen)
+ {
+ _parent.WindowState = WindowState.FullScreen;
+ }
+
+ if (_parent.WindowState == WindowState.FullScreen)
+ {
+ _parent.ViewModel.ShowMenuAndStatusBar = false;
+ }
+ });
+
+ IRenderer renderer = Device.Gpu.Renderer;
+
+ if (renderer is ThreadedRenderer tr)
+ {
+ renderer = tr.BaseRenderer;
+ }
+
+ _renderer = renderer;
+
+ _renderer.ScreenCaptured += Renderer_ScreenCaptured;
+
+ (_renderer as Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GameContext));
+
+ Renderer.MakeCurrent();
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ Width = (int)Renderer.Bounds.Width;
+ Height = (int)Renderer.Bounds.Height;
+
+ _renderer.Window.SetSize((int)(Width * Program.WindowScaleFactor), (int)(Height * Program.WindowScaleFactor));
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+ Translator.IsReadyForTranslation.Set();
+
+ Renderer.Start();
+
+ Renderer.QueueRender();
+
+ while (_isActive)
+ {
+ if (Device.WaitFifo())
+ {
+ Device.Statistics.RecordFifoStart();
+ Device.ProcessFrame();
+ Device.Statistics.RecordFifoEnd();
+ }
+
+ while (Device.ConsumeFrameAvailable())
+ {
+ if (!_renderingStarted)
+ {
+ _renderingStarted = true;
+ _parent.SwitchToGameControl();
+ }
+
+ Device.PresentFrame(Present);
+ }
+ }
+
+ Renderer.Stop();
+ });
+
+ Renderer?.MakeCurrent(null);
+
+ Renderer.SizeChanged -= Window_SizeChanged;
+ }
+
+ private void Present(object image)
+ {
+ // 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
+ string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance["Docked"] : LocaleManager.Instance["Handheld"];
+ float scale = GraphicsConfig.ResScale;
+
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
+
+ string vendor = _renderer is Renderer renderer ? renderer.GpuVendor : "";
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ Device.GetVolume(),
+ dockedMode,
+ ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
+ LocaleManager.Instance["Game"] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
+ $"GPU: {vendor}"));
+
+ Renderer.Present(image);
+ }
+
+ public async Task ShowExitPrompt()
+ {
+ bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
+
+ if (!shouldExit)
+ {
+ if (_dialogShown)
+ {
+ return;
+ }
+
+ _dialogShown = true;
+ shouldExit = await ContentDialogHelper.CreateStopEmulationDialog(_parent);
+
+ _dialogShown = false;
+ }
+
+ if (shouldExit)
+ {
+ Stop();
+ }
+ }
+
+ private void HandleScreenState(KeyboardStateSnapshot keyboard, KeyboardStateSnapshot lastKeyboard)
+ {
+ if (ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ if (_isMouseInClient)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _parent.Cursor = InvisibleCursor;
+ });
+ }
+ }
+ else
+ {
+ if (_hideCursorOnIdle)
+ {
+ long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _parent.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
+ });
+ }
+ }
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return false;
+ }
+
+ if (_parent.IsActive)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
+
+ HandleScreenState(keyboard, _lastKeyboardSnapshot);
+
+ if (keyboard.IsPressed(Key.Delete))
+ {
+ if (_parent.WindowState != WindowState.FullScreen)
+ {
+ Ptc.Continue();
+ }
+ }
+
+ _lastKeyboardSnapshot = keyboard;
+ });
+ }
+
+ NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+
+ if (_parent.IsActive)
+ {
+ KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
+
+ if (currentHotkeyState != _prevHotkeyState)
+ {
+ switch (currentHotkeyState)
+ {
+ case KeyboardHotkeyState.ToggleVSync:
+ Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
+ break;
+ case KeyboardHotkeyState.Screenshot:
+ ScreenshotRequested = true;
+ break;
+ case KeyboardHotkeyState.ShowUi:
+ _parent.ViewModel.ShowMenuAndStatusBar = true;
+ break;
+ case KeyboardHotkeyState.Pause:
+ if (_parent.ViewModel.IsPaused)
+ {
+ Resume();
+ }
+ else
+ {
+ Pause();
+ }
+ break;
+ case KeyboardHotkeyState.ToggleMute:
+ if (Device.IsAudioMuted())
+ {
+ Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+ }
+ else
+ {
+ Device.SetVolume(0);
+ }
+
+ _parent.ViewModel.Volume = Device.GetVolume();
+ break;
+ case KeyboardHotkeyState.None:
+ (_keyboardInterface as AvaloniaKeyboard).Clear();
+ break;
+ }
+ }
+
+ _prevHotkeyState = currentHotkeyState;
+
+ if (ScreenshotRequested)
+ {
+ ScreenshotRequested = false;
+ _renderer.Screenshot();
+ }
+ }
+
+ // Touchscreen
+ bool hasTouch = false;
+
+ if (_parent.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ Device.Hid.Touchscreen.Update();
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ return true;
+ }
+
+ private KeyboardHotkeyState GetHotkeyState()
+ {
+ KeyboardHotkeyState state = KeyboardHotkeyState.None;
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
+ {
+ state = KeyboardHotkeyState.ToggleVSync;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
+ {
+ state = KeyboardHotkeyState.Screenshot;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi))
+ {
+ state = KeyboardHotkeyState.ShowUi;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
+ {
+ state = KeyboardHotkeyState.Pause;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
+ {
+ state = KeyboardHotkeyState.ToggleMute;
+ }
+
+ return state;
+ }
+ }
+} \ No newline at end of file