mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-08-07 10:48:28 +02:00

* Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Apply suggestions from code review Co-authored-by: gdkchan <gab.dark.100@gmail.com> * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again * Don't add duplicates to update/dlc windows * Avoid double lookup * Preserve DLC enabled state for bundled DLCs * Fix DLCWindow not opening using GTK * Fix missing information when loading applications from file * Address review feedback Rename ContentCollection to ContentMetaData Fix casing issues in log messages Use null as the default value for updatePath * Fix re-adding bundled DLCs every time * Fix bundled DLCs disappearing * Abstract common code to open application pfs * Remove unused imports * Fix file exists check when loading DLCs * Load bundled DLCs only using dlc.json * Load AoC items correctly * Add all DLCs from a PFS * Add argument to launch a specific application id * Use application-id argument for shortcuts if necessary * Return the application id from the control NCA if possible * GetApplicationInformation: Don't overwrite application ids Move SaveDataOwnerId check to the top, since it seems to be more reliable. * Get application ids from CNMT again This commit reverts some parts of 61615b8f0d6f90ae86778958ddc38eaf6dc280ab. Since the issue wasn't actually related to the application id in CMNTs, we can remove the wrong assumptions. * Revert erroneous axaml change from adca8900 * Rename title to application * Wrap nsp/pfs0 case with curly braces * Check if _applicationData.ControlHolder.ByteSpan is zeros only once * Catch exceptions while loading applications from nsps --------- Co-authored-by: gdkchan <gab.dark.100@gmail.com>
1289 lines
50 KiB
C#
1289 lines
50 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
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.Helpers;
|
|
using Ryujinx.Ava.UI.Models;
|
|
using Ryujinx.Ava.UI.Renderer;
|
|
using Ryujinx.Ava.UI.ViewModels;
|
|
using Ryujinx.Ava.UI.Windows;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Configuration;
|
|
using Ryujinx.Common.Configuration.Multiplayer;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Common.SystemInterop;
|
|
using Ryujinx.Common.Utilities;
|
|
using Ryujinx.Graphics.GAL;
|
|
using Ryujinx.Graphics.GAL.Multithreading;
|
|
using Ryujinx.Graphics.Gpu;
|
|
using Ryujinx.Graphics.OpenGL;
|
|
using Ryujinx.Graphics.Vulkan;
|
|
using Ryujinx.HLE;
|
|
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.App.Common;
|
|
using Ryujinx.UI.Common;
|
|
using Ryujinx.UI.Common.Configuration;
|
|
using Ryujinx.UI.Common.Helper;
|
|
using Silk.NET.Vulkan;
|
|
using SkiaSharp;
|
|
using SPB.Graphics.Vulkan;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
|
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
|
using InputManager = Ryujinx.Input.HLE.InputManager;
|
|
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
|
|
using Key = Ryujinx.Input.Key;
|
|
using MouseButton = Ryujinx.Input.MouseButton;
|
|
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
|
using Size = Avalonia.Size;
|
|
using Switch = Ryujinx.HLE.Switch;
|
|
|
|
namespace Ryujinx.Ava
|
|
{
|
|
internal class AppHost
|
|
{
|
|
private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
|
|
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
|
private const int TargetFps = 60;
|
|
private const float VolumeDelta = 0.05f;
|
|
|
|
private static readonly Cursor _invisibleCursor = new(StandardCursorType.None);
|
|
private readonly IntPtr _invisibleCursorWin;
|
|
private readonly IntPtr _defaultCursorWin;
|
|
|
|
private readonly long _ticksPerFrame;
|
|
private readonly Stopwatch _chrono;
|
|
private long _ticks;
|
|
|
|
private readonly AccountManager _accountManager;
|
|
private readonly UserChannelPersistence _userChannelPersistence;
|
|
private readonly InputManager _inputManager;
|
|
|
|
private readonly MainWindowViewModel _viewModel;
|
|
private readonly IKeyboard _keyboardInterface;
|
|
private readonly TopLevel _topLevel;
|
|
public RendererHost RendererHost;
|
|
|
|
private readonly GraphicsDebugLevel _glLogLevel;
|
|
private float _newVolume;
|
|
private KeyboardHotkeyState _prevHotkeyState;
|
|
|
|
private long _lastCursorMoveTime;
|
|
private bool _isCursorInRenderer = true;
|
|
private bool _ignoreCursorState = false;
|
|
|
|
private enum CursorStates
|
|
{
|
|
CursorIsHidden,
|
|
CursorIsVisible,
|
|
ForceChangeCursor
|
|
};
|
|
|
|
private CursorStates _cursorState = !ConfigurationState.Instance.Hid.EnableMouse.Value ?
|
|
CursorStates.CursorIsVisible : CursorStates.CursorIsHidden;
|
|
|
|
private bool _isStopped;
|
|
private bool _isActive;
|
|
private bool _renderingStarted;
|
|
|
|
private readonly ManualResetEvent _gpuDoneEvent;
|
|
|
|
private IRenderer _renderer;
|
|
private readonly Thread _renderingThread;
|
|
private readonly CancellationTokenSource _gpuCancellationTokenSource;
|
|
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
|
|
|
|
private bool _dialogShown;
|
|
private readonly bool _isFirmwareTitle;
|
|
|
|
private readonly object _lockObject = new();
|
|
|
|
public event EventHandler AppExit;
|
|
public event EventHandler<StatusInitEventArgs> StatusInitEvent;
|
|
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
|
|
|
public VirtualFileSystem VirtualFileSystem { get; }
|
|
public ContentManager ContentManager { get; }
|
|
public NpadManager NpadManager { get; }
|
|
public TouchScreenManager TouchScreenManager { get; }
|
|
public Switch Device { get; set; }
|
|
|
|
public int Width { get; private set; }
|
|
public int Height { get; private set; }
|
|
public string ApplicationPath { get; private set; }
|
|
public ulong ApplicationId { get; private set; }
|
|
public bool ScreenshotRequested { get; set; }
|
|
|
|
public AppHost(
|
|
RendererHost renderer,
|
|
InputManager inputManager,
|
|
string applicationPath,
|
|
ulong applicationId,
|
|
VirtualFileSystem virtualFileSystem,
|
|
ContentManager contentManager,
|
|
AccountManager accountManager,
|
|
UserChannelPersistence userChannelPersistence,
|
|
MainWindowViewModel viewmodel,
|
|
TopLevel topLevel)
|
|
{
|
|
_viewModel = viewmodel;
|
|
_inputManager = inputManager;
|
|
_accountManager = accountManager;
|
|
_userChannelPersistence = userChannelPersistence;
|
|
_renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" };
|
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
|
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
|
|
_topLevel = topLevel;
|
|
|
|
_inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
|
|
|
|
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
|
|
|
|
NpadManager = _inputManager.CreateNpadManager();
|
|
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
|
ApplicationPath = applicationPath;
|
|
ApplicationId = applicationId;
|
|
VirtualFileSystem = virtualFileSystem;
|
|
ContentManager = contentManager;
|
|
|
|
RendererHost = renderer;
|
|
|
|
_chrono = new Stopwatch();
|
|
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
|
|
|
|
if (ApplicationPath.StartsWith("@SystemContent"))
|
|
{
|
|
ApplicationPath = VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
|
|
|
|
_isFirmwareTitle = true;
|
|
}
|
|
|
|
ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed;
|
|
|
|
_topLevel.PointerMoved += TopLevel_PointerEnteredOrMoved;
|
|
_topLevel.PointerEntered += TopLevel_PointerEnteredOrMoved;
|
|
_topLevel.PointerExited += TopLevel_PointerExited;
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
_invisibleCursorWin = CreateEmptyCursor();
|
|
_defaultCursorWin = CreateArrowCursor();
|
|
}
|
|
|
|
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
|
|
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
|
|
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
|
|
ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
|
|
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
|
|
ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
|
|
ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAntiAliasing;
|
|
ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
|
|
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
|
|
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
|
|
|
|
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
|
|
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
|
|
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
|
|
|
|
_gpuCancellationTokenSource = new CancellationTokenSource();
|
|
_gpuDoneEvent = new ManualResetEvent(false);
|
|
}
|
|
|
|
private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e)
|
|
{
|
|
if (!_viewModel.IsActive)
|
|
{
|
|
_isCursorInRenderer = false;
|
|
_ignoreCursorState = false;
|
|
return;
|
|
}
|
|
|
|
if (sender is MainWindow window)
|
|
{
|
|
if (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle)
|
|
{
|
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
|
}
|
|
|
|
var point = e.GetCurrentPoint(window).Position;
|
|
var bounds = RendererHost.EmbeddedWindow.Bounds;
|
|
var windowYOffset = bounds.Y + window.MenuBarHeight;
|
|
var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1;
|
|
|
|
if (!_viewModel.ShowMenuAndStatusBar)
|
|
{
|
|
windowYOffset -= window.MenuBarHeight;
|
|
windowYLimit += window.StatusBarHeight + 1;
|
|
}
|
|
|
|
_isCursorInRenderer = point.X >= bounds.X &&
|
|
Math.Ceiling(point.X) <= (int)window.Bounds.Width &&
|
|
point.Y >= windowYOffset &&
|
|
point.Y <= windowYLimit &&
|
|
!_viewModel.IsSubMenuOpen;
|
|
|
|
_ignoreCursorState = false;
|
|
}
|
|
}
|
|
|
|
private void TopLevel_PointerExited(object sender, PointerEventArgs e)
|
|
{
|
|
_isCursorInRenderer = false;
|
|
|
|
if (sender is MainWindow window)
|
|
{
|
|
var point = e.GetCurrentPoint(window).Position;
|
|
var bounds = RendererHost.EmbeddedWindow.Bounds;
|
|
var windowYOffset = bounds.Y + window.MenuBarHeight;
|
|
var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1;
|
|
|
|
if (!_viewModel.ShowMenuAndStatusBar)
|
|
{
|
|
windowYOffset -= window.MenuBarHeight;
|
|
windowYLimit += window.StatusBarHeight + 1;
|
|
}
|
|
|
|
_ignoreCursorState = (point.X == bounds.X ||
|
|
Math.Ceiling(point.X) == (int)window.Bounds.Width) &&
|
|
point.Y >= windowYOffset &&
|
|
point.Y <= windowYLimit;
|
|
}
|
|
|
|
_cursorState = CursorStates.ForceChangeCursor;
|
|
}
|
|
|
|
private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
|
|
{
|
|
_renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
|
|
_renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
|
|
}
|
|
|
|
private void UpdateScalingFilter(object sender, ReactiveEventArgs<ScalingFilter> e)
|
|
{
|
|
_renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
|
|
_renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
|
|
}
|
|
|
|
private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
_renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
|
|
}
|
|
|
|
private void ShowCursor()
|
|
{
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
_viewModel.Cursor = Cursor.Default;
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
if (_cursorState != CursorStates.CursorIsHidden && !_ignoreCursorState)
|
|
{
|
|
SetCursor(_defaultCursorWin);
|
|
}
|
|
}
|
|
});
|
|
|
|
_cursorState = CursorStates.CursorIsVisible;
|
|
}
|
|
|
|
private void HideCursor()
|
|
{
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
_viewModel.Cursor = _invisibleCursor;
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
SetCursor(_invisibleCursorWin);
|
|
}
|
|
});
|
|
|
|
_cursorState = CursorStates.CursorIsHidden;
|
|
}
|
|
|
|
private void SetRendererWindowSize(Size size)
|
|
{
|
|
if (_renderer != null)
|
|
{
|
|
double scale = _topLevel.RenderScaling;
|
|
|
|
_renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
|
|
}
|
|
}
|
|
|
|
private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
|
|
{
|
|
if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
lock (_lockObject)
|
|
{
|
|
string applicationName = Device.Processes.ActiveApplication.Name;
|
|
string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName);
|
|
DateTime currentTime = DateTime.Now;
|
|
|
|
string filename = $"{sanitizedApplicationName}_{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 or AppDataManager.LaunchMode.Custom => 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;
|
|
}
|
|
|
|
var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
|
|
using var bitmap = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
|
|
|
|
Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length);
|
|
|
|
SKBitmap bitmapToSave = null;
|
|
|
|
if (e.FlipX || e.FlipY)
|
|
{
|
|
bitmapToSave = new SKBitmap(bitmap.Width, bitmap.Height);
|
|
|
|
using var canvas = new SKCanvas(bitmapToSave);
|
|
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
float scaleX = e.FlipX ? -1 : 1;
|
|
float scaleY = e.FlipY ? -1 : 1;
|
|
|
|
var matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f);
|
|
|
|
canvas.SetMatrix(matrix);
|
|
|
|
canvas.DrawBitmap(bitmap, new SKPoint(e.FlipX ? -bitmap.Width : 0, e.FlipY ? -bitmap.Height : 0));
|
|
}
|
|
|
|
SaveBitmapAsPng(bitmapToSave ?? bitmap, path);
|
|
bitmapToSave?.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");
|
|
}
|
|
}
|
|
|
|
private void SaveBitmapAsPng(SKBitmap bitmap, string path)
|
|
{
|
|
using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100);
|
|
using var stream = File.OpenWrite(path);
|
|
|
|
data.SaveTo(stream);
|
|
}
|
|
|
|
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);
|
|
|
|
_viewModel.IsGameRunning = true;
|
|
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version);
|
|
});
|
|
|
|
_viewModel.SetUiProgressHandlers(Device);
|
|
|
|
RendererHost.BoundsChanged += Window_BoundsChanged;
|
|
|
|
_isActive = true;
|
|
|
|
_renderingThread.Start();
|
|
|
|
_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 UpdateAntiAliasing(object sender, ReactiveEventArgs<AntiAliasing> e)
|
|
{
|
|
_renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.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(() =>
|
|
{
|
|
_viewModel.Volume = e.NewValue;
|
|
});
|
|
}
|
|
|
|
private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
Device.Configuration.EnableInternetAccess = e.NewValue;
|
|
}
|
|
|
|
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
|
|
{
|
|
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
|
|
}
|
|
|
|
private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs<MultiplayerMode> e)
|
|
{
|
|
Device.Configuration.MultiplayerMode = e.NewValue;
|
|
}
|
|
|
|
public void ToggleVSync()
|
|
{
|
|
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
|
|
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
|
|
}
|
|
|
|
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;
|
|
|
|
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
|
|
// We only need to wait for all commands submitted during the main gpu loop to be processed.
|
|
_gpuDoneEvent.WaitOne();
|
|
_gpuDoneEvent.Dispose();
|
|
|
|
DisplaySleep.Restore();
|
|
|
|
NpadManager.Dispose();
|
|
TouchScreenManager.Dispose();
|
|
Device.Dispose();
|
|
|
|
DisposeGpu();
|
|
|
|
AppExit?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
private void Dispose()
|
|
{
|
|
if (Device.Processes != null)
|
|
{
|
|
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
|
|
}
|
|
|
|
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
|
|
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
|
|
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
|
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
|
|
ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
|
|
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
|
|
ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing;
|
|
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough;
|
|
|
|
_topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved;
|
|
_topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved;
|
|
_topLevel.PointerExited -= TopLevel_PointerExited;
|
|
|
|
_gpuCancellationTokenSource.Cancel();
|
|
_gpuCancellationTokenSource.Dispose();
|
|
|
|
_chrono.Stop();
|
|
}
|
|
|
|
public void DisposeGpu()
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
_windowsMultimediaTimerResolution?.Dispose();
|
|
_windowsMultimediaTimerResolution = null;
|
|
}
|
|
|
|
if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow)
|
|
{
|
|
// Try to bind the OpenGL context before calling the shutdown event.
|
|
openGlWindow.MakeCurrent(false, false);
|
|
|
|
Device.DisposeGpu();
|
|
|
|
// Unbind context and destroy everything.
|
|
openGlWindow.MakeCurrent(true, false);
|
|
}
|
|
else
|
|
{
|
|
Device.DisposeGpu();
|
|
}
|
|
}
|
|
|
|
private void HideCursorState_Changed(object sender, ReactiveEventArgs<HideCursorMode> state)
|
|
{
|
|
if (state.NewValue == HideCursorMode.OnIdle)
|
|
{
|
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
|
}
|
|
|
|
_cursorState = CursorStates.ForceChangeCursor;
|
|
}
|
|
|
|
public async Task<bool> LoadGuestApplication()
|
|
{
|
|
InitializeSwitchInstance();
|
|
MainWindow.UpdateGraphicsConfig();
|
|
|
|
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
|
|
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
|
|
{
|
|
{
|
|
if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
|
|
{
|
|
if (userError == UserError.NoFirmware)
|
|
{
|
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
|
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
|
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
|
"");
|
|
|
|
if (result != UserResult.Yes)
|
|
{
|
|
await UserErrorDialog.ShowUserErrorDialog(userError);
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
|
|
{
|
|
await UserErrorDialog.ShowUserErrorDialog(userError);
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Tell the user that we installed a firmware for them.
|
|
if (userError == UserError.NoFirmware)
|
|
{
|
|
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
|
|
|
_viewModel.RefreshFirmwareStatus();
|
|
|
|
await ContentDialogHelper.CreateInfoDialog(
|
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
|
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
|
|
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
|
"",
|
|
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await UserErrorDialog.ShowUserErrorDialog(userError);
|
|
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).");
|
|
|
|
if (!Device.LoadNca(ApplicationPath))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
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.");
|
|
|
|
if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
|
|
|
|
if (!Device.LoadCart(ApplicationPath))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else if (File.Exists(ApplicationPath))
|
|
{
|
|
switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
|
|
{
|
|
case ".xci":
|
|
{
|
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
|
|
|
if (!Device.LoadXci(ApplicationPath, ApplicationId))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ".nca":
|
|
{
|
|
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
|
|
|
if (!Device.LoadNca(ApplicationPath))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ".nsp":
|
|
case ".pfs0":
|
|
{
|
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
|
|
|
if (!Device.LoadNsp(ApplicationPath, ApplicationId))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
Logger.Info?.Print(LogClass.Application, "Loading as homebrew.");
|
|
|
|
try
|
|
{
|
|
if (!Device.LoadProgram(ApplicationPath))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
|
|
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
|
|
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
|
|
DiscordIntegrationModule.SwitchToPlayingState(Device.Processes.ActiveApplication.ProgramIdText, Device.Processes.ActiveApplication.Name);
|
|
|
|
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
|
{
|
|
appMetadata.UpdatePreGame();
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void Resume()
|
|
{
|
|
Device?.System.TogglePauseEmulation(false);
|
|
|
|
_viewModel.IsPaused = false;
|
|
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
|
|
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
|
|
}
|
|
|
|
internal void Pause()
|
|
{
|
|
Device?.System.TogglePauseEmulation(true);
|
|
|
|
_viewModel.IsPaused = true;
|
|
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
|
|
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
|
|
}
|
|
|
|
private void InitializeSwitchInstance()
|
|
{
|
|
// Initialize KeySet.
|
|
VirtualFileSystem.ReloadKeySet();
|
|
|
|
// Initialize Renderer.
|
|
IRenderer renderer;
|
|
|
|
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
|
|
{
|
|
renderer = new VulkanRenderer(
|
|
Vk.GetApi(),
|
|
(RendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
|
|
VulkanHelper.GetRequiredInstanceExtensions,
|
|
ConfigurationState.Instance.Graphics.PreferredGpu.Value);
|
|
}
|
|
else
|
|
{
|
|
renderer = new OpenGLRenderer();
|
|
}
|
|
|
|
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}");
|
|
|
|
// Initialize Configuration.
|
|
var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB;
|
|
|
|
HLEConfiguration configuration = new(VirtualFileSystem,
|
|
_viewModel.LibHacHorizonManager,
|
|
ContentManager,
|
|
_accountManager,
|
|
_userChannelPersistence,
|
|
renderer,
|
|
InitializeAudio(),
|
|
memoryConfiguration,
|
|
_viewModel.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,
|
|
ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
|
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,
|
|
ConfigurationState.Instance.System.UseHypervisor,
|
|
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
|
|
ConfigurationState.Instance.Multiplayer.Mode);
|
|
|
|
Device = new Switch(configuration);
|
|
}
|
|
|
|
private static IHardwareDeviceDriver InitializeAudio()
|
|
{
|
|
var availableBackends = new List<AudioBackend>
|
|
{
|
|
AudioBackend.SDL2,
|
|
AudioBackend.SoundIo,
|
|
AudioBackend.OpenAl,
|
|
AudioBackend.Dummy,
|
|
};
|
|
|
|
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
|
|
|
|
for (int i = 0; i < availableBackends.Count; i++)
|
|
{
|
|
if (availableBackends[i] == preferredBackend)
|
|
{
|
|
availableBackends.RemoveAt(i);
|
|
availableBackends.Insert(0, preferredBackend);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static IHardwareDeviceDriver InitializeAudioBackend<T>(AudioBackend backend, AudioBackend nextBackend) where T : IHardwareDeviceDriver, new()
|
|
{
|
|
if (T.IsSupported)
|
|
{
|
|
return new T();
|
|
}
|
|
|
|
Logger.Warning?.Print(LogClass.Audio, $"{backend} is not supported, falling back to {nextBackend}.");
|
|
|
|
return null;
|
|
}
|
|
|
|
IHardwareDeviceDriver deviceDriver = null;
|
|
|
|
for (int i = 0; i < availableBackends.Count; i++)
|
|
{
|
|
AudioBackend currentBackend = availableBackends[i];
|
|
AudioBackend nextBackend = i + 1 < availableBackends.Count ? availableBackends[i + 1] : AudioBackend.Dummy;
|
|
|
|
deviceDriver = currentBackend switch
|
|
{
|
|
AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
|
|
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
|
|
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
|
|
_ => new DummyHardwareDeviceDriver(),
|
|
};
|
|
|
|
if (deviceDriver != null)
|
|
{
|
|
ConfigurationState.Instance.System.AudioBackend.Value = currentBackend;
|
|
break;
|
|
}
|
|
}
|
|
|
|
MainWindowViewModel.SaveConfig();
|
|
|
|
return deviceDriver;
|
|
}
|
|
|
|
private void Window_BoundsChanged(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 void RenderLoop()
|
|
{
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
if (_viewModel.StartGamesInFullscreen)
|
|
{
|
|
_viewModel.WindowState = WindowState.FullScreen;
|
|
}
|
|
|
|
if (_viewModel.WindowState == WindowState.FullScreen)
|
|
{
|
|
_viewModel.ShowMenuAndStatusBar = false;
|
|
}
|
|
});
|
|
|
|
_renderer = Device.Gpu.Renderer is ThreadedRenderer tr ? tr.BaseRenderer : Device.Gpu.Renderer;
|
|
|
|
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
|
|
|
|
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
|
|
|
|
Device.Gpu.Renderer.Initialize(_glLogLevel);
|
|
|
|
_renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
|
|
_renderer?.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
|
|
_renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
|
|
_renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
|
|
|
|
Width = (int)RendererHost.Bounds.Width;
|
|
Height = (int)RendererHost.Bounds.Height;
|
|
|
|
_renderer.Window.SetSize((int)(Width * _topLevel.RenderScaling), (int)(Height * _topLevel.RenderScaling));
|
|
|
|
_chrono.Start();
|
|
|
|
Device.Gpu.Renderer.RunLoop(() =>
|
|
{
|
|
Device.Gpu.SetGpuThread();
|
|
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
|
|
|
|
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
|
|
|
|
while (_isActive)
|
|
{
|
|
_ticks += _chrono.ElapsedTicks;
|
|
|
|
_chrono.Restart();
|
|
|
|
if (Device.WaitFifo())
|
|
{
|
|
Device.Statistics.RecordFifoStart();
|
|
Device.ProcessFrame();
|
|
Device.Statistics.RecordFifoEnd();
|
|
}
|
|
|
|
while (Device.ConsumeFrameAvailable())
|
|
{
|
|
if (!_renderingStarted)
|
|
{
|
|
_renderingStarted = true;
|
|
_viewModel.SwitchToRenderer(false);
|
|
InitStatus();
|
|
}
|
|
|
|
Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
|
|
}
|
|
|
|
if (_ticks >= _ticksPerFrame)
|
|
{
|
|
UpdateStatus();
|
|
}
|
|
}
|
|
|
|
// Make sure all commands in the run loop are fully executed before leaving the loop.
|
|
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
|
|
{
|
|
threaded.FlushThreadedCommands();
|
|
}
|
|
|
|
_gpuDoneEvent.Set();
|
|
});
|
|
|
|
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true);
|
|
}
|
|
|
|
public void InitStatus()
|
|
{
|
|
StatusInitEvent?.Invoke(this, new StatusInitEventArgs(
|
|
ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch
|
|
{
|
|
GraphicsBackend.Vulkan => "Vulkan",
|
|
GraphicsBackend.OpenGl => "OpenGL",
|
|
_ => throw new NotImplementedException()
|
|
},
|
|
$"GPU: {_renderer.GetHardwareInfo().GpuDriver}"));
|
|
}
|
|
|
|
public void UpdateStatus()
|
|
{
|
|
// 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[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
|
|
|
|
if (GraphicsConfig.ResScale != 1)
|
|
{
|
|
dockedMode += $" ({GraphicsConfig.ResScale}x)";
|
|
}
|
|
|
|
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
|
|
Device.EnableDeviceVsync,
|
|
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
|
|
dockedMode,
|
|
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
|
|
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
|
|
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %"));
|
|
}
|
|
|
|
public async Task ShowExitPrompt()
|
|
{
|
|
bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
|
|
if (!shouldExit)
|
|
{
|
|
if (_dialogShown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dialogShown = true;
|
|
|
|
shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
|
|
|
|
_dialogShown = false;
|
|
}
|
|
|
|
if (shouldExit)
|
|
{
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
private bool UpdateFrame()
|
|
{
|
|
if (!_isActive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
|
|
|
if (_viewModel.IsActive)
|
|
{
|
|
bool isCursorVisible = true;
|
|
|
|
if (_isCursorInRenderer && !_viewModel.ShowLoadProgress)
|
|
{
|
|
if (ConfigurationState.Instance.Hid.EnableMouse.Value)
|
|
{
|
|
isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never;
|
|
}
|
|
else
|
|
{
|
|
isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never ||
|
|
(ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle &&
|
|
Stopwatch.GetTimestamp() - _lastCursorMoveTime < CursorHideIdleTime * Stopwatch.Frequency);
|
|
}
|
|
}
|
|
|
|
if (_cursorState != (isCursorVisible ? CursorStates.CursorIsVisible : CursorStates.CursorIsHidden))
|
|
{
|
|
if (isCursorVisible)
|
|
{
|
|
ShowCursor();
|
|
}
|
|
else
|
|
{
|
|
HideCursor();
|
|
}
|
|
}
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
|
|
{
|
|
Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
|
|
}
|
|
});
|
|
|
|
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
|
|
|
if (currentHotkeyState != _prevHotkeyState)
|
|
{
|
|
switch (currentHotkeyState)
|
|
{
|
|
case KeyboardHotkeyState.ToggleVSync:
|
|
ToggleVSync();
|
|
break;
|
|
case KeyboardHotkeyState.Screenshot:
|
|
ScreenshotRequested = true;
|
|
break;
|
|
case KeyboardHotkeyState.ShowUI:
|
|
_viewModel.ShowMenuAndStatusBar = !_viewModel.ShowMenuAndStatusBar;
|
|
break;
|
|
case KeyboardHotkeyState.Pause:
|
|
if (_viewModel.IsPaused)
|
|
{
|
|
Resume();
|
|
}
|
|
else
|
|
{
|
|
Pause();
|
|
}
|
|
break;
|
|
case KeyboardHotkeyState.ToggleMute:
|
|
if (Device.IsAudioMuted())
|
|
{
|
|
Device.SetVolume(_viewModel.VolumeBeforeMute);
|
|
}
|
|
else
|
|
{
|
|
_viewModel.VolumeBeforeMute = Device.GetVolume();
|
|
Device.SetVolume(0);
|
|
}
|
|
|
|
_viewModel.Volume = Device.GetVolume();
|
|
break;
|
|
case KeyboardHotkeyState.ResScaleUp:
|
|
GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
|
|
break;
|
|
case KeyboardHotkeyState.ResScaleDown:
|
|
GraphicsConfig.ResScale =
|
|
(MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
|
|
break;
|
|
case KeyboardHotkeyState.VolumeUp:
|
|
_newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
|
|
Device.SetVolume(_newVolume);
|
|
|
|
_viewModel.Volume = Device.GetVolume();
|
|
break;
|
|
case KeyboardHotkeyState.VolumeDown:
|
|
_newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
|
|
Device.SetVolume(_newVolume);
|
|
|
|
_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 (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value)
|
|
{
|
|
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;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
|
|
{
|
|
state = KeyboardHotkeyState.ResScaleUp;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
|
|
{
|
|
state = KeyboardHotkeyState.ResScaleDown;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
|
|
{
|
|
state = KeyboardHotkeyState.VolumeUp;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
|
|
{
|
|
state = KeyboardHotkeyState.VolumeDown;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
}
|
|
}
|