Rewrite Display Code and more

This commit is contained in:
Stossy11 2025-03-06 11:57:03 +11:00
parent e372f6eb35
commit 8ca88def54
40 changed files with 1804 additions and 582 deletions

View file

@ -0,0 +1,173 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Input.HLE;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using static SDL2.SDL;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Vulkan.Extensions.KHR;
namespace Ryujinx.Headless.SDL2.Vulkan
{
class MoltenVKWindow : WindowBase
{
public IntPtr nativeMetalLayer = IntPtr.Zero;
private Vk _vk;
private ExtMetalSurface _metalSurface;
private SurfaceKHR _surface;
private bool _surfaceCreated;
public MoltenVKWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
HideCursorMode hideCursorMode) : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
{
_vk = Vk.GetApi();
_surfaceCreated = false;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
protected override void InitializeWindowRenderer() {}
protected override void InitializeRenderer()
{
if (IsExclusiveFullscreen)
{
Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
}
else
{
Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
}
}
public void SetNativeWindow(IntPtr metalLayer)
{
if (metalLayer == IntPtr.Zero)
{
return;
}
nativeMetalLayer = IntPtr.Zero;
nativeMetalLayer = metalLayer;
}
private static void BasicInvoke(Action action)
{
action();
}
public unsafe IntPtr CreateWindowSurface(IntPtr instanceHandle)
{
if (_surfaceCreated)
{
return (IntPtr)(ulong)_surface.Handle;
}
if (nativeMetalLayer == IntPtr.Zero)
{
throw new Exception("Cannot create Vulkan surface: No CAMetalLayer set");
}
var instance = new Instance((nint)instanceHandle);
if (!_vk.TryGetInstanceExtension(instance, out _metalSurface))
{
throw new Exception("Failed to get ExtMetalSurface extension");
}
var createInfo = new MetalSurfaceCreateInfoEXT
{
SType = StructureType.MetalSurfaceCreateInfoExt,
PNext = null,
PLayer = (nint*)nativeMetalLayer
};
SurfaceKHR* surfacePtr = stackalloc SurfaceKHR[1];
Result result = _metalSurface.CreateMetalSurface(instance, &createInfo, null, surfacePtr);
if (result != Result.Success)
{
throw new Exception($"vkCreateMetalSurfaceEXT failed with error code {result}");
}
_surface = *surfacePtr;
_surfaceCreated = true;
return (IntPtr)(ulong)_surface.Handle;
}
public unsafe string[] GetRequiredInstanceExtensions()
{
List<string> requiredExtensions = new List<string>
{
"VK_KHR_surface",
"VK_EXT_metal_surface"
};
uint extensionCount = 0;
_vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, null);
if (extensionCount == 0)
{
string errorMessage = "Failed to enumerate Vulkan instance extensions";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
ExtensionProperties* extensions = stackalloc ExtensionProperties[(int)extensionCount];
Result result = _vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, extensions);
if (result != Result.Success)
{
string errorMessage = $"Failed to enumerate Vulkan instance extensions, error: {result}";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
List<string> availableExtensions = new List<string>();
for (int i = 0; i < extensionCount; i++)
{
string extName = Marshal.PtrToStringAnsi((IntPtr)extensions[i].ExtensionName);
availableExtensions.Add(extName);
}
Logger.Info?.Print(LogClass.Application, $"Available Vulkan extensions: {string.Join(", ", availableExtensions)}");
foreach (string requiredExt in requiredExtensions)
{
if (!availableExtensions.Contains(requiredExt))
{
string errorMessage = $"Required Vulkan extension {requiredExt} is not available";
Logger.Error?.Print(LogClass.Application, errorMessage);
}
}
Logger.Info?.Print(LogClass.Application, $"Using Vulkan extensions: {string.Join(", ", requiredExtensions)}");
return requiredExtensions.ToArray();
}
protected override void FinalizeWindowRenderer()
{
if (_surfaceCreated)
{
_surface = default;
_surfaceCreated = false;
}
nativeMetalLayer = IntPtr.Zero;
}
protected override void SwapBuffers() {}
}
}

View file

@ -113,6 +113,8 @@ namespace Ryujinx.Headless.SDL2
private static List<InputConfig> _inputConfiguration;
private static bool _enableKeyboard;
private static bool _enableMouse;
private static IntPtr nativeMetalLayer = IntPtr.Zero;
private static readonly object metalLayerLock = new object();
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@ -142,6 +144,19 @@ namespace Ryujinx.Headless.SDL2
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "set_native_window")]
public static unsafe void SetNativeWindow(IntPtr layer) {
lock (metalLayerLock) {
nativeMetalLayer = layer;
Logger.Info?.Print(LogClass.Application, $"SetNativeWindow called with layer: {layer}");
}
}
public static IntPtr GetNativeMetalLayer()
{
return nativeMetalLayer;
}
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
{
@ -391,12 +406,13 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
public static void StopEmulation()
{
if (_window != null)
{
_window.Exit();
_emulationContext.Dispose();
_emulationContext = null;
if (_window._isPaused) {
_window._isPaused = false;
} else {
_window._isPaused = true;
}
}
}
@ -977,8 +993,7 @@ namespace Ryujinx.Headless.SDL2
}
else
{
bool isAppleController = gamepadName.Contains("Apple") ? option.OnScreenCorrespond : false;
bool isNintendoStyle = gamepadName.Contains("Nintendo") || isAppleController;
bool isNintendoStyle = gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons");
config = new StandardControllerInputConfig
{
@ -1190,7 +1205,7 @@ namespace Ryujinx.Headless.SDL2
}
if (option.InputPath == "MiiMaker") {
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001000, StorageId.BuiltInSystem, NcaContentType.Program);
option.InputPath = contentPath;
}
@ -1292,18 +1307,25 @@ namespace Ryujinx.Headless.SDL2
private static WindowBase CreateWindow(Options options)
{
return options.GraphicsBackend == GraphicsBackend.Vulkan
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
if (OperatingSystem.IsIOS()) {
return new MoltenVKWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
}
else
{
return options.GraphicsBackend == GraphicsBackend.Vulkan
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
}
}
private static IRenderer CreateRenderer(Options options, WindowBase window)
{
if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
if (options.GraphicsBackend == GraphicsBackend.Vulkan)
{
string preferredGpuId = string.Empty;
Vk api = Vk.GetApi();
// Handle GPU preference selection
if (!string.IsNullOrEmpty(options.PreferredGPUVendor))
{
string preferredGpuVendor = options.PreferredGPUVendor.ToLowerInvariant();
@ -1319,13 +1341,25 @@ namespace Ryujinx.Headless.SDL2
}
}
return new VulkanRenderer(
api,
(instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
vulkanWindow.GetRequiredInstanceExtensions,
preferredGpuId);
if (window is VulkanWindow vulkanWindow)
{
return new VulkanRenderer(
api,
(instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
vulkanWindow.GetRequiredInstanceExtensions,
preferredGpuId);
}
else if (window is MoltenVKWindow mvulkanWindow)
{
return new VulkanRenderer(
api,
(instance, vk) => new SurfaceKHR((ulong)(mvulkanWindow.CreateWindowSurface(instance.Handle))),
mvulkanWindow.GetRequiredInstanceExtensions,
preferredGpuId);
}
}
// Fallback to OpenGL renderer if Vulkan is not used
return new OpenGLRenderer();
}
@ -1333,12 +1367,7 @@ namespace Ryujinx.Headless.SDL2
{
BackendThreading threadingMode = options.BackendThreading;
bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
if (threadedGAL)
{
renderer = new ThreadedRenderer(renderer);
}
renderer = new ThreadedRenderer(renderer);
bool AppleHV = false;
@ -1413,6 +1442,11 @@ namespace Ryujinx.Headless.SDL2
Logger.RestartTime();
WindowBase window = CreateWindow(options);
if (window is MoltenVKWindow mvulkanWindow) {
mvulkanWindow.SetNativeWindow(nativeMetalLayer);
}
IRenderer renderer = CreateRenderer(options, window);
_window = window;

View file

@ -66,7 +66,7 @@ namespace Ryujinx.Headless.SDL2
public ScalingFilter ScalingFilter { get; set; }
public int ScalingFilterLevel { get; set; }
public SDL2MouseDriver MouseDriver;
public iOSTouchDriver MouseDriver;
private readonly InputManager _inputManager;
private readonly IKeyboard _keyboardInterface;
private readonly GraphicsDebugLevel _glLogLevel;
@ -81,6 +81,8 @@ namespace Ryujinx.Headless.SDL2
private bool _isStopped;
private uint _windowId;
public bool _isPaused = false;
private string _gpuVendorName;
private readonly AspectRatio _aspectRatio;
@ -93,7 +95,7 @@ namespace Ryujinx.Headless.SDL2
bool enableMouse,
HideCursorMode hideCursorMode)
{
MouseDriver = new SDL2MouseDriver(hideCursorMode);
MouseDriver = new iOSTouchDriver(hideCursorMode);
_inputManager = inputManager;
_inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager();
@ -159,48 +161,59 @@ namespace Ryujinx.Headless.SDL2
private void InitializeWindow()
{
var activeProcess = Device.Processes.ActiveApplication;
var nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
if (this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow) {
string message = $"Not using SDL Windows, Skipping...";
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)";
Logger.Info?.Print(LogClass.Application, message);
Width = DefaultWidth;
Height = DefaultHeight;
Width = DefaultWidth;
Height = DefaultHeight;
if (IsExclusiveFullscreen)
{
Width = ExclusiveFullscreenWidth;
Height = ExclusiveFullscreenHeight;
MouseDriver.SetClientSize(Width, Height);
} else {
var activeProcess = Device.Processes.ActiveApplication;
var nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
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)";
Width = DefaultWidth;
Height = DefaultHeight;
if (IsExclusiveFullscreen)
{
Width = ExclusiveFullscreenWidth;
Height = ExclusiveFullscreenHeight;
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
}
else if (IsFullscreen)
{
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
// WindowHandle = SDL_GetWindowFromID(1);
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | 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);
}
else if (IsFullscreen)
{
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
// WindowHandle = SDL_GetWindowFromID(1);
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | 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);
}
private void HandleWindowEvent(SDL_Event evnt)
@ -232,7 +245,7 @@ namespace Ryujinx.Headless.SDL2
}
else
{
MouseDriver.Update(evnt);
// MouseDriver.Update(evnt);
}
}
@ -289,6 +302,11 @@ namespace Ryujinx.Headless.SDL2
return;
}
if (_isPaused)
{
return;
}
_ticks += _chrono.ElapsedTicks;
_chrono.Restart();
@ -369,6 +387,13 @@ namespace Ryujinx.Headless.SDL2
{
while (_isActive)
{
if (_isPaused)
{
Thread.Sleep(1);
return;
}
UpdateFrame();
SDL_PumpEvents();
@ -408,6 +433,11 @@ namespace Ryujinx.Headless.SDL2
return true;
}
if (_isPaused)
{
return true;
}
if (_isStopped)
{
return false;
@ -416,13 +446,12 @@ namespace Ryujinx.Headless.SDL2
NpadManager.Update();
// Touchscreen
bool hasTouch = false;
bool hasTouch = true;
MouseDriver.SetClientSize(Width, Height);
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as iOSTouchDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
// Get screen touch position
if (!_enableMouse)
{
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
}
if (!hasTouch)
{
@ -545,11 +574,14 @@ namespace Ryujinx.Headless.SDL2
TouchScreenManager?.Dispose();
NpadManager.Dispose();
SDL2Driver.Instance.UnregisterWindow(_windowId);
if (!(this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow))
{
SDL2Driver.Instance.UnregisterWindow(_windowId);
SDL_DestroyWindow(WindowHandle);
SDL_DestroyWindow(WindowHandle);
SDL2Driver.Instance.Dispose();
SDL2Driver.Instance.Dispose();
}
}
}
}

View file

@ -0,0 +1,90 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Input;
using System;
using System.Drawing;
using System.Numerics;
namespace Ryujinx.Headless.SDL2
{
class iOSMouse : IMouse
{
private iOSTouchDriver _driver;
public GamepadFeaturesFlag Features => throw new NotImplementedException();
public string Id => "0";
public string Name => "iOSMouse";
public bool IsConnected => true;
public bool[] Buttons => _driver.PressedButtons;
Size IMouse.ClientSize => _driver.GetClientSize();
public iOSMouse(iOSTouchDriver driver)
{
_driver = driver;
}
public Vector2 GetPosition()
{
return _driver.CurrentPosition;
}
public Vector2 GetScroll()
{
return _driver.Scroll;
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
throw new NotImplementedException();
}
public Vector3 GetMotionData(MotionInputId inputId)
{
throw new NotImplementedException();
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotImplementedException();
}
public (float, float) GetStick(StickInputId inputId)
{
throw new NotImplementedException();
}
public bool IsButtonPressed(MouseButton button)
{
return _driver.IsButtonPressed(button);
}
public bool IsPressed(GamepadButtonInputId inputId)
{
throw new NotImplementedException();
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
throw new NotImplementedException();
}
public void SetConfiguration(InputConfig configuration)
{
throw new NotImplementedException();
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();
}
public void Dispose()
{
_driver = null;
}
}
}

View file

@ -0,0 +1,156 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Headless.SDL2
{
class iOSTouchDriver : IGamepadDriver
{
private const int CursorHideIdleTime = 5;
private bool _isDisposed;
private readonly HideCursorMode _hideCursorMode;
private bool _isHidden;
private long _lastCursorMoveTime;
public bool[] PressedButtons { get; }
public Vector2 CurrentPosition { get; private set; }
public Vector2 Scroll { get; private set; }
public Size ClientSize;
private static Dictionary<int, Vector2> _activeTouches = new();
public iOSTouchDriver(HideCursorMode hideCursorMode)
{
PressedButtons = new bool[(int)MouseButton.Count];
_hideCursorMode = hideCursorMode;
}
[UnmanagedCallersOnly(EntryPoint = "touch_began")]
public static void TouchBeganAtPoint(float x, float y, int index)
{
Vector2 position = new Vector2(x, y);
_activeTouches[index] = position;
}
[UnmanagedCallersOnly(EntryPoint = "touch_moved")]
public static void TouchMovedAtPoint(float x, float y, int index)
{
if (_activeTouches.ContainsKey(index))
{
_activeTouches[index] = new Vector2(x, y);
}
}
[UnmanagedCallersOnly(EntryPoint = "touch_ended")]
public static void TouchEndedForIndex(int index)
{
if (_activeTouches.ContainsKey(index))
{
_activeTouches.Remove(index);
}
}
public void UpdatePosition()
{
if (_activeTouches.Count > 0)
{
var touch = _activeTouches.Values.GetEnumerator();
touch.MoveNext();
Vector2 position = touch.Current;
if (CurrentPosition != position)
{
CurrentPosition = position;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
}
CheckIdle();
}
private void CheckIdle()
{
if (_hideCursorMode != HideCursorMode.OnIdle)
{
return;
}
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
{
if (!_isHidden)
{
Logger.Debug?.Print(LogClass.Application, "Hiding cursor due to inactivity.");
_isHidden = true;
}
}
else
{
if (_isHidden)
{
Logger.Debug?.Print(LogClass.Application, "Showing cursor after activity.");
_isHidden = false;
}
}
}
public void SetClientSize(int width, int height)
{
ClientSize = new Size(width, height);
}
public bool IsButtonPressed(MouseButton button)
{
if (_activeTouches.Count > 0)
{
return true;
}
return false;
}
public Size GetClientSize()
{
return ClientSize;
}
public string DriverName => "iOSTouchDriver";
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
public IGamepad GetGamepad(string id)
{
return new iOSMouse(this);
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
}
}
}