Ryujinx/Ryujinx.Headless.SDL2/WindowBase.cs
Ac_K 4c2d9ff3ff
HLE: Refactoring of ApplicationLoader (#4480)
* HLE: Refactoring of ApplicationLoader

* Fix SDL2 Headless

* Addresses gdkchan feedback

* Fixes LoadUnpackedNca RomFS loading

* remove useless casting

* Cleanup and fixe empty application name

* Remove ProcessInfo

* Fixes typo

* ActiveProcess to ActiveApplication

* Update check

* Clean using.

* Use the correct filepath when loading Homebrew.npdm

* Fix NRE in ProcessResult if MetaLoader is null

* Add more checks for valid processId & return success

* Add missing logging statement for npdm error

* Return result for LoadKip()

* Move error logging out of PFS load extension method

This avoids logging "Could not find Main NCA"
followed by "Loading main..." when trying to start hbl.

* Fix GUIs not checking load results

* Fix style and formatting issues

* Fix formatting and wording

* gtk: Refactor LoadApplication()

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
2023-03-31 21:16:46 +02:00

499 lines
17 KiB
C#

using ARMeilleure.Translation;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.Ui;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using static SDL2.SDL;
using Switch = Ryujinx.HLE.Switch;
namespace Ryujinx.Headless.SDL2
{
abstract partial class WindowBase : IHostUiHandler, IDisposable
{
protected const int DefaultWidth = 1280;
protected const int DefaultHeight = 720;
private const SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN;
private const int TargetFps = 60;
private static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();
[LibraryImport("SDL2")]
// TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
public static void QueueMainThreadAction(Action action)
{
MainThreadActions.Enqueue(action);
}
public NpadManager NpadManager { get; }
public TouchScreenManager TouchScreenManager { get; }
public Switch Device { get; private set; }
public IRenderer Renderer { get; private set; }
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
protected IntPtr WindowHandle { get; set; }
public IHostUiTheme HostUiTheme { get; }
public int Width { get; private set; }
public int Height { get; private set; }
protected SDL2MouseDriver MouseDriver;
private InputManager _inputManager;
private IKeyboard _keyboardInterface;
private GraphicsDebugLevel _glLogLevel;
private readonly Stopwatch _chrono;
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
private readonly ManualResetEvent _exitEvent;
private long _ticks;
private bool _isActive;
private bool _isStopped;
private uint _windowId;
private string _gpuVendorName;
private AspectRatio _aspectRatio;
private bool _enableMouse;
public WindowBase(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
HideCursor hideCursor)
{
MouseDriver = new SDL2MouseDriver(hideCursor);
_inputManager = inputManager;
_inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
_glLogLevel = glLogLevel;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
HostUiTheme = new HeadlessHostUiTheme();
SDL2Driver.Instance.Initialize();
}
public void Initialize(Switch device, List<InputConfig> inputConfigs, bool enableKeyboard, bool enableMouse)
{
Device = device;
IRenderer renderer = Device.Gpu.Renderer;
if (renderer is ThreadedRenderer tr)
{
renderer = tr.BaseRenderer;
}
Renderer = renderer;
NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse);
TouchScreenManager.Initialize(device);
}
private void SetWindowIcon()
{
Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
byte[] iconBytes = new byte[iconStream!.Length];
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
{
Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
iconStream.Close();
return;
}
iconStream.Close();
unsafe
{
fixed (byte* iconPtr = iconBytes)
{
IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
SDL_SetWindowIcon(WindowHandle, iconHandle);
SDL_FreeSurface(iconHandle);
}
}
}
private void InitializeWindow()
{
var activeProcess = Device.Processes.ActiveApplication;
var nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags());
if (WindowHandle == IntPtr.Zero)
{
string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
SetWindowIcon();
_windowId = SDL_GetWindowID(WindowHandle);
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
Width = DefaultWidth;
Height = DefaultHeight;
}
private void HandleWindowEvent(SDL_Event evnt)
{
if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
{
switch (evnt.window.windowEvent)
{
case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
Width = evnt.window.data1;
Height = evnt.window.data2;
Renderer?.Window.SetSize(Width, Height);
MouseDriver.SetClientSize(Width, Height);
break;
case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
Exit();
break;
default:
break;
}
}
else
{
MouseDriver.Update(evnt);
}
}
protected abstract void InitializeWindowRenderer();
protected abstract void InitializeRenderer();
protected abstract void FinalizeWindowRenderer();
protected abstract void SwapBuffers();
public abstract SDL_WindowFlags GetWindowFlags();
private string GetGpuVendorName()
{
return Renderer.GetHardwareInfo().GpuVendor;
}
public void Render()
{
InitializeWindowRenderer();
Device.Gpu.Renderer.Initialize(_glLogLevel);
InitializeRenderer();
_gpuVendorName = GetGpuVendorName();
Device.Gpu.Renderer.RunLoop(() =>
{
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
Translator.IsReadyForTranslation.Set();
while (_isActive)
{
if (_isStopped)
{
return;
}
_ticks += _chrono.ElapsedTicks;
_chrono.Restart();
if (Device.WaitFifo())
{
Device.Statistics.RecordFifoStart();
Device.ProcessFrame();
Device.Statistics.RecordFifoEnd();
}
while (Device.ConsumeFrameAvailable())
{
Device.PresentFrame(SwapBuffers);
}
if (_ticks >= _ticksPerFrame)
{
string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
float scale = Graphics.Gpu.GraphicsConfig.ResScale;
if (scale != 1)
{
dockedMode += $" ({scale}x)";
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
$"GPU: {_gpuVendorName}"));
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
});
FinalizeWindowRenderer();
}
public void Exit()
{
TouchScreenManager?.Dispose();
NpadManager?.Dispose();
if (_isStopped)
{
return;
}
_gpuCancellationTokenSource.Cancel();
_isStopped = true;
_isActive = false;
_exitEvent.WaitOne();
_exitEvent.Dispose();
}
public void ProcessMainThreadQueue()
{
while (MainThreadActions.TryDequeue(out Action action))
{
action();
}
}
public void MainLoop()
{
while (_isActive)
{
UpdateFrame();
SDL_PumpEvents();
ProcessMainThreadQueue();
// Polling becomes expensive if it's not slept
Thread.Sleep(1);
}
_exitEvent.Set();
}
private void NVStutterWorkaround()
{
while (_isActive)
{
// When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
// The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
// However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
// This creates a new thread every second or so.
// The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
// This is a little over budget on a frame time of 16ms, so creates a large stutter.
// The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
// TODO: This should be removed when the issue with the GateThread is resolved.
ThreadPool.QueueUserWorkItem((state) => { });
Thread.Sleep(300);
}
}
private bool UpdateFrame()
{
if (!_isActive)
{
return true;
}
if (_isStopped)
{
return false;
}
NpadManager.Update();
// Touchscreen
bool hasTouch = false;
// Get screen touch position
if (!_enableMouse)
{
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
}
if (!hasTouch)
{
TouchScreenManager.Update(false);
}
Device.Hid.DebugPad.Update();
// TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
MouseDriver.UpdatePosition();
return true;
}
public void Execute()
{
_chrono.Restart();
_isActive = true;
InitializeWindow();
Thread renderLoopThread = new Thread(Render)
{
Name = "GUI.RenderLoop"
};
renderLoopThread.Start();
Thread nvStutterWorkaround = null;
if (Renderer is Graphics.OpenGL.OpenGLRenderer)
{
nvStutterWorkaround = new Thread(NVStutterWorkaround)
{
Name = "GUI.NVStutterWorkaround"
};
nvStutterWorkaround.Start();
}
MainLoop();
renderLoopThread.Join();
nvStutterWorkaround?.Join();
Exit();
}
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
{
// SDL2 doesn't support input dialogs
userText = "Ryujinx";
return true;
}
public bool DisplayMessageDialog(string title, string message)
{
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
return true;
}
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
{
string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
string message = $"Application requests {playerCount} player(s) with:\n\n"
+ $"TYPES: {args.SupportedStyles}\n\n"
+ $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n"
+ (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "")
+ "Please reconfigure Input now and then press OK.";
return DisplayMessageDialog("Controller Applet", message);
}
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
{
return new HeadlessDynamicTextInputHandler();
}
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
{
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
Exit();
}
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
{
SDL_MessageBoxData data = new SDL_MessageBoxData
{
title = title,
message = message,
buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
numbuttons = buttonsText.Length,
window = WindowHandle
};
for (int i = 0; i < buttonsText.Length; i++)
{
data.buttons[i] = new SDL_MessageBoxButtonData
{
buttonid = i,
text = buttonsText[i]
};
}
SDL_ShowMessageBox(ref data, out int _);
return true;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_isActive = false;
TouchScreenManager?.Dispose();
NpadManager.Dispose();
SDL2Driver.Instance.UnregisterWindow(_windowId);
SDL_DestroyWindow(WindowHandle);
SDL2Driver.Instance.Dispose();
}
}
}
}