From 0dbca88e08c1cafd115058b4f6ee5dafc88830bf Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 13 Aug 2024 09:05:36 +0000 Subject: [PATCH] =?UTF-8?q?libryujinx=20-=20fix=20branch=20fix=20libryujin?= =?UTF-8?q?x=20content=20manager=20rebase=20disable=20trim=20warning=20sup?= =?UTF-8?q?pression=20libryujinx=20-=20add=20graphics=20logging=20libryuji?= =?UTF-8?q?nx=20-=20use=20pointers=20for=20game=20info=20struct=20libryuji?= =?UTF-8?q?nx=20-=20update=20rd=20LibRyujinx:=20Fix=20path=20to=20Ryujinx.?= =?UTF-8?q?UI.Common=20project=20libryujinx=20-=20cleanup=20Start=20GameIn?= =?UTF-8?q?foNative=20Expand=20InitializeDeviceNative=20Signature=20libryu?= =?UTF-8?q?jinx=20-=20Expose=20InstallFirmware=20libryujinx=20-=20Expose?= =?UTF-8?q?=20GetInstalledFirmwareVersion=20Don=E2=80=99t=20crash=20if=20n?= =?UTF-8?q?o=20firmware=20is=20installed=20libryujinx=20-=20Expose=20Accel?= =?UTF-8?q?erometer=20&=20Gyro=20Functions=20libryujinx=20-=20add=20stream?= =?UTF-8?q?=20support=20libryujinx=20-=20add=20motion=20controls=20libryuj?= =?UTF-8?q?inx=20-=20add=20openal=20reference,=20mii=20applet=20launch=20a?= =?UTF-8?q?pi=20rebase=20fix=20libryujinx=20-=20load=20firmware=20version?= =?UTF-8?q?=20at=20launch,=20add=20user=20manager=20api=20libryujinx=20-?= =?UTF-8?q?=20fix=20whitespace=20and=20remove=20unused=20usings=20libryuij?= =?UTF-8?q?inx=20-=20fix=20rd.xml=20libryujinx=20-=20some=20optimizations.?= =?UTF-8?q?=20apply=20current=20transform=20to=20native=20window=20instead?= =?UTF-8?q?=20of=20defaulting=20to=20Identity=20libryujinx=20-=20update=20?= =?UTF-8?q?libryujinx=20-=20Add=20more=20debug=20information=20when=20load?= =?UTF-8?q?ing=20game=20files=20libryujinx=20-=20call=20swapbuffer=20callb?= =?UTF-8?q?ack=20libryujinx=20-=20update=20input=20add=20file=20logs=20add?= =?UTF-8?q?=20game=20stats=20helper=20libryujinx-update=20add=20basic=20to?= =?UTF-8?q?uch=20and=20button=20input=20interface=20remove=20armeilleire?= =?UTF-8?q?=20reference=20in=20rd=20file=20libryujinx=20-=20disable=20shad?= =?UTF-8?q?er=20cache=20remove=20redundant=20project=20reference=20add=20n?= =?UTF-8?q?ativaot=20libryujinx=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 6288d793c6d9322c7ea188d689f524a9b73eaa9c) --- .gitignore | 1 - Directory.Packages.props | 3 +- Ryujinx.sln | 11 + nuget.config | 3 +- .../LibRyujinx.NativeSample.csproj | 16 + .../LibRyujinxInterop.cs | 228 +++++ src/LibRyujinx.NativeSample/NativeWindow.cs | 260 ++++++ src/LibRyujinx.NativeSample/Program.cs | 33 + src/LibRyujinx/LibRyujinx.Device.cs | 293 ++++++ src/LibRyujinx/LibRyujinx.Graphics.cs | 291 ++++++ src/LibRyujinx/LibRyujinx.Input.cs | 595 ++++++++++++ src/LibRyujinx/LibRyujinx.User.cs | 82 ++ src/LibRyujinx/LibRyujinx.cs | 849 ++++++++++++++++++ src/LibRyujinx/LibRyujinx.csproj | 36 + src/LibRyujinx/OpenTKBindingsContext.cs | 20 + src/LibRyujinx/rd.xml | 534 +++++++++++ src/Ryujinx.Common/ReleaseInformation.cs | 3 +- .../VulkanDebugMessenger.cs | 7 +- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 11 + src/Ryujinx.Graphics.Vulkan/Window.cs | 17 +- src/Ryujinx.Graphics.Vulkan/WindowBase.cs | 3 + 21 files changed, 3289 insertions(+), 7 deletions(-) create mode 100644 src/LibRyujinx.NativeSample/LibRyujinx.NativeSample.csproj create mode 100644 src/LibRyujinx.NativeSample/LibRyujinxInterop.cs create mode 100644 src/LibRyujinx.NativeSample/NativeWindow.cs create mode 100644 src/LibRyujinx.NativeSample/Program.cs create mode 100644 src/LibRyujinx/LibRyujinx.Device.cs create mode 100644 src/LibRyujinx/LibRyujinx.Graphics.cs create mode 100644 src/LibRyujinx/LibRyujinx.Input.cs create mode 100644 src/LibRyujinx/LibRyujinx.User.cs create mode 100644 src/LibRyujinx/LibRyujinx.cs create mode 100644 src/LibRyujinx/LibRyujinx.csproj create mode 100644 src/LibRyujinx/OpenTKBindingsContext.cs create mode 100644 src/LibRyujinx/rd.xml diff --git a/.gitignore b/.gitignore index 37b419d07..eb2f9f049 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,6 @@ build/ *.vssscc .builds *.pidb -*.log *.scc # Visual C++ cache files diff --git a/Directory.Packages.props b/Directory.Packages.props index 301024cf8..7eb217ce6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ + @@ -49,4 +50,4 @@ - \ No newline at end of file + diff --git a/Ryujinx.sln b/Ryujinx.sln index 76ebd573f..3fc147e13 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -89,6 +89,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibRyujinx", "src\LibRyujinx\LibRyujinx.csproj", "{5BBF478C-A520-41E7-9B88-890AD26766B8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibRyujinx.NativeSample", "src\LibRyujinx.NativeSample\LibRyujinx.NativeSample.csproj", "{63D2C96B-5194-4592-BC91-30BEB11C06BD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -255,6 +259,13 @@ Global {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.Build.0 = Release|Any CPU + {5BBF478C-A520-41E7-9B88-890AD26766B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BBF478C-A520-41E7-9B88-890AD26766B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BBF478C-A520-41E7-9B88-890AD26766B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BBF478C-A520-41E7-9B88-890AD26766B8}.Release|Any CPU.Build.0 = Release|Any CPU + {63D2C96B-5194-4592-BC91-30BEB11C06BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63D2C96B-5194-4592-BC91-30BEB11C06BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63D2C96B-5194-4592-BC91-30BEB11C06BD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/nuget.config b/nuget.config index 80f5bd7fc..5afd86443 100644 --- a/nuget.config +++ b/nuget.config @@ -2,6 +2,7 @@ - + diff --git a/src/LibRyujinx.NativeSample/LibRyujinx.NativeSample.csproj b/src/LibRyujinx.NativeSample/LibRyujinx.NativeSample.csproj new file mode 100644 index 000000000..ac6f5f177 --- /dev/null +++ b/src/LibRyujinx.NativeSample/LibRyujinx.NativeSample.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + true + AnyCPU + + + + + + + diff --git a/src/LibRyujinx.NativeSample/LibRyujinxInterop.cs b/src/LibRyujinx.NativeSample/LibRyujinxInterop.cs new file mode 100644 index 000000000..a5a03dc6b --- /dev/null +++ b/src/LibRyujinx.NativeSample/LibRyujinxInterop.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace LibRyujinx.Sample +{ + internal static class LibRyujinxInterop + { + private const string dll = "LibRyujinx.dll"; + + [DllImport(dll, EntryPoint = "initialize")] + public extern static bool Initialize(IntPtr path); + + + [DllImport(dll, EntryPoint = "graphics_initialize")] + public extern static bool InitializeGraphics(GraphicsConfiguration graphicsConfiguration); + + [DllImport(dll, EntryPoint = "device_initialize")] + internal extern static bool InitializeDevice(bool isHostMapped, + bool useHypervisor, + SystemLanguage systemLanguage, + RegionCode regionCode, + bool enableVsync, + bool enableDockedMode, + bool enablePtc, + bool enableInternetAccess, + IntPtr timeZone, + bool ignoreMissingServices); + + [DllImport(dll, EntryPoint = "graphics_initialize_renderer")] + internal extern static bool InitializeGraphicsRenderer(GraphicsBackend backend, NativeGraphicsInterop nativeGraphicsInterop); + + [DllImport(dll, EntryPoint = "device_load")] + internal extern static bool LoadApplication(IntPtr pathPtr); + + [DllImport(dll, EntryPoint = "graphics_renderer_run_loop")] + internal extern static void RunLoop(); + + [DllImport(dll, EntryPoint = "graphics_renderer_set_size")] + internal extern static void SetRendererSize(int width, int height); + + [DllImport(dll, EntryPoint = "graphics_renderer_set_swap_buffer_callback")] + internal extern static void SetSwapBuffersCallback(IntPtr swapBuffers); + + [DllImport(dll, EntryPoint = "graphics_renderer_set_vsync")] + internal extern static void SetVsyncState(bool enabled); + + [DllImport(dll, EntryPoint = "input_initialize")] + internal extern static void InitializeInput(int width, int height); + + [DllImport(dll, EntryPoint = "input_set_client_size")] + internal extern static void SetClientSize(int width, int height); + + [DllImport(dll, EntryPoint = "input_set_touch_point")] + internal extern static void SetTouchPoint(int x, int y); + + [DllImport(dll, EntryPoint = "input_release_touch_point")] + internal extern static void ReleaseTouchPoint(); + + [DllImport(dll, EntryPoint = "input_update")] + internal extern static void UpdateInput(); + + [DllImport(dll, EntryPoint = "input_set_button_pressed")] + public extern static void SetButtonPressed(GamepadButtonInputId button, IntPtr idPtr); + + [DllImport(dll, EntryPoint = "input_set_button_released")] + public extern static void SetButtonReleased(GamepadButtonInputId button, IntPtr idPtr); + + [DllImport(dll, EntryPoint = "input_set_stick_axis")] + public extern static void SetStickAxis(StickInputId stick, Vector2 axes, IntPtr idPtr); + + [DllImport(dll, EntryPoint = "input_connect_gamepad")] + public extern static IntPtr ConnectGamepad(int index); + } + + public enum GraphicsBackend + { + Vulkan, + OpenGl + } + + public enum BackendThreading + { + Auto, + Off, + On + } + + [StructLayout(LayoutKind.Sequential)] + public struct GraphicsConfiguration + { + public float ResScale = 1f; + public float MaxAnisotropy = -1; + public bool FastGpuTime = true; + public bool Fast2DCopy = true; + public bool EnableMacroJit = false; + public bool EnableMacroHLE = true; + public bool EnableShaderCache = true; + public bool EnableTextureRecompression = false; + public BackendThreading BackendThreading = BackendThreading.Auto; + public AspectRatio AspectRatio = AspectRatio.Fixed16x9; + + public GraphicsConfiguration() + { + } + } + public enum SystemLanguage + { + Japanese, + AmericanEnglish, + French, + German, + Italian, + Spanish, + Chinese, + Korean, + Dutch, + Portuguese, + Russian, + Taiwanese, + BritishEnglish, + CanadianFrench, + LatinAmericanSpanish, + SimplifiedChinese, + TraditionalChinese, + BrazilianPortuguese, + } + public enum RegionCode + { + Japan, + USA, + Europe, + Australia, + China, + Korea, + Taiwan, + + Min = Japan, + Max = Taiwan, + } + + public struct NativeGraphicsInterop + { + public IntPtr GlGetProcAddress; + public IntPtr VkNativeContextLoader; + public IntPtr VkCreateSurface; + public IntPtr VkRequiredExtensions; + public int VkRequiredExtensionsCount; + } + + public enum AspectRatio + { + Fixed4x3, + Fixed16x9, + Fixed16x10, + Fixed21x9, + Fixed32x9, + Stretched + } + + /// + /// Represent a button from a gamepad. + /// + public enum GamepadButtonInputId : byte + { + Unbound, + A, + B, + X, + Y, + LeftStick, + RightStick, + LeftShoulder, + RightShoulder, + + // Likely axis + LeftTrigger, + // Likely axis + RightTrigger, + + DpadUp, + DpadDown, + DpadLeft, + DpadRight, + + // Special buttons + + Minus, + Plus, + + Back = Minus, + Start = Plus, + + Guide, + Misc1, + + // Xbox Elite paddle + Paddle1, + Paddle2, + Paddle3, + Paddle4, + + // PS5 touchpad button + Touchpad, + + // Virtual buttons for single joycon + SingleLeftTrigger0, + SingleRightTrigger0, + + SingleLeftTrigger1, + SingleRightTrigger1, + + Count + } + + public enum StickInputId : byte + { + Unbound, + Left, + Right, + + Count + } +} diff --git a/src/LibRyujinx.NativeSample/NativeWindow.cs b/src/LibRyujinx.NativeSample/NativeWindow.cs new file mode 100644 index 000000000..0dd160ea3 --- /dev/null +++ b/src/LibRyujinx.NativeSample/NativeWindow.cs @@ -0,0 +1,260 @@ +using LibRyujinx.Sample; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using OpenTK.Windowing.Common; +using OpenTK.Windowing.Desktop; +using OpenTK.Windowing.GraphicsLibraryFramework; +using System.Runtime.InteropServices; + +namespace LibRyujinx.NativeSample +{ + internal class NativeWindow : OpenTK.Windowing.Desktop.NativeWindow + { + private nint del; + public delegate void SwapBuffersCallback(); + public delegate IntPtr GetProcAddress(string name); + public delegate IntPtr CreateSurface(IntPtr instance); + + private bool _run; + private bool _isVulkan; + private Vector2 _lastPosition; + private bool _mousePressed; + private nint _gamepadIdPtr; + private string? _gamepadId; + + public NativeWindow(NativeWindowSettings nativeWindowSettings) : base(nativeWindowSettings) + { + _isVulkan = true; + } + + internal unsafe void Start(string gamePath) + { + if (!_isVulkan) + { + MakeCurrent(); + } + + var getProcAddress = Marshal.GetFunctionPointerForDelegate(x => GLFW.GetProcAddress(x)); + var createSurface = Marshal.GetFunctionPointerForDelegate( x => + { + VkHandle surface; + GLFW.CreateWindowSurface(new VkHandle(x) ,this.WindowPtr, null, out surface); + + return surface.Handle; + }); + var vkExtensions = GLFW.GetRequiredInstanceExtensions(); + + + var pointers = new IntPtr[vkExtensions.Length]; + for (int i = 0; i < vkExtensions.Length; i++) + { + pointers[i] = Marshal.StringToHGlobalAnsi(vkExtensions[i]); + } + + fixed (IntPtr* ptr = pointers) + { + var nativeGraphicsInterop = new NativeGraphicsInterop() + { + GlGetProcAddress = getProcAddress, + VkRequiredExtensions = (nint)ptr, + VkRequiredExtensionsCount = pointers.Length, + VkCreateSurface = createSurface + }; + var success = LibRyujinxInterop.InitializeGraphicsRenderer(_isVulkan ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGl, nativeGraphicsInterop); + var timeZone = Marshal.StringToHGlobalAnsi("UTC"); + success = LibRyujinxInterop.InitializeDevice(true, + false, + SystemLanguage.AmericanEnglish, + RegionCode.USA, + true, + true, + true, + false, + timeZone, + false); + LibRyujinxInterop.InitializeInput(ClientSize.X, ClientSize.Y); + Marshal.FreeHGlobal(timeZone); + + var path = Marshal.StringToHGlobalAnsi(gamePath); + var loaded = LibRyujinxInterop.LoadApplication(path); + LibRyujinxInterop.SetRendererSize(Size.X, Size.Y); + Marshal.FreeHGlobal(path); + } + + _gamepadIdPtr = LibRyujinxInterop.ConnectGamepad(0); + _gamepadId = Marshal.PtrToStringAnsi(_gamepadIdPtr); + + if (!_isVulkan) + { + Context.MakeNoneCurrent(); + } + + _run = true; + var thread = new Thread(new ThreadStart(RunLoop)); + thread.Start(); + + UpdateLoop(); + + thread.Join(); + + foreach(var ptr in pointers) + { + Marshal.FreeHGlobal(ptr); + } + + Marshal.FreeHGlobal(_gamepadIdPtr); + } + + public void RunLoop() + { + del = Marshal.GetFunctionPointerForDelegate(SwapBuffers); + LibRyujinxInterop.SetSwapBuffersCallback(del); + + if (!_isVulkan) + { + MakeCurrent(); + + Context.SwapInterval = 0; + } + + /* Task.Run(async () => + { + await Task.Delay(1000); + + LibRyujinxInterop.SetVsyncState(true); + });*/ + + LibRyujinxInterop.RunLoop(); + + _run = false; + + if (!_isVulkan) + { + Context.MakeNoneCurrent(); + } + } + + private void SwapBuffers() + { + if (!_isVulkan) + { + this.Context.SwapBuffers(); + } + } + + protected override void OnMouseMove(MouseMoveEventArgs e) + { + base.OnMouseMove(e); + _lastPosition = e.Position; + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + if(e.Button == MouseButton.Left) + { + _mousePressed = true; + } + } + + protected override void OnResize(ResizeEventArgs e) + { + base.OnResize(e); + + if (_run) + { + LibRyujinxInterop.SetRendererSize(e.Width, e.Height); + LibRyujinxInterop.SetClientSize(e.Width, e.Height); + } + } + + protected override void OnMouseUp(MouseButtonEventArgs e) + { + base.OnMouseUp(e); + if (e.Button == MouseButton.Left) + { + _mousePressed = false; + } + } + + protected override void OnKeyUp(KeyboardKeyEventArgs e) + { + base.OnKeyUp(e); + + if (_gamepadIdPtr != IntPtr.Zero) + { + var key = GetKeyMapping(e.Key); + + LibRyujinxInterop.SetButtonReleased(key, _gamepadIdPtr); + } + } + + protected override void OnKeyDown(KeyboardKeyEventArgs e) + { + base.OnKeyDown(e); + + if (_gamepadIdPtr != IntPtr.Zero) + { + var key = GetKeyMapping(e.Key); + + LibRyujinxInterop.SetButtonPressed(key, _gamepadIdPtr); + } + } + + public void UpdateLoop() + { + while(_run) + { + ProcessWindowEvents(true); + NewInputFrame(); + ProcessWindowEvents(IsEventDriven); + if (_mousePressed) + { + LibRyujinxInterop.SetTouchPoint((int)_lastPosition.X, (int)_lastPosition.Y); + } + else + { + LibRyujinxInterop.ReleaseTouchPoint(); + } + + LibRyujinxInterop.UpdateInput(); + + Thread.Sleep(1); + } + } + + public GamepadButtonInputId GetKeyMapping(Keys key) + { + if(_keyMapping.TryGetValue(key, out var mapping)) + { + return mapping; + } + + return GamepadButtonInputId.Unbound; + } + + private Dictionary _keyMapping = new Dictionary() + { + {Keys.A, GamepadButtonInputId.A }, + {Keys.S, GamepadButtonInputId.B }, + {Keys.Z, GamepadButtonInputId.X }, + {Keys.X, GamepadButtonInputId.Y }, + {Keys.Equal, GamepadButtonInputId.Plus }, + {Keys.Minus, GamepadButtonInputId.Minus }, + {Keys.Q, GamepadButtonInputId.LeftShoulder }, + {Keys.D1, GamepadButtonInputId.LeftTrigger }, + {Keys.W, GamepadButtonInputId.RightShoulder }, + {Keys.D2, GamepadButtonInputId.RightTrigger }, + {Keys.E, GamepadButtonInputId.LeftStick }, + {Keys.R, GamepadButtonInputId.RightStick }, + {Keys.Up, GamepadButtonInputId.DpadUp }, + {Keys.Down, GamepadButtonInputId.DpadDown }, + {Keys.Left, GamepadButtonInputId.DpadLeft }, + {Keys.Right, GamepadButtonInputId.DpadRight }, + {Keys.U, GamepadButtonInputId.SingleLeftTrigger0 }, + {Keys.D7, GamepadButtonInputId.SingleLeftTrigger1 }, + {Keys.O, GamepadButtonInputId.SingleRightTrigger0 }, + {Keys.D9, GamepadButtonInputId.SingleRightTrigger1 } + }; + } +} diff --git a/src/LibRyujinx.NativeSample/Program.cs b/src/LibRyujinx.NativeSample/Program.cs new file mode 100644 index 000000000..d39a64a94 --- /dev/null +++ b/src/LibRyujinx.NativeSample/Program.cs @@ -0,0 +1,33 @@ +using LibRyujinx.Sample; +using OpenTK.Mathematics; +using OpenTK.Windowing.Common; +using OpenTK.Windowing.Desktop; + +namespace LibRyujinx.NativeSample +{ + internal class Program + { + static void Main(string[] args) + { + if (args.Length > 0) + { + var success = LibRyujinxInterop.Initialize(IntPtr.Zero); + success = LibRyujinxInterop.InitializeGraphics(new GraphicsConfiguration()); + var nativeWindowSettings = new NativeWindowSettings() + { + Size = new Vector2i(800, 600), + Title = "Ryujinx", + API = ContextAPI.NoAPI, + IsEventDriven = false, + // This is needed to run on macos + Flags = ContextFlags.ForwardCompatible, + }; + + using var window = new NativeWindow(nativeWindowSettings); + + window.IsVisible = true; + window.Start(args[0]); + } + } + } +} \ No newline at end of file diff --git a/src/LibRyujinx/LibRyujinx.Device.cs b/src/LibRyujinx/LibRyujinx.Device.cs new file mode 100644 index 000000000..2400182c0 --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.Device.cs @@ -0,0 +1,293 @@ +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using Microsoft.Win32.SafeHandles; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.SystemState; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + [UnmanagedCallersOnly(EntryPoint = "device_initialize")] + public static bool InitializeDeviceNative(bool isHostMapped, + bool useHypervisor, + SystemLanguage systemLanguage, + RegionCode regionCode, + bool enableVsync, + bool enableDockedMode, + bool enablePtc, + bool enableInternetAccess, + IntPtr timeZone, + bool ignoreMissingServices) + { + return InitializeDevice(isHostMapped, + useHypervisor, + systemLanguage, + regionCode, + enableVsync, + enableDockedMode, + enablePtc, + enableInternetAccess, + Marshal.PtrToStringAnsi(timeZone), + ignoreMissingServices); + } + + [UnmanagedCallersOnly(EntryPoint = "device_reloadFilesystem")] + public static void ReloadFileSystem() + { + SwitchDevice?.ReloadFileSystem(); + } + + public static bool InitializeDevice(bool isHostMapped, + bool useHypervisor, + SystemLanguage systemLanguage, + RegionCode regionCode, + bool enableVsync, + bool enableDockedMode, + bool enablePtc, + bool enableInternetAccess, + string? timeZone, + bool ignoreMissingServices) + { + if (SwitchDevice == null) + { + return false; + } + + return SwitchDevice.InitializeContext(isHostMapped, + useHypervisor, + systemLanguage, + regionCode, + enableVsync, + enableDockedMode, + enablePtc, + enableInternetAccess, + timeZone, + ignoreMissingServices); + } + + [UnmanagedCallersOnly(EntryPoint = "device_load")] + public static bool LoadApplicationNative(IntPtr pathPtr) + { + if(SwitchDevice?.EmulationContext == null) + { + return false; + } + + var path = Marshal.PtrToStringAnsi(pathPtr); + + return LoadApplication(path); + } + + [UnmanagedCallersOnly(EntryPoint = "device_install_firmware")] + public static void InstallFirmwareNative(int descriptor, bool isXci) + { + var stream = OpenFile(descriptor); + + InstallFirmware(stream, isXci); + } + + [UnmanagedCallersOnly(EntryPoint = "device_get_installed_firmware_version")] + public static IntPtr GetInstalledFirmwareVersionNative() + { + var result = GetInstalledFirmwareVersion(); + return Marshal.StringToHGlobalAnsi(result); + } + + public static void InstallFirmware(Stream stream, bool isXci) + { + SwitchDevice?.ContentManager.InstallFirmware(stream, isXci); + } + + public static string GetInstalledFirmwareVersion() + { + var version = SwitchDevice?.ContentManager.GetCurrentFirmwareVersion(); + + if (version != null) + { + return version.VersionString; + } + + return String.Empty; + } + + public static SystemVersion? VerifyFirmware(Stream stream, bool isXci) + { + return SwitchDevice?.ContentManager?.VerifyFirmwarePackage(stream, isXci) ?? null; + } + + public static bool LoadApplication(Stream stream, FileType type, Stream? updateStream = null) + { + var emulationContext = SwitchDevice.EmulationContext; + return type switch + { + FileType.None => false, + FileType.Nsp => emulationContext?.LoadNsp(stream, 0, updateStream) ?? false, + FileType.Xci => emulationContext?.LoadXci(stream, 0, updateStream) ?? false, + FileType.Nro => emulationContext?.LoadProgram(stream, true, "") ?? false, + }; + } + + public static bool LaunchMiiEditApplet() + { + string contentPath = SwitchDevice.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); + + return LoadApplication(contentPath); + } + + public static bool LoadApplication(string? path) + { + var emulationContext = SwitchDevice.EmulationContext; + + if (Directory.Exists(path)) + { + string[] romFsFiles = Directory.GetFiles(path, "*.istorage"); + + if (romFsFiles.Length == 0) + { + romFsFiles = Directory.GetFiles(path, "*.romfs"); + } + + if (romFsFiles.Length > 0) + { + Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); + + if (!emulationContext.LoadCart(path, romFsFiles[0])) + { + SwitchDevice.DisposeContext(); + + return false; + } + } + else + { + Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); + + if (!emulationContext.LoadCart(path)) + { + SwitchDevice.DisposeContext(); + + return false; + } + } + } + else if (File.Exists(path)) + { + switch (Path.GetExtension(path).ToLowerInvariant()) + { + case ".xci": + Logger.Info?.Print(LogClass.Application, "Loading as XCI."); + + if (!emulationContext.LoadXci(path)) + { + SwitchDevice.DisposeContext(); + + return false; + } + break; + case ".nca": + Logger.Info?.Print(LogClass.Application, "Loading as NCA."); + + if (!emulationContext.LoadNca(path)) + { + SwitchDevice.DisposeContext(); + + return false; + } + break; + case ".nsp": + case ".pfs0": + Logger.Info?.Print(LogClass.Application, "Loading as NSP."); + + if (!emulationContext.LoadNsp(path)) + { + SwitchDevice.DisposeContext(); + + return false; + } + break; + default: + Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); + try + { + if (!emulationContext.LoadProgram(path)) + { + SwitchDevice.DisposeContext(); + + return false; + } + } + catch (ArgumentOutOfRangeException) + { + Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); + + SwitchDevice.DisposeContext(); + + return false; + } + break; + } + } + else + { + Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{path}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); + + SwitchDevice.DisposeContext(); + + return false; + } + + return true; + } + + public static void SignalEmulationClose() + { + _isStopped = true; + _isActive = false; + } + + public static void CloseEmulation() + { + if (SwitchDevice == null) + return; + + _npadManager?.Dispose(); + _npadManager = null; + + _touchScreenManager?.Dispose(); + _touchScreenManager = null; + + SwitchDevice!.InputManager?.Dispose(); + SwitchDevice!.InputManager = null; + _inputManager = null; + + if (Renderer != null) + { + _gpuDoneEvent.WaitOne(); + _gpuDoneEvent.Dispose(); + _gpuDoneEvent = null; + SwitchDevice?.DisposeContext(); + Renderer = null; + } + } + + private static FileStream OpenFile(int descriptor) + { + var safeHandle = new SafeFileHandle(descriptor, false); + + return new FileStream(safeHandle, FileAccess.ReadWrite); + } + + public enum FileType + { + None, + Nsp, + Xci, + Nro + } + } +} diff --git a/src/LibRyujinx/LibRyujinx.Graphics.cs b/src/LibRyujinx/LibRyujinx.Graphics.cs new file mode 100644 index 000000000..78f8155e5 --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.Graphics.cs @@ -0,0 +1,291 @@ +using LibRyujinx.Shared; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using Ryujinx.Cpu; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.GAL.Multithreading; +using Ryujinx.Graphics.Gpu; +using Ryujinx.Graphics.Gpu.Shader; +using Ryujinx.Graphics.OpenGL; +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Vulkan; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + private static bool _isActive; + private static bool _isStopped; + private static CancellationTokenSource _gpuCancellationTokenSource; + private static SwapBuffersCallback? _swapBuffersCallback; + private static NativeGraphicsInterop _nativeGraphicsInterop; + private static ManualResetEvent _gpuDoneEvent; + private static bool _enableGraphicsLogging; + + public delegate void SwapBuffersCallback(); + public delegate IntPtr GetProcAddress(string name); + public delegate IntPtr CreateSurface(IntPtr instance); + + public static IRenderer? Renderer { get; set; } + public static GraphicsConfiguration GraphicsConfiguration { get; private set; } + + [UnmanagedCallersOnly(EntryPoint = "graphics_initialize")] + public static bool InitializeGraphicsNative(GraphicsConfiguration graphicsConfiguration) + { + return InitializeGraphics(graphicsConfiguration); + } + + public static bool InitializeGraphics(GraphicsConfiguration graphicsConfiguration) + { + GraphicsConfig.ResScale = graphicsConfiguration.ResScale; + GraphicsConfig.MaxAnisotropy = graphicsConfiguration.MaxAnisotropy; + GraphicsConfig.FastGpuTime = graphicsConfiguration.FastGpuTime; + GraphicsConfig.Fast2DCopy = graphicsConfiguration.Fast2DCopy; + GraphicsConfig.EnableMacroJit = graphicsConfiguration.EnableMacroJit; + GraphicsConfig.EnableMacroHLE = graphicsConfiguration.EnableMacroHLE; + GraphicsConfig.EnableShaderCache = graphicsConfiguration.EnableShaderCache; + GraphicsConfig.EnableTextureRecompression = graphicsConfiguration.EnableTextureRecompression; + + GraphicsConfiguration = graphicsConfiguration; + + return true; + } + + [UnmanagedCallersOnly(EntryPoint = "graphics_initialize_renderer")] + public unsafe static bool InitializeGraphicsRendererNative(GraphicsBackend graphicsBackend, NativeGraphicsInterop nativeGraphicsInterop) + { + _nativeGraphicsInterop = nativeGraphicsInterop; + if (Renderer != null) + { + return false; + } + + List extensions = new List(); + var size = Marshal.SizeOf(); + var extPtr = (IntPtr*)nativeGraphicsInterop.VkRequiredExtensions; + for (int i = 0; i < nativeGraphicsInterop.VkRequiredExtensionsCount; i++) + { + var ptr = extPtr[i]; + extensions.Add(Marshal.PtrToStringAnsi(ptr) ?? string.Empty); + } + + CreateSurface createSurfaceFunc = nativeGraphicsInterop.VkCreateSurface == IntPtr.Zero ? default : Marshal.GetDelegateForFunctionPointer(nativeGraphicsInterop.VkCreateSurface); + + return InitializeGraphicsRenderer(graphicsBackend, createSurfaceFunc, extensions.ToArray()); + } + + public static bool InitializeGraphicsRenderer(GraphicsBackend graphicsBackend, CreateSurface createSurfaceFunc, string?[] requiredExtensions) + { + if (Renderer != null) + { + return false; + } + + if (graphicsBackend == GraphicsBackend.OpenGl) + { + Renderer = new OpenGLRenderer(); + } + else if (graphicsBackend == GraphicsBackend.Vulkan) + { + Renderer = new VulkanRenderer(Vk.GetApi(), (instance, vk) => new SurfaceKHR((ulong?)createSurfaceFunc(instance.Handle)), + () => requiredExtensions, + null); + } + else + { + return false; + } + + return true; + } + + + [UnmanagedCallersOnly(EntryPoint = "graphics_renderer_set_size")] + public static void SetRendererSizeNative(int width, int height) + { + SetRendererSize(width, height); + } + + public static void SetRendererSize(int width, int height) + { + Renderer?.Window?.SetSize(width, height); + } + + [UnmanagedCallersOnly(EntryPoint = "graphics_renderer_run_loop")] + public static void RunLoopNative() + { + if (Renderer is OpenGLRenderer) + { + var proc = Marshal.GetDelegateForFunctionPointer(_nativeGraphicsInterop.GlGetProcAddress); + GL.LoadBindings(new OpenTKBindingsContext(x => proc!.Invoke(x))); + } + RunLoop(); + } + + [UnmanagedCallersOnly(EntryPoint = "graphics_renderer_set_vsync")] + public static void SetVsyncStateNative(bool enabled) + { + SetVsyncState(enabled); + } + + public static void SetVsyncState(bool enabled) + { + var device = SwitchDevice!.EmulationContext!; + device.EnableDeviceVsync = enabled; + device.Gpu.Renderer.Window.ChangeVSyncMode(enabled); + } + + public static void RunLoop() + { + if (Renderer == null) + { + return; + } + var device = SwitchDevice!.EmulationContext!; + _gpuDoneEvent = new ManualResetEvent(true); + + device.Gpu.Renderer.Initialize(_enableGraphicsLogging ? GraphicsDebugLevel.All : GraphicsDebugLevel.None); + + _gpuCancellationTokenSource = new CancellationTokenSource(); + + device.Gpu.ShaderCacheStateChanged += LoadProgressStateChangedHandler; + device.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += LoadProgressStateChangedHandler; + + try + { + device.Gpu.Renderer.RunLoop(() => + { + _gpuDoneEvent.Reset(); + device.Gpu.SetGpuThread(); + device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); + + _isActive = true; + + while (_isActive) + { + if (_isStopped) + { + break; + } + + if (device.WaitFifo()) + { + device.Statistics.RecordFifoStart(); + device.ProcessFrame(); + device.Statistics.RecordFifoEnd(); + } + + while (device.ConsumeFrameAvailable()) + { + device.PresentFrame(() =>_swapBuffersCallback?.Invoke()); + } + } + + if (device.Gpu.Renderer is ThreadedRenderer threaded) + { + threaded.FlushThreadedCommands(); + } + + _gpuDoneEvent.Set(); + }); + } + finally + { + device.Gpu.ShaderCacheStateChanged -= LoadProgressStateChangedHandler; + device.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= LoadProgressStateChangedHandler; + } + } + + private static void LoadProgressStateChangedHandler(T state, int current, int total) where T : Enum + { + void SetInfo(string status, float value) + { + var ptr = Marshal.StringToHGlobalAnsi(status); + // add setinfo callback + + Marshal.FreeHGlobal(ptr); + } + var status = $"{current} / {total}"; + var progress = current / (float)total; + + switch (state) + { + case LoadState ptcState: + if (float.IsNaN((progress))) + progress = 0; + + switch (ptcState) + { + case LoadState.Unloaded: + case LoadState.Loading: + SetInfo($"Loading PTC {status}", progress); + break; + case LoadState.Loaded: + SetInfo($"PTC Loaded", -1); + break; + } + break; + case ShaderCacheState shaderCacheState: + switch (shaderCacheState) + { + case ShaderCacheState.Start: + case ShaderCacheState.Loading: + SetInfo($"Compiling Shaders {status}", progress); + break; + case ShaderCacheState.Packaging: + SetInfo($"Packaging Shaders {status}", progress); + break; + case ShaderCacheState.Loaded: + SetInfo($"Shaders Loaded", -1); + break; + } + break; + default: + throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); + } + } + + [UnmanagedCallersOnly(EntryPoint = "graphics_renderer_set_swap_buffer_callback")] + public static void SetSwapBuffersCallbackNative(IntPtr swapBuffersCallback) + { + _swapBuffersCallback = Marshal.GetDelegateForFunctionPointer(swapBuffersCallback); + } + + public static void SetSwapBuffersCallback(SwapBuffersCallback swapBuffersCallback) + { + _swapBuffersCallback = swapBuffersCallback; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct GraphicsConfiguration + { + public float ResScale = 1f; + public float MaxAnisotropy = -1; + public bool FastGpuTime = true; + public bool Fast2DCopy = true; + public bool EnableMacroJit = false; + public bool EnableMacroHLE = true; + public bool EnableShaderCache = true; + public bool EnableTextureRecompression = false; + public BackendThreading BackendThreading = BackendThreading.Auto; + public AspectRatio AspectRatio = AspectRatio.Fixed16x9; + + public GraphicsConfiguration() + { + } + } + + public struct NativeGraphicsInterop + { + public IntPtr GlGetProcAddress; + public IntPtr VkNativeContextLoader; + public IntPtr VkCreateSurface; + public IntPtr VkRequiredExtensions; + public int VkRequiredExtensionsCount; + } +} diff --git a/src/LibRyujinx/LibRyujinx.Input.cs b/src/LibRyujinx/LibRyujinx.Input.cs new file mode 100644 index 000000000..087b4d8cc --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.Input.cs @@ -0,0 +1,595 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Input; +using Ryujinx.Input.HLE; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; +using StickInputId = Ryujinx.Input.StickInputId; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + private static VirtualGamepadDriver? _gamepadDriver; + private static VirtualTouchScreen? _virtualTouchScreen; + private static VirtualTouchScreenDriver? _touchScreenDriver; + private static TouchScreenManager? _touchScreenManager; + private static InputManager? _inputManager; + private static NpadManager? _npadManager; + private static InputConfig[] _configs; + + public static void InitializeInput(int width, int height) + { + if(SwitchDevice!.InputManager != null) + { + throw new InvalidOperationException("Input is already initialized"); + } + + _gamepadDriver = new VirtualGamepadDriver(4); + _configs = new InputConfig[4]; + _virtualTouchScreen = new VirtualTouchScreen(); + _touchScreenDriver = new VirtualTouchScreenDriver(_virtualTouchScreen); + _inputManager = new InputManager(null, _gamepadDriver); + _inputManager.SetMouseDriver(_touchScreenDriver); + _npadManager = _inputManager.CreateNpadManager(); + + SwitchDevice!.InputManager = _inputManager; + + _touchScreenManager = _inputManager.CreateTouchScreenManager(); + _touchScreenManager.Initialize(SwitchDevice!.EmulationContext); + + _npadManager.Initialize(SwitchDevice.EmulationContext, new List(), false, false); + + _virtualTouchScreen.ClientSize = new Size(width, height); + } + + public static void SetClientSize(int width, int height) + { + _virtualTouchScreen!.ClientSize = new Size(width, height); + } + + public static void SetTouchPoint(int x, int y) + { + _virtualTouchScreen?.SetPosition(x, y); + } + + public static void ReleaseTouchPoint() + { + _virtualTouchScreen?.ReleaseTouch(); + } + + public static void SetButtonPressed(GamepadButtonInputId button, int id) + { + _gamepadDriver?.SetButtonPressed(button, id); + } + + public static void SetButtonReleased(GamepadButtonInputId button, int id) + { + _gamepadDriver?.SetButtonReleased(button, id); + } + + public static void SetAccelerometerData(Vector3 accel, int id) + { + _gamepadDriver?.SetAccelerometerData(accel, id); + } + + public static void SetGryoData(Vector3 gyro, int id) + { + _gamepadDriver?.SetGryoData(gyro, id); + } + + public static void SetStickAxis(StickInputId stick, Vector2 axes, int deviceId) + { + _gamepadDriver?.SetStickAxis(stick, axes, deviceId); + } + + public static int ConnectGamepad(int index) + { + var gamepad = _gamepadDriver?.GetGamepad(index); + if (gamepad != null) + { + var config = CreateDefaultInputConfig(); + + config.Id = gamepad.Id; + config.PlayerIndex = (PlayerIndex)index; + + _configs[index] = config; + } + + _npadManager?.ReloadConfiguration(_configs.Where(x => x != null).ToList(), false, false); + + return int.TryParse(gamepad?.Id, out var idInt) ? idInt : -1; + } + + private static InputConfig CreateDefaultInputConfig() + { + return new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = null, + ControllerType = ControllerType.ProController, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ConfigGamepadInputId.A, + ButtonB = ConfigGamepadInputId.B, + ButtonX = ConfigGamepadInputId.X, + ButtonY = ConfigGamepadInputId.Y, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false + } + }; + } + + public static void UpdateInput() + { + _npadManager?.Update(GraphicsConfiguration.AspectRatio.ToFloat()); + + if(!_touchScreenManager!.Update(true, _virtualTouchScreen!.IsButtonPressed(MouseButton.Button1), GraphicsConfiguration.AspectRatio.ToFloat())) + { + SwitchDevice!.EmulationContext?.Hid.Touchscreen.Update(); + } + } + + // Native Methods + + [UnmanagedCallersOnly(EntryPoint = "input_initialize")] + public static void InitializeInputNative(int width, int height) + { + InitializeInput(width, height); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_client_size")] + public static void SetClientSizeNative(int width, int height) + { + SetClientSize(width, height); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_touch_point")] + public static void SetTouchPointNative(int x, int y) + { + SetTouchPoint(x, y); + } + + + [UnmanagedCallersOnly(EntryPoint = "input_release_touch_point")] + public static void ReleaseTouchPointNative() + { + ReleaseTouchPoint(); + } + + [UnmanagedCallersOnly(EntryPoint = "input_update")] + public static void UpdateInputNative() + { + UpdateInput(); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_button_pressed")] + public static void SetButtonPressedNative(GamepadButtonInputId button, int id) + { + SetButtonPressed(button, id); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_button_released")] + public static void SetButtonReleasedNative(GamepadButtonInputId button, int id) + { + SetButtonReleased(button, id); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_accelerometer_data")] + public static void SetAccelerometerDataNative(Vector3 accel, int id) + { + SetAccelerometerData(accel, id); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_gyro_data")] + public static void SetGryoDataNatuve(Vector3 gyro, int id) + { + SetGryoData(gyro, id); + } + + [UnmanagedCallersOnly(EntryPoint = "input_set_stick_axis")] + public static void SetStickAxisNative(StickInputId stick, Vector2 axes, int id) + { + SetStickAxis(stick, axes, id); + } + + [UnmanagedCallersOnly(EntryPoint = "input_connect_gamepad")] + public static IntPtr ConnectGamepadNative(int index) + { + return ConnectGamepad(index); + } + + } + + public class VirtualTouchScreen : IMouse + { + public Size ClientSize { get; set; } + + public bool[] Buttons { get; } + + public VirtualTouchScreen() + { + Buttons = new bool[2]; + } + + public Vector2 CurrentPosition { get; private set; } + public Vector2 Scroll { get; private set; } + public string Id => "0"; + public string Name => "AvaloniaMouse"; + + public bool IsConnected => true; + public GamepadFeaturesFlag Features => throw new NotImplementedException(); + + public void Dispose() + { + + } + + public GamepadStateSnapshot GetMappedStateSnapshot() + { + throw new NotImplementedException(); + } + + public void SetPosition(int x, int y) + { + CurrentPosition = new Vector2(x, y); + + Buttons[0] = true; + } + + public void ReleaseTouch() + { + Buttons[0] = false; + } + + public Vector3 GetMotionData(MotionInputId inputId) + { + throw new NotImplementedException(); + } + + public Vector2 GetPosition() + { + return CurrentPosition; + } + + public Vector2 GetScroll() + { + return Scroll; + } + + public GamepadStateSnapshot GetStateSnapshot() + { + throw new NotImplementedException(); + } + + public (float, float) GetStick(Ryujinx.Input.StickInputId inputId) + { + throw new NotImplementedException(); + } + + public bool IsButtonPressed(MouseButton button) + { + return Buttons[0]; + } + + 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 class VirtualTouchScreenDriver : IGamepadDriver + { + private readonly VirtualTouchScreen _virtualTouchScreen; + + public VirtualTouchScreenDriver(VirtualTouchScreen virtualTouchScreen) + { + _virtualTouchScreen = virtualTouchScreen; + } + + public string DriverName => "VirtualTouchDriver"; + + public ReadOnlySpan GamepadsIds => new[] { "0" }; + + + public event Action OnGamepadConnected + { + add { } + remove { } + } + + public event Action OnGamepadDisconnected + { + add { } + remove { } + } + + public void Dispose() + { + + } + + public IGamepad GetGamepad(string id) + { + return _virtualTouchScreen; + } + } + + public class VirtualGamepadDriver : IGamepadDriver + { + private readonly int _controllerCount; + + public ReadOnlySpan GamepadsIds => _gamePads.Keys.Select(x => x.ToString()).ToArray(); + + public string DriverName => "Virtual"; + + public event Action OnGamepadConnected; + public event Action OnGamepadDisconnected; + + private Dictionary _gamePads; + + public VirtualGamepadDriver(int controllerCount) + { + _gamePads = new Dictionary(); + for (int joystickIndex = 0; joystickIndex < controllerCount; joystickIndex++) + { + HandleJoyStickConnected(joystickIndex); + } + + _controllerCount = controllerCount; + } + + private void HandleJoyStickConnected(int joystickDeviceId) + { + _gamePads[joystickDeviceId] = new VirtualGamepad(this, joystickDeviceId); + OnGamepadConnected?.Invoke(joystickDeviceId.ToString()); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Simulate a full disconnect when disposing + var ids = GamepadsIds; + foreach (string id in ids) + { + OnGamepadDisconnected?.Invoke(id); + } + + _gamePads.Clear(); + } + } + + public void Dispose() + { + Dispose(true); + } + + public IGamepad GetGamepad(string id) + { + return _gamePads[int.Parse(id)]; + } + + public IGamepad GetGamepad(int index) + { + return _gamePads[index]; + } + + public void SetStickAxis(StickInputId stick, Vector2 axes, int deviceId) + { + if(_gamePads.TryGetValue(deviceId, out var gamePad)) + { + gamePad.StickInputs[(int)stick] = axes; + } + } + + public void SetButtonPressed(GamepadButtonInputId button, int deviceId) + { + if (_gamePads.TryGetValue(deviceId, out var gamePad)) + { + gamePad.ButtonInputs[(int)button] = true; + } + } + + public void SetButtonReleased(GamepadButtonInputId button, int deviceId) + { + if (_gamePads.TryGetValue(deviceId, out var gamePad)) + { + gamePad.ButtonInputs[(int)button] = false; + } + } + + public void SetAccelerometerData(Vector3 accel, int deviceId) + { + if (_gamePads.TryGetValue(deviceId, out var gamePad)) + { + gamePad.Accelerometer = accel; + } + } + + public void SetGryoData(Vector3 gyro, int deviceId) + { + if (_gamePads.TryGetValue(deviceId, out var gamePad)) + { + gamePad.Gyro = gyro; + } + } + } + + public class VirtualGamepad : IGamepad + { + private readonly VirtualGamepadDriver _driver; + + private bool[] _buttonInputs; + + private Vector2[] _stickInputs; + + public VirtualGamepad(VirtualGamepadDriver driver, int id) + { + _buttonInputs = new bool[(int)GamepadButtonInputId.Count]; + _stickInputs = new Vector2[(int)StickInputId.Count]; + _driver = driver; + Id = id.ToString(); + IdInt = id; + } + + public void Dispose() { } + + public GamepadFeaturesFlag Features { get; } = GamepadFeaturesFlag.Motion; + public string Id { get; } + + internal readonly int IdInt; + + public string Name => Id; + public bool IsConnected { get; } + public Vector2[] StickInputs { get => _stickInputs; set => _stickInputs = value; } + public bool[] ButtonInputs { get => _buttonInputs; set => _buttonInputs = value; } + public Vector3 Accelerometer { get; internal set; } + public Vector3 Gyro { get; internal set; } + + public bool IsPressed(GamepadButtonInputId inputId) + { + return _buttonInputs[(int)inputId]; + } + + public (float, float) GetStick(StickInputId inputId) + { + var v = _stickInputs[(int)inputId]; + + return (v.X, v.Y); + } + + public Vector3 GetMotionData(MotionInputId inputId) + { + if (inputId == MotionInputId.Accelerometer) + return Accelerometer; + else if (inputId == MotionInputId.Gyroscope) + return RadToDegree(Gyro); + return new Vector3(); + } + + private static Vector3 RadToDegree(Vector3 rad) + { + return rad * (180 / MathF.PI); + } + + public void SetTriggerThreshold(float triggerThreshold) + { + //throw new System.NotImplementedException(); + } + + public void SetConfiguration(InputConfig configuration) + { + //throw new System.NotImplementedException(); + } + + public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + //throw new System.NotImplementedException(); + } + + public GamepadStateSnapshot GetMappedStateSnapshot() + { + GamepadStateSnapshot result = default; + + foreach (var button in Enum.GetValues()) + { + // Do not touch state of button already pressed + if (button != GamepadButtonInputId.Count && !result.IsPressed(button)) + { + result.SetPressed(button, IsPressed(button)); + } + } + + (float leftStickX, float leftStickY) = GetStick(StickInputId.Left); + (float rightStickX, float rightStickY) = GetStick(StickInputId.Right); + + result.SetStick(StickInputId.Left, leftStickX, leftStickY); + result.SetStick(StickInputId.Right, rightStickX, rightStickY); + + return result; + } + + public GamepadStateSnapshot GetStateSnapshot() + { + return new GamepadStateSnapshot(); + } + } +} diff --git a/src/LibRyujinx/LibRyujinx.User.cs b/src/LibRyujinx/LibRyujinx.User.cs new file mode 100644 index 000000000..f78ee22be --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.User.cs @@ -0,0 +1,82 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Linq; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + public static string GetOpenedUser() + { + var lastProfile = SwitchDevice?.AccountManager.LastOpenedUser; + + return lastProfile?.UserId.ToString() ?? ""; + } + + public static string GetUserPicture(string userId) + { + var uid = new UserId(userId); + + var user = SwitchDevice?.AccountManager.GetAllUsers().FirstOrDefault(x => x.UserId == uid); + + if (user == null) + return ""; + + var pic = user.Image; + + return pic != null ? Convert.ToBase64String(pic) : ""; + } + + public static void SetUserPicture(string userId, string picture) + { + var uid = new UserId(userId); + + SwitchDevice?.AccountManager.SetUserImage(uid, Convert.FromBase64String(picture)); + } + + public static string GetUserName(string userId) + { + var uid = new UserId(userId); + + var user = SwitchDevice?.AccountManager.GetAllUsers().FirstOrDefault(x => x.UserId == uid); + + return user?.Name ?? ""; + } + + public static void SetUserName(string userId, string name) + { + var uid = new UserId(userId); + + SwitchDevice?.AccountManager.SetUserName(uid, name); + } + + public static string[] GetAllUsers() + { + return SwitchDevice?.AccountManager.GetAllUsers().Select(x => x.UserId.ToString()).ToArray() ?? + Array.Empty(); + } + + public static void AddUser(string userName, string picture) + { + SwitchDevice?.AccountManager.AddUser(userName, Convert.FromBase64String(picture)); + } + + public static void DeleteUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.DeleteUser(uid); + } + + public static void OpenUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.OpenUser(uid); + } + + public static void CloseUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.CloseUser(uid); + } + } +} diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs new file mode 100644 index 000000000..5aa058e11 --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.cs @@ -0,0 +1,849 @@ +// State class for the library +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.HOS; +using Ryujinx.Input.HLE; +using Ryujinx.HLE; +using System; +using System.Runtime.InteropServices; +using Ryujinx.Common.Configuration; +using LibHac.Tools.FsSystem; +using Ryujinx.Graphics.GAL.Multithreading; +using Ryujinx.Audio.Backends.Dummy; +using Ryujinx.HLE.HOS.SystemState; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Backends.SDL2; +using System.IO; +using LibHac.Common.Keys; +using LibHac.Common; +using LibHac.Ns; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Fs; +using Path = System.IO.Path; +using LibHac; +using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.Common.Utilities; +using System.Globalization; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.Common.Logging.Targets; +using System.Collections.Generic; +using System.Text; +using Ryujinx.HLE.UI; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + internal static IHardwareDeviceDriver AudioDriver { get; set; } = new DummyHardwareDeviceDriver(); + + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public static SwitchDevice? SwitchDevice { get; set; } + + [UnmanagedCallersOnly(EntryPoint = "initialize")] + public static bool Initialize(IntPtr basePathPtr) + { + var path = Marshal.PtrToStringAnsi(basePathPtr); + + var res = Initialize(path); + + InitializeAudio(); + + return res; + } + + public static bool Initialize(string? basePath) + { + if (SwitchDevice != null) + { + return false; + } + + try + { + AppDataManager.Initialize(basePath); + + ConfigurationState.Initialize(); + LoggerModule.Initialize(); + + string logDir = Path.Combine(AppDataManager.BaseDirPath, "Logs"); + FileStream logFile = FileLogTarget.PrepareLogFile(logDir); + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000, + AsyncLogTargetOverflowAction.Block + )); + SwitchDevice = new SwitchDevice(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + return false; + } + + return true; + } + + public static void InitializeAudio() + { + AudioDriver = new SDL2HardwareDeviceDriver(); + } + + public static GameStats GetGameStats() + { + if (SwitchDevice?.EmulationContext == null) + return new GameStats(); + + var context = SwitchDevice.EmulationContext; + + return new GameStats + { + Fifo = context.Statistics.GetFifoPercent(), + GameFps = context.Statistics.GetGameFrameRate(), + GameTime = context.Statistics.GetGameFrameTime() + }; + } + + + public static GameInfo? GetGameInfo(string? file) + { + if (string.IsNullOrWhiteSpace(file)) + { + return new GameInfo(); + } + + Logger.Info?.Print(LogClass.Application, $"Getting game info for file: {file}"); + + using var stream = File.Open(file, FileMode.Open); + + return GetGameInfo(stream, new FileInfo(file).Extension.Remove('.')); + } + + [UnmanagedCallersOnly(EntryPoint = "get_game_info")] + public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr) + { + var extension = Marshal.PtrToStringAnsi(extensionPtr); + var stream = OpenFile(descriptor); + + var gameInfo = GetGameInfo(stream, extension); + + return new GameInfoNative(gameInfo.FileSize, gameInfo.TitleName, gameInfo.TitleId, gameInfo.Developer, gameInfo.Version, gameInfo.Icon); + } + + public static GameInfo? GetGameInfo(Stream gameStream, string extension) + { + if (SwitchDevice == null) + { + Logger.Error?.Print(LogClass.Application, "SwitchDevice is not initialized."); + return null; + } + GameInfo gameInfo = GetDefaultInfo(gameStream); + + const Language TitleLanguage = Language.AmericanEnglish; + + BlitStruct controlHolder = new(1); + + try + { + try + { + if (extension == "nsp" || extension == "pfs0" || extension == "xci") + { + IFileSystem pfs; + + bool isExeFs = false; + + if (extension == "xci") + { + Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + return null; + } + } + + if (isExeFs) + { + using UniqueRef npdmFile = new(); + + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + gameInfo.TitleName = npdm.TitleName; + gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id); + + gameInfo.TitleId = id; + + if (controlFs == null) + { + Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}"); + return null; + } + + // Check if there is an update available. + if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + gameInfo.Icon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + gameInfo.Icon = stream.ToArray(); + + if (gameInfo.Icon != null) + { + break; + } + } + + } + } + } + else if (extension == "nro") + { + BinaryReader reader = new(gameStream); + + byte[] Read(long position, int size) + { + gameStream.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + gameStream.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + gameInfo.Icon = Read(assetOffset + iconOffset, (int)iconSize); + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version); + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException exception) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. {exception}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The gameStream encountered was not of a valid type. Error: {exception}"); + + return null; + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + + void ReadControlData(IFileSystem? controlFs, Span outProperty) + { + using UniqueRef controlFile = new(); + + controlFs?.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); + } + + void GetGameInformation(ref ApplicationControlProperty controlData, out string? titleName, out string titleId, out string? publisher, out string? version) + { + _ = Enum.TryParse(TitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + + if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) + { + titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + } + else + { + titleName = null; + publisher = null; + } + + if (string.IsNullOrWhiteSpace(titleName)) + { + foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) + { + if (!controlTitle.NameString.IsEmpty()) + { + titleName = controlTitle.NameString.ToString(); + + break; + } + } + } + + if (string.IsNullOrWhiteSpace(publisher)) + { + foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) + { + if (!controlTitle.PublisherString.IsEmpty()) + { + publisher = controlTitle.PublisherString.ToString(); + + break; + } + } + } + + if (controlData.PresenceGroupId != 0) + { + titleId = controlData.PresenceGroupId.ToString("x16"); + } + else if (controlData.SaveDataOwnerId != 0) + { + titleId = controlData.SaveDataOwnerId.ToString(); + } + else if (controlData.AddOnContentBaseId != 0) + { + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; + } + + version = controlData.DisplayVersionString.ToString(); + } + + void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem? controlFs, out string? titleId) + { + if (SwitchDevice == null) + { + Logger.Error?.Print(LogClass.Application, "SwitchDevice is not initialized."); + + controlFs = null; + titleId = null; + return; + } + (_, _, Nca? controlNca) = GetGameData(SwitchDevice.VirtualFileSystem, pfs, 0); + + if (controlNca == null) + { + Logger.Warning?.Print(LogClass.Application, "Control NCA is null. Unable to load control FS."); + } + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + + (Nca? mainNca, Nca? patchNca, Nca? controlNca) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) + { + Nca? mainNca = null; + Nca? patchNca = null; + Nca? controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (mainNca, patchNca, controlNca); + } + + bool IsUpdateApplied(string? titleId, out IFileSystem? updatedControlFs) + { + updatedControlFs = null; + + string? updatePath = "(unknown)"; + + if (SwitchDevice?.VirtualFileSystem == null) + { + Logger.Error?.Print(LogClass.Application, "SwitchDevice was not initialized."); + return false; + } + + try + { + (Nca? patchNca, Nca? controlNca) = GetGameUpdateData(SwitchDevice.VirtualFileSystem, titleId, 0, out updatePath); + + if (patchNca != null && controlNca != null) + { + updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + + return true; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); + } + + return false; + } + + (Nca? patch, Nca? control) GetGameUpdateData(VirtualFileSystem fileSystem, string? titleId, int programIndex, out string? updatePath) + { + updatePath = null; + + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } + + (Nca? patchNca, Nca? controlNca) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + { + Nca? patchNca = null; + Nca? controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (patchNca, controlNca); + } + + return gameInfo; + } + + private static GameInfo GetDefaultInfo(Stream gameStream) + { + return new GameInfo + { + FileSize = gameStream.Length * 0.000000000931, + TitleName = "Unknown", + TitleId = "0000000000000000", + Developer = "Unknown", + Version = "0", + Icon = null + }; + } + + public static string GetDlcTitleId(string path, string ncaPath) + { + if (File.Exists(path)) + { + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + SwitchDevice.VirtualFileSystem.ImportTickets(partitionFileSystem); + + using UniqueRef ncaFile = new(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, ncaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), ncaPath); + if (nca != null) + { + return nca.Header.TitleId.ToString("X16"); + } + } + + return string.Empty; + } + + + private static Nca TryOpenNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(SwitchDevice.VirtualFileSystem.KeySet, ncaStorage); + } + catch (Exception ex) + { + } + + return null; + } + + public static List GetDlcContentList(string path, ulong titleId) + { + if (!File.Exists(path)) + return new List(); + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + SwitchDevice.VirtualFileSystem.ImportTickets(partitionFileSystem); + List paths = new List(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != titleId) + { + break; + } + + paths.Add(fileEntry.FullPath); + } + } + + return paths; + } + } + + public class SwitchDevice : IDisposable + { + private readonly SystemVersion _firmwareVersion; + public VirtualFileSystem VirtualFileSystem { get; set; } + public ContentManager ContentManager { get; set; } + public AccountManager AccountManager { get; set; } + public LibHacHorizonManager LibHacHorizonManager { get; set; } + public UserChannelPersistence UserChannelPersistence { get; set; } + public InputManager? InputManager { get; set; } + public Switch? EmulationContext { get; set; } + public IHostUIHandler? HostUiHandler { get; set; } + + public void Dispose() + { + GC.SuppressFinalize(this); + + VirtualFileSystem.Dispose(); + InputManager?.Dispose(); + EmulationContext?.Dispose(); + } + + public SwitchDevice() + { + VirtualFileSystem = VirtualFileSystem.CreateInstance(); + LibHacHorizonManager = new LibHacHorizonManager(); + + LibHacHorizonManager.InitializeFsServer(VirtualFileSystem); + LibHacHorizonManager.InitializeArpServer(); + LibHacHorizonManager.InitializeBcatServer(); + LibHacHorizonManager.InitializeSystemClients(); + + ContentManager = new ContentManager(VirtualFileSystem); + AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient); + UserChannelPersistence = new UserChannelPersistence(); + + _firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); + + if (_firmwareVersion != null) + { + Logger.Notice.Print(LogClass.Application, $"System Firmware Version: {_firmwareVersion.VersionString}"); + } + else + { + Logger.Notice.Print(LogClass.Application, $"System Firmware not installed"); + } + } + + public bool InitializeContext(bool isHostMapped, + bool useHypervisor, + SystemLanguage systemLanguage, + RegionCode regionCode, + bool enableVsync, + bool enableDockedMode, + bool enablePtc, + bool enableInternetAccess, + string? timeZone, + bool ignoreMissingServices) + { + if (LibRyujinx.Renderer == null) + { + return false; + } + + var renderer = LibRyujinx.Renderer; + BackendThreading threadingMode = LibRyujinx.GraphicsConfiguration.BackendThreading; + + bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading); + + if (threadedGAL) + { + renderer = new ThreadedRenderer(renderer); + } + + HLEConfiguration configuration = new HLEConfiguration(VirtualFileSystem, + LibHacHorizonManager, + ContentManager, + AccountManager, + UserChannelPersistence, + renderer, + LibRyujinx.AudioDriver, //Audio + MemoryConfiguration.MemoryConfiguration4GiB, + HostUiHandler, + systemLanguage, + regionCode, + enableVsync, + enableDockedMode, + enablePtc, + enableInternetAccess, + IntegrityCheckLevel.None, + 0, + 0, + timeZone, + isHostMapped ? MemoryManagerMode.HostMappedUnsafe : MemoryManagerMode.SoftwarePageTable, + ignoreMissingServices, + LibRyujinx.GraphicsConfiguration.AspectRatio, + 100, + useHypervisor, + "", + Ryujinx.Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + + EmulationContext = new Switch(configuration); + + return true; + } + + internal void ReloadFileSystem() + { + VirtualFileSystem.ReloadKeySet(); + ContentManager = new ContentManager(VirtualFileSystem); + AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient); + } + + internal void DisposeContext() + { + EmulationContext?.Dispose(); + EmulationContext?.DisposeGpu(); + EmulationContext = null; + LibRyujinx.Renderer = null; + } + } + + public class GameInfo + { + public double FileSize; + public string? TitleName; + public string? TitleId; + public string? Developer; + public string? Version; + public byte[]? Icon; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct GameInfoNative + { + public double FileSize; + public char* TitleName; + public char* TitleId; + public char* Developer; + public char* Version; + public char* Icon; + + public GameInfoNative() + { + + } + + public GameInfoNative(double fileSize, string? titleName, string? titleId, string? developer, string? version, byte[]? icon) + { + FileSize = fileSize; + TitleId = (char*)Marshal.StringToHGlobalAnsi(titleId); + Version = (char*)Marshal.StringToHGlobalAnsi(version); + Developer = (char*)Marshal.StringToHGlobalAnsi(developer); + TitleName = (char*)Marshal.StringToHGlobalAnsi(titleName); + + if (icon != null) + { + Icon = (char*)Marshal.StringToHGlobalAnsi(Convert.ToBase64String(icon)); + } + else + { + Icon = (char*)0; + } + } + + public GameInfoNative(GameInfo info) : this(info.FileSize, info.TitleName, info.TitleId, info.Developer, info.Version, info.Icon){} + } + + public class GameStats + { + public double Fifo; + public double GameFps; + public double GameTime; + } +} diff --git a/src/LibRyujinx/LibRyujinx.csproj b/src/LibRyujinx/LibRyujinx.csproj new file mode 100644 index 000000000..9f5d630f8 --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.csproj @@ -0,0 +1,36 @@ + + + net8.0 + enable + $(DefineConstants);FORCE_EXTERNAL_BASE_DIR + + + true + true + true + true + false + + + Speed + + + + + + + + + + + + + + + + + + + + + diff --git a/src/LibRyujinx/OpenTKBindingsContext.cs b/src/LibRyujinx/OpenTKBindingsContext.cs new file mode 100644 index 000000000..203caee8b --- /dev/null +++ b/src/LibRyujinx/OpenTKBindingsContext.cs @@ -0,0 +1,20 @@ +using OpenTK; +using System; + +namespace LibRyujinx.Shared +{ + public class OpenTKBindingsContext : IBindingsContext + { + private readonly Func _getProcAddress; + + public OpenTKBindingsContext(Func getProcAddress) + { + _getProcAddress = getProcAddress; + } + + public IntPtr GetProcAddress(string procName) + { + return _getProcAddress(procName); + } + } +} \ No newline at end of file diff --git a/src/LibRyujinx/rd.xml b/src/LibRyujinx/rd.xml new file mode 100644 index 000000000..b1cecb007 --- /dev/null +++ b/src/LibRyujinx/rd.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index 774ae012a..411706545 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; namespace Ryujinx.Common { @@ -26,6 +27,6 @@ namespace Ryujinx.Common public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannelOwner); - public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; + public static string Version => IsValid ? BuildVersion : !RuntimeFeature.IsDynamicCodeCompiled ? "libryujinx" : Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs b/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs index 496a90fbe..167cd355d 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Silk.NET.Core; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.EXT; using System; @@ -16,6 +17,7 @@ namespace Ryujinx.Graphics.Vulkan private readonly ExtDebugUtils _debugUtils; private readonly DebugUtilsMessengerEXT? _debugUtilsMessenger; private bool _disposed; + private unsafe delegate* unmanaged[Cdecl] _messageDelegate; public VulkanDebugMessenger(Vk api, Instance instance, GraphicsDebugLevel logLevel) { @@ -71,7 +73,8 @@ namespace Ryujinx.Graphics.Vulkan unsafe { - debugUtilsMessengerCreateInfo.PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(UserCallback); + _messageDelegate = (delegate* unmanaged[Cdecl])Marshal.GetFunctionPointerForDelegate(UserCallback); + debugUtilsMessengerCreateInfo.PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(_messageDelegate); } DebugUtilsMessengerEXT messengerHandle = default; @@ -89,7 +92,7 @@ namespace Ryujinx.Graphics.Vulkan return Result.Success; } - private unsafe static uint UserCallback( + private unsafe static Bool32 UserCallback( DebugUtilsMessageSeverityFlagsEXT messageSeverity, DebugUtilsMessageTypeFlagsEXT messageTypes, DebugUtilsMessengerCallbackDataEXT* pCallbackData, diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 33e41ab48..4e3f73fca 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -74,6 +74,8 @@ namespace Ryujinx.Graphics.Vulkan public IWindow Window => _window; + public SurfaceTransformFlagsKHR CurrentTransform => _window.CurrentTransform; + private readonly Func _getSurface; private readonly Func _getRequiredExtensions; private readonly string _preferredGpuId; @@ -978,6 +980,15 @@ namespace Ryujinx.Graphics.Vulkan return !(IsMoltenVk || IsQualcommProprietary); } + internal unsafe void RecreateSurface() + { + SurfaceApi.DestroySurface(_instance.Instance, _surface, null); + + _surface = _getSurface(_instance.Instance, Api); + + (_window as Window)?.SetSurface(_surface); + } + public unsafe void Dispose() { if (!_initialized) diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index d67362be3..bb7be7657 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -14,10 +14,10 @@ namespace Ryujinx.Graphics.Vulkan private const int SurfaceHeight = 720; private readonly VulkanRenderer _gd; - private readonly SurfaceKHR _surface; private readonly PhysicalDevice _physicalDevice; private readonly Device _device; private SwapchainKHR _swapchain; + private SurfaceKHR _surface; private Image[] _swapchainImages; private TextureView[] _swapchainImageViews; @@ -84,6 +84,12 @@ namespace Ryujinx.Graphics.Vulkan CreateSwapchain(); } + internal void SetSurface(SurfaceKHR surface) + { + _surface = surface; + RecreateSwapchain(); + } + private unsafe void CreateSwapchain() { _gd.SurfaceApi.GetPhysicalDeviceSurfaceCapabilities(_physicalDevice, _surface, out var capabilities); @@ -126,6 +132,8 @@ namespace Ryujinx.Graphics.Vulkan var oldSwapchain = _swapchain; + CurrentTransform = capabilities.CurrentTransform; + var swapchainCreateInfo = new SwapchainCreateInfoKHR { SType = StructureType.SwapchainCreateInfoKhr, @@ -332,6 +340,10 @@ namespace Ryujinx.Graphics.Vulkan RecreateSwapchain(); semaphoreIndex = (_frameIndex - 1) % _imageAvailableSemaphores.Length; } + else if(acquireResult == Result.ErrorSurfaceLostKhr) + { + _gd.RecreateSurface(); + } else { acquireResult.ThrowOnError(); @@ -481,6 +493,9 @@ namespace Ryujinx.Graphics.Vulkan { _gd.SwapchainApi.QueuePresent(_gd.Queue, in presentInfo); } + + //While this does nothing in most cases, it's useful to notify the end of the frame. + swapBuffersCallback?.Invoke(); } public override void SetAntiAliasing(AntiAliasing effect) diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c..f8c7d053a 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -1,4 +1,5 @@ using Ryujinx.Graphics.GAL; +using Silk.NET.Vulkan; using System; namespace Ryujinx.Graphics.Vulkan @@ -7,6 +8,8 @@ namespace Ryujinx.Graphics.Vulkan { public bool ScreenCaptureRequested { get; set; } + public SurfaceTransformFlagsKHR CurrentTransform { get; set; } + public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height);