mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-06-27 22:06:24 +02:00

With this change, the play time reported internally by a game's gameplay timer should match (or be much closer to matching) what Ryujinx displays in the application library. Aside from this being closer to the natural expectation of what "hours played" would take into account (as by definition time spent paused is time spent not playing), this also brings us closer to the behavior of other emulators and game libraries.
1423 lines
54 KiB
C#
1423 lines
54 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Input;
|
|
using Avalonia.Threading;
|
|
using DiscordRPC;
|
|
using Gommon;
|
|
using LibHac.Common;
|
|
using LibHac.Ns;
|
|
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.Systems.AppLibrary;
|
|
using Ryujinx.Ava.Systems.Configuration;
|
|
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.Ava.Utilities;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Configuration;
|
|
using Ryujinx.Common.Configuration.Multiplayer;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Common.SystemInterop;
|
|
using Ryujinx.Common.UI;
|
|
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.FileSystem;
|
|
using Ryujinx.HLE.HOS;
|
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
|
using Ryujinx.Input;
|
|
using Ryujinx.Input.HLE;
|
|
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;
|
|
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
|
|
|
|
namespace Ryujinx.Ava.Systems
|
|
{
|
|
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 nint _invisibleCursorWin;
|
|
private readonly nint _defaultCursorWin;
|
|
|
|
private readonly long _ticksPerFrame;
|
|
private readonly Stopwatch _chrono;
|
|
private readonly Stopwatch _pauseTimer;
|
|
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 DateTime _lastShaderReset;
|
|
private uint _displayCount;
|
|
private uint _previousCount = 0;
|
|
|
|
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 Lock _lockObject = new();
|
|
|
|
public event EventHandler AppExit;
|
|
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
|
|
|
public VirtualFileSystem VirtualFileSystem { get; }
|
|
public ContentManager ContentManager { get; }
|
|
public NpadManager NpadManager { get; }
|
|
public TouchScreenManager TouchScreenManager { get; }
|
|
public HLE.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;
|
|
_pauseTimer = new Stopwatch();
|
|
|
|
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.Graphics.VSyncMode.Event += UpdateVSyncMode;
|
|
ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue;
|
|
ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled;
|
|
|
|
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
|
|
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
|
|
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
|
|
ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState;
|
|
ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState;
|
|
ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState;
|
|
|
|
_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();
|
|
}
|
|
|
|
Point point = e.GetCurrentPoint(window).Position;
|
|
Rect bounds = RendererHost.EmbeddedWindow.Bounds;
|
|
double windowYOffset = bounds.Y + window.MenuBarHeight;
|
|
double 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)
|
|
{
|
|
Point point = e.GetCurrentPoint(window).Position;
|
|
Rect bounds = RendererHost.EmbeddedWindow.Bounds;
|
|
double windowYOffset = bounds.Y + window.MenuBarHeight;
|
|
double 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(ConfigurationState.Instance.Graphics.ScalingFilter);
|
|
_renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel);
|
|
}
|
|
|
|
private void UpdateScalingFilter(object sender, ReactiveEventArgs<ScalingFilter> e)
|
|
{
|
|
_renderer.Window?.SetScalingFilter(ConfigurationState.Instance.Graphics.ScalingFilter);
|
|
_renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel);
|
|
}
|
|
|
|
private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
_renderer.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough);
|
|
}
|
|
|
|
public void UpdateVSyncMode(object sender, ReactiveEventArgs<VSyncMode> e)
|
|
{
|
|
if (Device != null)
|
|
{
|
|
Device.VSyncMode = e.NewValue;
|
|
Device.UpdateVSyncInterval();
|
|
}
|
|
|
|
_renderer.Window?.ChangeVSyncMode(e.NewValue);
|
|
|
|
_viewModel.UpdateVSyncIntervalPicker();
|
|
}
|
|
|
|
public void VSyncModeToggle()
|
|
{
|
|
VSyncMode oldVSyncMode = Device.VSyncMode;
|
|
bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value;
|
|
|
|
UpdateVSyncMode(this, new ReactiveEventArgs<VSyncMode>(
|
|
oldVSyncMode,
|
|
oldVSyncMode.Next(customVSyncIntervalEnabled))
|
|
);
|
|
}
|
|
|
|
private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs<int> e)
|
|
{
|
|
if (Device != null)
|
|
{
|
|
Device.TargetVSyncInterval = e.NewValue;
|
|
Device.UpdateVSyncInterval();
|
|
}
|
|
}
|
|
|
|
private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
if (Device != null)
|
|
{
|
|
Device.CustomVSyncIntervalEnabled = e.NewValue;
|
|
Device.UpdateVSyncInterval();
|
|
}
|
|
}
|
|
|
|
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 = Path.Combine(AppDataManager.BaseDirPath, "screenshots");
|
|
|
|
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;
|
|
}
|
|
|
|
SKColorType colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
|
|
using SKBitmap bitmap = new(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
|
|
|
|
Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length);
|
|
|
|
using SKBitmap bitmapToSave = new(bitmap.Width, bitmap.Height);
|
|
using SKCanvas canvas = new(bitmapToSave);
|
|
|
|
canvas.Clear(SKColors.Black);
|
|
|
|
float scaleX = e.FlipX ? -1 : 1;
|
|
float scaleY = e.FlipY ? -1 : 1;
|
|
|
|
SKMatrix matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f);
|
|
|
|
canvas.SetMatrix(matrix);
|
|
canvas.DrawBitmap(bitmap, SKPoint.Empty);
|
|
|
|
SaveBitmapAsPng(bitmapToSave, path);
|
|
|
|
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 static void SaveBitmapAsPng(SKBitmap bitmap, string path)
|
|
{
|
|
using SKData data = bitmap.Encode(SKEncodedImageFormat.Png, 100);
|
|
using FileStream stream = File.OpenWrite(path);
|
|
|
|
data.SaveTo(stream);
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
_windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
|
|
}
|
|
|
|
DisplaySleep.Prevent();
|
|
|
|
if (ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig)
|
|
{
|
|
NpadManager.Initialize(Device, ConfigurationState.InstanceExtra.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
|
}
|
|
else
|
|
{
|
|
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, !ConfigurationState.Instance.ShowOldUI);
|
|
});
|
|
|
|
_viewModel.SetUiProgressHandlers(Device);
|
|
|
|
RendererHost.BoundsChanged += Window_BoundsChanged;
|
|
|
|
_isActive = true;
|
|
|
|
_renderingThread.Start();
|
|
|
|
_viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
|
|
|
|
Rainbow.Enable();
|
|
|
|
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(e.NewValue);
|
|
}
|
|
|
|
private void UpdateDockedModeState(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
Device?.System.ChangeDockedModeState(e.NewValue);
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs<string> e)
|
|
{
|
|
Device.Configuration.MultiplayerLdnPassphrase = e.NewValue;
|
|
}
|
|
|
|
private void UpdateLdnServerState(object sender, ReactiveEventArgs<string> e)
|
|
{
|
|
Device.Configuration.MultiplayerLdnServer = e.NewValue;
|
|
}
|
|
|
|
private void UpdateDisableP2pState(object sender, ReactiveEventArgs<bool> e)
|
|
{
|
|
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
_isActive = false;
|
|
}
|
|
|
|
private void Exit()
|
|
{
|
|
(_keyboardInterface as AvaloniaKeyboard)?.Clear();
|
|
|
|
if (_isStopped)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
|
|
{
|
|
gamepad?.ClearLed();
|
|
gamepad?.Dispose();
|
|
}
|
|
|
|
DiscordIntegrationModule.GuestAppStartedAt = null;
|
|
|
|
Rainbow.Disable();
|
|
Rainbow.Reset();
|
|
|
|
_isStopped = true;
|
|
Stop();
|
|
}
|
|
|
|
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, _pauseTimer.Elapsed);
|
|
|
|
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();
|
|
_pauseTimer.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(BlitStruct<ApplicationControlProperty>? customNacpData = null)
|
|
{
|
|
DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now;
|
|
|
|
InitEmulatedSwitch();
|
|
MainWindow.UpdateGraphicsConfig();
|
|
|
|
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
|
|
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
|
|
{
|
|
if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
|
|
{
|
|
if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
|
|
{
|
|
if (userError is 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],
|
|
string.Empty);
|
|
|
|
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 is 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],
|
|
string.Empty,
|
|
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, customNacpData))
|
|
{
|
|
Device.Dispose();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
else if (Directory.Exists(ApplicationPath))
|
|
{
|
|
string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage");
|
|
|
|
if (romFsFiles.Length == 0)
|
|
{
|
|
romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
|
|
}
|
|
|
|
Logger.Notice.Print(LogClass.Application, $"Loading unpacked content archive from '{ApplicationPath}'.");
|
|
|
|
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))
|
|
{
|
|
Logger.Notice.Print(LogClass.Application, $"Loading content archive from '{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;
|
|
}
|
|
|
|
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
|
|
appMetadata => appMetadata.UpdatePreGame()
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void Resume()
|
|
{
|
|
Device?.System.TogglePauseEmulation(false);
|
|
|
|
_viewModel.IsPaused = false;
|
|
_pauseTimer.Stop();
|
|
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
|
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
|
|
}
|
|
|
|
internal void Pause()
|
|
{
|
|
Device?.System.TogglePauseEmulation(true);
|
|
|
|
_viewModel.IsPaused = true;
|
|
_pauseTimer.Start();
|
|
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
|
|
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
|
|
}
|
|
|
|
private void InitEmulatedSwitch()
|
|
{
|
|
// Initialize KeySet.
|
|
VirtualFileSystem.ReloadKeySet();
|
|
|
|
// Initialize Renderer.
|
|
GraphicsBackend backend = ConfigurationState.Instance.Graphics.GraphicsBackend;
|
|
|
|
IRenderer renderer = backend switch
|
|
{
|
|
GraphicsBackend.Vulkan => VulkanRenderer.Create(
|
|
ConfigurationState.Instance.Graphics.PreferredGpu,
|
|
(RendererHost.EmbeddedWindow as EmbeddedWindowVulkan)!.CreateSurface,
|
|
VulkanHelper.GetRequiredInstanceExtensions),
|
|
_ => new OpenGLRenderer()
|
|
};
|
|
|
|
// Initialize Configuration.
|
|
Device = new Switch(ConfigurationState.Instance.CreateHleConfiguration()
|
|
.Configure(
|
|
VirtualFileSystem,
|
|
_viewModel.LibHacHorizonManager,
|
|
ContentManager,
|
|
_accountManager,
|
|
_userChannelPersistence,
|
|
renderer.TryMakeThreaded(ConfigurationState.Instance.Graphics.BackendThreading),
|
|
InitializeAudio(),
|
|
_viewModel.UiHandler
|
|
)
|
|
);
|
|
}
|
|
|
|
private static IHardwareDeviceDriver InitializeAudio()
|
|
{
|
|
List<AudioBackend> availableBackends =
|
|
[
|
|
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 (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;
|
|
_viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true;
|
|
}
|
|
|
|
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)
|
|
{
|
|
_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(ConfigurationState.Instance.Graphics.AntiAliasing);
|
|
_renderer?.Window?.SetScalingFilter(ConfigurationState.Instance.Graphics.ScalingFilter);
|
|
_renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel);
|
|
_renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough);
|
|
|
|
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.VSyncMode);
|
|
|
|
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);
|
|
|
|
// Reload settings when the game is turned off
|
|
// (resets custom settings if there were any)
|
|
Program.ReloadConfig();
|
|
|
|
// Reload application list (changes the status of the user setting if it was added or removed during the game)
|
|
Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.LoadApplications());
|
|
}
|
|
|
|
public void InitStatus()
|
|
{
|
|
_viewModel.BackendText = RendererHost.Backend switch
|
|
{
|
|
GraphicsBackend.Vulkan => "Vulkan",
|
|
GraphicsBackend.OpenGl => "OpenGL",
|
|
_ => throw new NotImplementedException()
|
|
};
|
|
|
|
_viewModel.GpuNameText = $"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];
|
|
string vSyncMode = Device.VSyncMode.ToString();
|
|
|
|
UpdateShaderCount();
|
|
|
|
if (GraphicsConfig.ResScale != 1)
|
|
{
|
|
dockedMode += $" ({GraphicsConfig.ResScale}x)";
|
|
}
|
|
|
|
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
|
|
vSyncMode,
|
|
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
|
|
dockedMode,
|
|
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
|
|
Device.System.IsPaused ? LocaleManager.GetUnformatted(LocaleKeys.Paused) : FormatGameFrameRate(),
|
|
Device.System.IsPaused ? string.Empty : Device.Statistics.FormatFifoPercent(),
|
|
_displayCount));
|
|
}
|
|
|
|
private string FormatGameFrameRate()
|
|
{
|
|
string frameRate = Device.Statistics.GetGameFrameRate().ToString("00.00");
|
|
string frameTime = Device.Statistics.GetGameFrameTime().ToString("00.00");
|
|
|
|
return Device.TurboMode
|
|
? LocaleManager.GetUnformatted(LocaleKeys.FpsTurboStatusBarText)
|
|
.Format(frameRate, frameTime, Device.TickScalar)
|
|
: LocaleManager.GetUnformatted(LocaleKeys.FpsStatusBarText)
|
|
.Format(frameRate, frameTime);
|
|
}
|
|
|
|
public async Task ShowExitPrompt()
|
|
{
|
|
bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
|
|
if (!shouldExit)
|
|
{
|
|
if (_dialogShown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dialogShown = true;
|
|
|
|
// The hard-coded hotkey mapped to exit is Escape, but it's also the same key
|
|
// that causes the dialog we launch to close (without doing anything). In release
|
|
// mode, a race is observed that between ShowExitPrompt() appearing on KeyDown
|
|
// and the ContentDialog we create seeing the key state before KeyUp. Merely waiting
|
|
// for the key to no longer be pressed appears to be insufficient.
|
|
// NB: Using _keyboardInterface.IsPressed(Key.Escape) does not currently work.
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
while (GetAsyncKeyState(0x1B) != 0)
|
|
{
|
|
await Task.Delay(100);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(250);
|
|
}
|
|
|
|
shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
|
|
|
|
_dialogShown = false;
|
|
}
|
|
|
|
if (shouldExit)
|
|
{
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
private void UpdateShaderCount()
|
|
{
|
|
if (_displayCount is 0 && _renderer.ProgramCount is 0)
|
|
return;
|
|
|
|
// If there is a mismatch between total program compile and previous count
|
|
// this means new shaders have been compiled and should be displayed.
|
|
if (_renderer.ProgramCount != _previousCount)
|
|
{
|
|
_displayCount += _renderer.ProgramCount - _previousCount;
|
|
_lastShaderReset = DateTime.Now;
|
|
_previousCount = _renderer.ProgramCount;
|
|
}
|
|
// Check if 5s has passed since any new shaders were compiled.
|
|
// If yes, reset the counter.
|
|
else if (_lastShaderReset.AddSeconds(5) <= DateTime.Now)
|
|
{
|
|
_displayCount = 0;
|
|
}
|
|
}
|
|
|
|
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 is not WindowState.FullScreen)
|
|
{
|
|
Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
|
|
}
|
|
});
|
|
|
|
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
|
|
|
if (currentHotkeyState != _prevHotkeyState)
|
|
{
|
|
if (ConfigurationState.Instance.Hid.Hotkeys.Value.TurboModeWhileHeld &&
|
|
_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.TurboMode) != Device.TurboMode)
|
|
{
|
|
Device.ToggleTurbo();
|
|
}
|
|
|
|
switch (currentHotkeyState)
|
|
{
|
|
case KeyboardHotkeyState.ToggleVSyncMode:
|
|
VSyncModeToggle();
|
|
break;
|
|
case KeyboardHotkeyState.CustomVSyncIntervalDecrement:
|
|
_viewModel.CustomVSyncInterval = Device.DecrementCustomVSyncInterval();
|
|
break;
|
|
case KeyboardHotkeyState.CustomVSyncIntervalIncrement:
|
|
_viewModel.CustomVSyncInterval = Device.IncrementCustomVSyncInterval();
|
|
break;
|
|
case KeyboardHotkeyState.TurboMode:
|
|
if (!ConfigurationState.Instance.Hid.Hotkeys.Value.TurboModeWhileHeld)
|
|
{
|
|
Device.ToggleTurbo();
|
|
}
|
|
|
|
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.ToggleVSyncMode))
|
|
{
|
|
state = KeyboardHotkeyState.ToggleVSyncMode;
|
|
}
|
|
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;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement))
|
|
{
|
|
state = KeyboardHotkeyState.CustomVSyncIntervalIncrement;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement))
|
|
{
|
|
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
|
|
}
|
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.TurboMode))
|
|
{
|
|
state = KeyboardHotkeyState.TurboMode;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
}
|
|
}
|