diff --git a/Ryujinx.sln b/Ryujinx.sln index 24def42a3..b89d5da0a 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "src\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}" @@ -555,6 +557,18 @@ Global {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ryujinx.Graphics.RenderDocApi/Capture.cs b/src/Ryujinx.Graphics.RenderDocApi/Capture.cs new file mode 100644 index 000000000..dd75bc120 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/Capture.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public readonly record struct Capture(int Index, string FileName, DateTime Timestamp) + { + public void SetComments(string comments) + { + RenderDoc.SetCaptureFileComments(FileName, comments); + } + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs new file mode 100644 index 000000000..dbb8494a8 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs @@ -0,0 +1,22 @@ +// ReSharper disable UnusedMember.Global + +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum CaptureOption + { + AllowVsync = 0, + AllowFullscreen = 1, + ApiValidation = 2, + CaptureCallstacks = 3, + CaptureCallstacksOnlyDraws = 4, + DelayForDebugger = 5, + VerifyBufferAccess = 6, + HookIntoChildren = 7, + RefAllSources = 8, + SaveAllInitials = 9, + CaptureAllCmdLists = 10, + DebugOutputMute = 11, + AllowUnsupportedVendorExtensions = 12, + SoftMemoryLimit = 13, + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs b/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs new file mode 100644 index 000000000..adef8e8e7 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs @@ -0,0 +1,83 @@ + +// ReSharper disable UnusedMember.Global +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum InputButton + { + // '0' - '9' matches ASCII values + Key0 = 0x30, + Key1 = 0x31, + Key2 = 0x32, + Key3 = 0x33, + Key4 = 0x34, + Key5 = 0x35, + Key6 = 0x36, + Key7 = 0x37, + Key8 = 0x38, + Key9 = 0x39, + + // 'A' - 'Z' matches ASCII values + A = 0x41, + B = 0x42, + C = 0x43, + D = 0x44, + E = 0x45, + F = 0x46, + G = 0x47, + H = 0x48, + I = 0x49, + J = 0x4A, + K = 0x4B, + L = 0x4C, + M = 0x4D, + N = 0x4E, + O = 0x4F, + P = 0x50, + Q = 0x51, + R = 0x52, + S = 0x53, + T = 0x54, + U = 0x55, + V = 0x56, + W = 0x57, + X = 0x58, + Y = 0x59, + Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + NonPrintable = 0x100, + + Divide, + Multiply, + Subtract, + Plus, + + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + + Home, + End, + Insert, + Delete, + PageUp, + PageDn, + + Backspace, + Tab, + PrtScrn, + Pause, + + Max, + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs new file mode 100644 index 000000000..1b5e41829 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs @@ -0,0 +1,19 @@ +// ReSharper disable UnusedMember.Global + +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + [Flags] + public enum OverlayBits + { + Enabled = 1 << 0, + FrameRate = 1 << 1, + FrameNumber = 1 << 2, + CaptureList = 1 << 3, + + Default = Enabled | FrameRate | FrameNumber | CaptureList, + All = ~0, + None = 0 + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/README.md b/src/Ryujinx.Graphics.RenderDocApi/README.md new file mode 100644 index 000000000..d134c57d5 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/README.md @@ -0,0 +1,5 @@ +# Ryujinx.Graphics.RenderDoc + +This is a C# binding for RenderDoc's application API. +This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp. +I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience. \ No newline at end of file diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs new file mode 100644 index 000000000..8a7d2ec21 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -0,0 +1,335 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public static unsafe class RenderDoc + { + /// + /// True if the API is available. + /// + public static bool IsAvailable => Api != null; + + /// + /// Set the minimum version of the API you require. + /// + /// Set this before you do anything else with the RenderDoc API, including . + public static Version MinimumRequired { get; set; } = new Version(1, 0, 0); + + /// + /// Set to true to assert versions. + /// + public static bool AssertVersionEnabled { get; set; } = true; + + /// + /// Version of the API available. + /// + [MemberNotNullWhen(true, nameof(IsAvailable))] + public static Version? Version + { + get + { + if (!IsAvailable) + return null; + + int major, minor, build; + Api->GetApiVersion(&major, &minor, &build); + return new Version(major, minor, build); + } + } + + [RenderDocApiVersion(1, 0)] + public static OverlayBits OverlayBits + { + get => Api->GetOverlayBits(); + set + { + Api->MaskOverlayBits(~value, value); + } + } + + [RenderDocApiVersion(1, 0)] + public static string CaptureFilePathTemplate + { + get + { + byte* ptr = Api->GetCaptureFilePathTemplate(); + return Marshal.PtrToStringUTF8((IntPtr)ptr)!; + } + set + { + fixed (byte* ptr = Encoding.UTF8.GetBytes(value + '\0')) + { + Api->SetCaptureFilePathTemplate(ptr); + } + } + } + + [RenderDocApiVersion(1, 0)] public static int NumCaptures => Api->GetNumCaptures(); + + [RenderDocApiVersion(1, 0)] public static bool IsTargetControlConnected => Api->IsTargetControlConnected() != 0; + + [RenderDocApiVersion(1, 0)] public static bool IsFrameCapturing => Api->IsFrameCapturing() != 0; + + [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, int integer) + { + return Api->SetCaptureOptionU32(option, integer) != 0; + } + + [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, float single) + { + return Api->SetCaptureOptionF32(option, single) != 0; + } + + [RenderDocApiVersion(1, 0)] + public static void GetCaptureOption(CaptureOption option, out int integer) + { + integer = Api->GetCaptureOptionU32(option); + } + + [RenderDocApiVersion(1, 0)] + public static void GetCaptureOption(CaptureOption option, out float single) + { + single = Api->GetCaptureOptionF32(option); + } + + [RenderDocApiVersion(1, 0)] + public static int GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); + + [RenderDocApiVersion(1, 0)] + public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option); + + [RenderDocApiVersion(1, 0)] + public static void SetFocusToggleKeys(ReadOnlySpan buttons) + { + fixed (InputButton* ptr = buttons) + { + Api->SetFocusToggleKeys(ptr, buttons.Length); + } + } + + [RenderDocApiVersion(1, 0)] + public static void SetCaptureKeys(ReadOnlySpan buttons) + { + fixed (InputButton* ptr = buttons) + { + Api->SetCaptureKeys(ptr, buttons.Length); + } + } + + [RenderDocApiVersion(1, 0)] + public static void RemoveHooks() + { + Api->RemoveHooks(); + } + + [RenderDocApiVersion(1, 0)] + public static void UnloadCrashHandler() + { + Api->UnloadCrashHandler(); + } + + [RenderDocApiVersion(1, 0)] + public static void TriggerCapture() + { + Api->TriggerCapture(); + } + + [RenderDocApiVersion(1, 0)] + public static Capture? GetCapture(int index) + { + int length = 0; + if (Api->GetCapture(index, null, &length, null) == 0) + { + return null; + } + + Span bytes = stackalloc byte[length + 1]; + long timestamp; + + fixed (byte* ptr = bytes) + Api->GetCapture(index, ptr, &length, ×tamp); + + string fileName = Encoding.UTF8.GetString(bytes.Slice(length)); + return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime); + } + + [RenderDocApiVersion(1, 0)] + public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine) + { + if (commandLine == null) + { + return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, null) != 0; + } + + fixed (byte* ptr = Encoding.UTF8.GetBytes(commandLine + '\0')) + { + return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, ptr) != 0; + } + } + + [RenderDocApiVersion(1, 0)] + public static void SetActiveWindow(nint hDevice, nint hWindow) + { + Api->SetActiveWindow((void*)hDevice, (void*)hWindow); + } + + [RenderDocApiVersion(1, 0)] + public static void StartFrameCapture(nint hDevice, nint hWindow) + { + Api->StartFrameCapture((void*)hDevice, (void*)hWindow); + } + + [RenderDocApiVersion(1, 0)] + public static bool EndFrameCapture(nint hDevice, nint hWindow) + { + return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0; + } + + [RenderDocApiVersion(1, 1)] + public static void TriggerMultiFrameCapture(int numFrames) + { + AssertAtLeast(1, 1); + Api->TriggerMultiFrameCapture(numFrames); + } + + [RenderDocApiVersion(1, 2)] + public static void SetCaptureFileComments(string fileName, string comments) + { + AssertAtLeast(1, 2); + + byte[] fileBytes = Encoding.UTF8.GetBytes(fileName + '\0'); + byte[] commentBytes = Encoding.UTF8.GetBytes(comments + '\0'); + + fixed (byte* pfile = fileBytes) + fixed (byte* pcomment = commentBytes) + { + Api->SetCaptureFileComments(pfile, pcomment); + } + } + + [RenderDocApiVersion(1, 3)] + public static void DiscardFrameCapture(nint hdevice, nint hWindow) + { + AssertAtLeast(1, 3); + Api->DiscardFrameCapture((void*)hdevice, (void*)hWindow); + } + + [RenderDocApiVersion(1, 5)] + public static bool ShowReplayUI() + { + AssertAtLeast(1, 5); + return Api->ShowReplayUI() != 0; + } + + [RenderDocApiVersion(1, 6)] + public static void SetCaptureTitle(string title) + { + AssertAtLeast(1, 6); + fixed (byte* ptr = Encoding.UTF8.GetBytes(title + '\0')) + Api->SetCaptureTitle(ptr); + } + + public static void ReloadApi(bool ignoreAlreadyLoaded = false) + { + if (_loaded && !ignoreAlreadyLoaded) + return; + + lock (typeof(RenderDoc)) + { + // Prevent double loads. + if (_loaded && !ignoreAlreadyLoaded) + return; + + _loaded = true; + _api = GetApi(SystemVersionToRenderdocVersion(MinimumRequired)); + + if (_api != null) + AssertAtLeast(MinimumRequired.Major, MinimumRequired.Minor, MinimumRequired.Build); + } + } + + private static RenderDocApi* _api = null; + private static bool _loaded = false; + + private static RenderDocApi* Api + { + get + { + ReloadApi(); + return _api; + } + } + + private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0) + { + Regex re = new Regex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + foreach (ProcessModule module in Process.GetCurrentProcess().Modules) + { + string moduleName = module.FileName ?? string.Empty; + + if (!re.IsMatch(moduleName)) + continue; + + if (!NativeLibrary.TryLoad(moduleName, out IntPtr moduleHandle)) + return null; + + if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out IntPtr procAddress)) + return null; + + var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl])procAddress; + + RenderDocApi* api; + return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null; + } + + return null; + } + + private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "") + { + if (!AssertVersionEnabled) + return; + + if (Version!.Major < major) + goto fail; + + if (Version.Major > major) + goto success; + if (Version.Minor < minor) + goto fail; + if (Version.Minor > minor) + goto success; + if (Version.Build < patch) + goto fail; + + success: + return; + + fail: + Version minVersion = + typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute()!.MinVersion; + throw new NotSupportedException( + $"This API was introduced in RenderdocAPI {minVersion}. Current API version is {Version}."); + } + + private static Version RenderdocVersionToSystemVersion(RenderDocVersion version) + { + int i = (int)version; + return new Version(i / 10000, (i % 10000) / 100, i % 100); + } + + private static RenderDocVersion SystemVersionToRenderdocVersion(Version version) => + (RenderDocVersion)(version.Major * 10000 + version.Minor * 100 + version.Build); + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs new file mode 100644 index 000000000..539679932 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs @@ -0,0 +1,51 @@ +namespace Ryujinx.Graphics.RenderDocApi +{ +#pragma warning disable CS0649 + internal unsafe struct RenderDocApi + { + public delegate* unmanaged[Cdecl] GetApiVersion; + + public delegate* unmanaged[Cdecl] SetCaptureOptionU32; + public delegate* unmanaged[Cdecl] SetCaptureOptionF32; + public delegate* unmanaged[Cdecl] GetCaptureOptionU32; + public delegate* unmanaged[Cdecl] GetCaptureOptionF32; + + public delegate* unmanaged[Cdecl] SetFocusToggleKeys; + public delegate* unmanaged[Cdecl] SetCaptureKeys; + + public delegate* unmanaged[Cdecl] GetOverlayBits; + public delegate* unmanaged[Cdecl] MaskOverlayBits; + + public delegate* unmanaged[Cdecl] RemoveHooks; + public delegate* unmanaged[Cdecl] UnloadCrashHandler; + public delegate* unmanaged[Cdecl] SetCaptureFilePathTemplate; + public delegate* unmanaged[Cdecl] GetCaptureFilePathTemplate; + + public delegate* unmanaged[Cdecl] GetNumCaptures; + public delegate* unmanaged[Cdecl] GetCapture; + public delegate* unmanaged[Cdecl] TriggerCapture; + public delegate* unmanaged[Cdecl] IsTargetControlConnected; + public delegate* unmanaged[Cdecl] LaunchReplayUI; + + public delegate* unmanaged[Cdecl] SetActiveWindow; + public delegate* unmanaged[Cdecl] StartFrameCapture; + public delegate* unmanaged[Cdecl] IsFrameCapturing; + public delegate* unmanaged[Cdecl] EndFrameCapture; + + // 1.1 + public delegate* unmanaged[Cdecl] TriggerMultiFrameCapture; + + // 1.2 + public delegate* unmanaged[Cdecl] SetCaptureFileComments; + + // 1.3 + public delegate* unmanaged[Cdecl] DiscardFrameCapture; + + // 1.5 + public delegate* unmanaged[Cdecl] ShowReplayUI; + + // 1.6 + public delegate* unmanaged[Cdecl] SetCaptureTitle; + } +#pragma warning restore CS0649 +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs new file mode 100644 index 000000000..ffbe3701e --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs @@ -0,0 +1,16 @@ + +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public sealed class RenderDocApiVersionAttribute : Attribute + { + public Version MinVersion { get; } + + public RenderDocApiVersionAttribute(int major, int minor, int patch = 0) + { + MinVersion = new Version(major, minor, patch); + } + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs new file mode 100644 index 000000000..f5aa8fed7 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs @@ -0,0 +1,19 @@ +namespace Ryujinx.Graphics.RenderDocApi +{ + internal enum RenderDocVersion : int + { + Version_1_0_0 = 10000, + Version_1_0_1 = 10001, + Version_1_0_2 = 10002, + Version_1_1_0 = 10100, + Version_1_1_1 = 10101, + Version_1_1_2 = 10102, + Version_1_2_0 = 10200, + Version_1_3_0 = 10300, + Version_1_4_0 = 10400, + Version_1_4_1 = 10401, + Version_1_4_2 = 10402, + Version_1_5_0 = 10500, + Version_1_6_0 = 10600, + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj new file mode 100644 index 000000000..4598335eb --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + disable + enable + true + + + + + + + + diff --git a/src/Ryujinx.Graphics.Vulkan/Helpers.cs b/src/Ryujinx.Graphics.Vulkan/Helpers.cs new file mode 100644 index 000000000..d29ac3440 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/Helpers.cs @@ -0,0 +1,32 @@ +using Silk.NET.Vulkan; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Vulkan +{ + public static class Helpers + { + extension(Vk api) + { + /// + /// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#. + /// + /// The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the 's pointer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void* GetRenderDocDevicePointer() => + api.CurrentInstance is not null + ? api.CurrentInstance.Value.GetRenderDocDevicePointer() + : null; + } + + extension(Instance instance) + { + /// + /// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#. + /// + /// The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the 's pointer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void* GetRenderDocDevicePointer() + => (*((void**)(instance.Handle))); + } + } +} diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index d77e79756..8d03f81da 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -18,6 +18,7 @@ using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.SystemInterop; using Ryujinx.Common.Utilities; +using Ryujinx.Graphics.RenderDocApi; using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Headless; using Ryujinx.SDL3.Common; diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index ddb013412..a8b4b8628 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -78,7 +78,7 @@ - + @@ -86,7 +86,6 @@ - diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index e360d42f7..bd9cfae51 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -4,6 +4,7 @@ using Avalonia.Platform; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Common.Configuration; using Ryujinx.Common.Helper; +using Ryujinx.Graphics.RenderDocApi; using SPB.Graphics; using SPB.Platform; using SPB.Platform.GLX; @@ -30,6 +31,7 @@ namespace Ryujinx.Ava.UI.Renderer protected nint MetalLayer { get; set; } public delegate void UpdateBoundsCallbackDelegate(Rect rect); + private UpdateBoundsCallbackDelegate _updateBoundsCallback; public event EventHandler WindowCreated; @@ -46,6 +48,23 @@ namespace Ryujinx.Ava.UI.Renderer protected virtual void OnWindowDestroyed() { } + public void StartRenderDocCapture() + { + if (!RenderDoc.IsAvailable) return; + + if (RenderDoc.IsFrameCapturing) return; + + try + { + RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); + } catch {} + } + + public bool EndRenderDocCapture() + { + return RenderDoc.IsAvailable && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); + } + protected virtual void OnWindowDestroying() { WindowHandle = nint.Zero; @@ -124,7 +143,9 @@ namespace Ryujinx.Ava.UI.Renderer } else { - X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow; + X11Window = PlatformHelper.CreateOpenGLWindow( + new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, + 100) as GLXWindow; } WindowHandle = X11Window.WindowHandle.RawHandle; @@ -138,7 +159,7 @@ namespace Ryujinx.Ava.UI.Renderer { _className = "NativeWindow-" + Guid.NewGuid(); - _wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam) + _wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam) { switch (msg) { @@ -161,7 +182,8 @@ namespace Ryujinx.Ava.UI.Renderer RegisterClassEx(ref wndClassEx); - WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero); + WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, + control.Handle, nint.Zero, nint.Zero, nint.Zero); SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc); diff --git a/src/Ryujinx/UI/RyujinxApp.axaml.cs b/src/Ryujinx/UI/RyujinxApp.axaml.cs index efe67d6a7..c778f27fb 100644 --- a/src/Ryujinx/UI/RyujinxApp.axaml.cs +++ b/src/Ryujinx/UI/RyujinxApp.axaml.cs @@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.RenderDocApi; using System; using System.Diagnostics; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 651dc901c..23037af2f 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -38,6 +38,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.UI; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; +using Ryujinx.Graphics.RenderDocApi; using Ryujinx.HLE; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; @@ -104,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial Brush ProgressBarForegroundColor { get; set; } [ObservableProperty] public partial Brush ProgressBarBackgroundColor { get; set; } - + #pragma warning disable MVVMTK0042 // Must stay a normal observable field declaration since this is used as an out parameter target [ObservableProperty] private ReadOnlyObservableCollection _appsObservableList; #pragma warning restore MVVMTK0042 @@ -129,8 +130,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial string LastScannedAmiiboId { get; set; } - [ObservableProperty] - public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64; + [ObservableProperty] public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64; [ObservableProperty] public partial bool ShowContent { get; set; } = true; [ObservableProperty] public partial float VolumeBeforeMute { get; set; } @@ -1865,6 +1865,21 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public void ReloadRenderDocApi() + { + RenderDoc.ReloadApi(ignoreAlreadyLoaded: true); + + OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton), nameof(RenderDocIsAvailable)); + + if (RenderDoc.IsAvailable) + RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + + NotificationHelper.ShowInformation( + "RenderDoc API reloaded", + RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available." + ); + } + public void ToggleFullscreen() { if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) @@ -1955,7 +1970,8 @@ namespace Ryujinx.Ava.UI.ViewModels if (ConfigurationState.Instance.Debug.EnableGdbStub) { NotificationHelper.ShowInformation( - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, + ConfigurationState.Instance.Debug.GdbStubPort.Value), LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]); } @@ -1964,10 +1980,12 @@ namespace Ryujinx.Ava.UI.ViewModels var memoryConfigurationLocaleKey = ConfigurationState.Instance.System.DramSize.Value switch { MemoryConfiguration.MemoryConfiguration4GiB or - MemoryConfiguration.MemoryConfiguration4GiBAppletDev or - MemoryConfiguration.MemoryConfiguration4GiBSystemDev => LocaleKeys.SettingsTabSystemDramSize4GiB, + MemoryConfiguration.MemoryConfiguration4GiBAppletDev or + MemoryConfiguration.MemoryConfiguration4GiBSystemDev => + LocaleKeys.SettingsTabSystemDramSize4GiB, MemoryConfiguration.MemoryConfiguration6GiB or - MemoryConfiguration.MemoryConfiguration6GiBAppletDev => LocaleKeys.SettingsTabSystemDramSize6GiB, + MemoryConfiguration.MemoryConfiguration6GiBAppletDev => + LocaleKeys.SettingsTabSystemDramSize6GiB, MemoryConfiguration.MemoryConfiguration8GiB => LocaleKeys.SettingsTabSystemDramSize8GiB, MemoryConfiguration.MemoryConfiguration12GiB => LocaleKeys.SettingsTabSystemDramSize12GiB, _ => LocaleKeys.SettingsTabSystemDramSize4GiB, @@ -1975,9 +1993,9 @@ namespace Ryujinx.Ava.UI.ViewModels NotificationHelper.ShowWarning( LocaleManager.Instance.UpdateAndGetDynamicValue( - LocaleKeys.NotificationLaunchCheckDramSizeTitle, + LocaleKeys.NotificationLaunchCheckDramSizeTitle, LocaleManager.Instance[memoryConfigurationLocaleKey] - ), + ), LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]); } } @@ -2462,6 +2480,41 @@ namespace Ryujinx.Ava.UI.ViewModels png.SaveTo(fileStream); }); + public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable; + public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable; + public static bool RenderDocIsAvailable => RenderDoc.IsAvailable; + + public bool RenderDocIsCapturing + { + get; + set + { + field = value; + OnPropertyChanged(); + OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton)); + } + } + + public static RelayCommand StartRenderDocCapture { get; } = + Commands.CreateConditional(_ => RenderDoc.IsAvailable, + viewModel => + { + if (!RenderDoc.IsFrameCapturing) + viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture(); + + viewModel.RenderDocIsCapturing = true; + }); + + public static RelayCommand EndRenderDocCapture { get; } = + Commands.CreateConditional(_ => RenderDoc.IsAvailable, + viewModel => + { + if (RenderDoc.IsFrameCapturing) + viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture(); + + viewModel.RenderDocIsCapturing = false; + }); + #endregion } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 47f79725c..5b567fa46 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -8,6 +8,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common" + xmlns:renderDocApi="clr-namespace:Ryujinx.Graphics.RenderDocApi;assembly=Ryujinx.Graphics.RenderDocApi" x:DataType="viewModels:MainWindowViewModel" x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> @@ -200,6 +201,21 @@ Header="{ext:Locale GameListContextMenuManageCheat}" Icon="{ext:Icon fa-solid fa-code}" IsEnabled="{Binding IsGameRunning}" /> + + + diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml index 684b39ef3..ef47bf4dc 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -41,6 +41,7 @@ +