Ryujinx/Ryujinx.Ava/AppHost.cs
Emmanuel Hansen deb99d2cae
Avalonia UI - Part 1 (#3270)
* avalonia part 1

* remove vulkan ui backend

* move ui common files to ui common project

* get name for oading screen from device

* rebase.

* review 1

* review 1.1

* review

* cleanup

* addressed review

* use cancellation token

* review

* review

* rebased

* cancel library loading when closing window

* remove star  image, use fonticon instead

* delete render control frame buffer when game ends. change position of fav star

* addressed @Thog review

* ensure the right ui is downloaded in updates

* fix crash when showing not supported dialog during controller request

* add prefix to artifact names

* Auto-format Avalonia project

* Fix input

* Fix build, simplify app disposal

* remove nv stutter thread

* addressed review

* add missing change

* maintain window size if new size is zero length

* add game, handheld, docked to local

* reverse scale main window

* Update de_DE.json

* Update de_DE.json

* Update de_DE.json

* Update italian json

* Update it_IT.json

* let render timer poll with no wait

* remove unused code

* more unused code

* enabled tiered compilation and trimming

* check if window event is not closed before signaling

* fix atmospher case

* locale fix

* locale fix

* remove explicit tiered compilation declarations

* Remove ) it_IT.json

* Remove ) de_DE.json

* Update it_IT.json

* Update pt_BR locale with latest strings

* Remove ')'

* add more strings to locale

* update locale

* remove extra slash

* remove extra slash

* set firmware version to 0 if key's not found

* fix

* revert timer changes

* lock  on object instead

* Update it_IT.json

* remove unused method

* add load screen text to locale

* drop swap event

* Update de_DE.json

* Update de_DE.json

* do null check when stopping emulator

* Update de_DE.json

* Create tr_TR.json

* Add tr_TR

* Add tr_TR + Turkish

* Update it_IT.json

* Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* addressed review

* Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* use avalonia's inbuilt renderer on linux

* removed whitespace

* workaround for queue render crash with vsync off

* drop custom backend

* format files

* fix not closing issue

* remove warnings

* rebase

* update avalonia library

* Reposition the Text and Button on About Page

* Assign build version

* Remove appveyor text

Co-authored-by: gdk <gab.dark.100@gmail.com>
Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com>
Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com>
Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
2022-05-15 13:30:15 +02:00

1056 lines
40 KiB
C#

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;
}
}
}