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}" />
+
+
+