From 938831a901faaef6d732c6da60815f7fd3239956 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 17:28:58 -0600 Subject: [PATCH 01/30] RenderDoc integration (start/stop capture menu bar items) --- Ryujinx.sln | 14 + src/Ryujinx.Graphics.RenderDocApi/Capture.cs | 12 + .../CaptureOption.cs | 22 ++ .../InputButton.cs | 83 +++++ .../OverlayBits.cs | 19 + src/Ryujinx.Graphics.RenderDocApi/README.md | 5 + .../RenderDoc.cs | 335 ++++++++++++++++++ .../RenderDocApi.cs | 51 +++ .../RenderDocApiVersionAttribute.cs | 16 + .../RenderDocVersion.cs | 19 + .../Ryujinx.Graphics.RenderDocApi.csproj | 15 + src/Ryujinx.Graphics.Vulkan/Helpers.cs | 32 ++ src/Ryujinx/Program.cs | 1 + src/Ryujinx/Ryujinx.csproj | 3 +- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 28 +- src/Ryujinx/UI/RyujinxApp.axaml.cs | 1 + .../UI/ViewModels/MainWindowViewModel.cs | 71 +++- .../UI/Views/Main/MainMenuBarView.axaml | 16 + src/Ryujinx/UI/Windows/MainWindow.axaml | 1 + 19 files changed, 730 insertions(+), 14 deletions(-) create mode 100644 src/Ryujinx.Graphics.RenderDocApi/Capture.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/InputButton.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/README.md create mode 100644 src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs create mode 100644 src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj create mode 100644 src/Ryujinx.Graphics.Vulkan/Helpers.cs 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 @@ + From 0365e1414c4bab2932780c160a9a27ec293ac3c5 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 18:59:09 -0600 Subject: [PATCH 02/30] Apply XMLdocs to Ryujinx.Graphics.RenderDocApi Most of these are a copy and paste from the official RenderDoc API documentation, as it is just a binding for that after all. Changes have been made to reference the bindings instead of their C++ counterparts where necessary and to make it read more C#-like than C++. --- .../Helpers.cs | 2 +- .../RenderDoc.cs | 203 +++++++++++++++++- 2 files changed, 193 insertions(+), 12 deletions(-) rename src/{Ryujinx.Graphics.Vulkan => Ryujinx.Graphics.RenderDocApi}/Helpers.cs (97%) diff --git a/src/Ryujinx.Graphics.Vulkan/Helpers.cs b/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs similarity index 97% rename from src/Ryujinx.Graphics.Vulkan/Helpers.cs rename to src/Ryujinx.Graphics.RenderDocApi/Helpers.cs index d29ac3440..2c4e04c74 100644 --- a/src/Ryujinx.Graphics.Vulkan/Helpers.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs @@ -1,7 +1,7 @@ using Silk.NET.Vulkan; using System.Runtime.CompilerServices; -namespace Ryujinx.Graphics.Vulkan +namespace Ryujinx.Graphics.RenderDocApi { public static class Helpers { diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 8a7d2ec21..e5c1b5ca5 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -44,6 +44,9 @@ namespace Ryujinx.Graphics.RenderDocApi } } + /// + /// The current mask which determines what sections of the overlay render on each window. + /// [RenderDocApiVersion(1, 0)] public static OverlayBits OverlayBits { @@ -54,6 +57,14 @@ namespace Ryujinx.Graphics.RenderDocApi } } + /// + /// The template for new captures.
+ /// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named. + /// If the path template is 'my_captures/example', then captures saved will be e.g. + /// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.
+ /// Relative paths will be saved relative to the process’s current working directory.
+ ///
+ /// The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename. [RenderDocApiVersion(1, 0)] public static string CaptureFilePathTemplate { @@ -71,42 +82,104 @@ namespace Ryujinx.Graphics.RenderDocApi } } - [RenderDocApiVersion(1, 0)] public static int NumCaptures => Api->GetNumCaptures(); + /// + /// The amount of frame captures that have been made. + /// + [RenderDocApiVersion(1, 0)] + public static int CaptureCount => Api->GetNumCaptures(); - [RenderDocApiVersion(1, 0)] public static bool IsTargetControlConnected => Api->IsTargetControlConnected() != 0; + /// + /// Checks if the RenderDoc UI is currently connected to this process. + /// + [RenderDocApiVersion(1, 0)] + public static bool IsTargetControlConnected => Api->IsTargetControlConnected() != 0; - [RenderDocApiVersion(1, 0)] public static bool IsFrameCapturing => Api->IsFrameCapturing() != 0; + /// + /// Checks if the current frame is capturing. + /// + [RenderDocApiVersion(1, 0)] + public static bool IsFrameCapturing => Api->IsFrameCapturing() != 0; + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the unsigned integer value to set for the option. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
[RenderDocApiVersion(1, 0)] public static bool SetCaptureOption(CaptureOption option, int integer) { return Api->SetCaptureOptionU32(option, integer) != 0; } + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the floating point value to set for the option. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
[RenderDocApiVersion(1, 0)] public static bool SetCaptureOption(CaptureOption option, float single) { return Api->SetCaptureOptionF32(option, single) != 0; } + /// + /// Gets the current value of one of the different options in , writing it to an out parameter. + /// + /// specifies which capture option should be retrieved. + /// the value of the capture option, if the option is a valid enum. Otherwise, . [RenderDocApiVersion(1, 0)] public static void GetCaptureOption(CaptureOption option, out int integer) { integer = Api->GetCaptureOptionU32(option); } + /// + /// Gets the current value of one of the different options in , writing it to an out parameter. + /// + /// specifies which capture option should be retrieved. + /// the value of the capture option, if the option is a valid enum. Otherwise, -. [RenderDocApiVersion(1, 0)] public static void GetCaptureOption(CaptureOption option, out float single) { single = Api->GetCaptureOptionF32(option); } + /// + /// Gets the current value of one of the different options in . + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, if the option is a valid enum. + /// Otherwise, returns + /// [RenderDocApiVersion(1, 0)] public static int GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); + /// + /// Gets the current value of one of the different options in . + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, if the option is a valid enum. + /// Otherwise, returns - + /// [RenderDocApiVersion(1, 0)] public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option); + /// + /// Changes the key bindings in-application for changing the focussed window. + /// + /// lists the keys to bind. [RenderDocApiVersion(1, 0)] public static void SetFocusToggleKeys(ReadOnlySpan buttons) { @@ -116,6 +189,10 @@ namespace Ryujinx.Graphics.RenderDocApi } } + /// + /// Changes the key bindings in-application for triggering a capture on the current window. + /// + /// lists the keys to bind. [RenderDocApiVersion(1, 0)] public static void SetCaptureKeys(ReadOnlySpan buttons) { @@ -125,24 +202,43 @@ namespace Ryujinx.Graphics.RenderDocApi } } + /// + /// Attempts to remove RenderDoc and its hooks from the target process.
+ /// It must be called as early as possible in the process, and will have undefined results + /// if any graphics API functions have been called. + ///
[RenderDocApiVersion(1, 0)] public static void RemoveHooks() { Api->RemoveHooks(); } + /// + /// Remove RenderDoc’s crash handler from the target process.
+ /// If you have your own crash handler that you want to handle any exceptions, + /// RenderDoc’s handler could interfere; so it can be disabled. + ///
[RenderDocApiVersion(1, 0)] public static void UnloadCrashHandler() { Api->UnloadCrashHandler(); } + /// + /// Trigger a capture as if the user had pressed one of the capture hotkeys.
+ /// The capture will be taken from the next frame presented to whichever window is considered current. + ///
[RenderDocApiVersion(1, 0)] public static void TriggerCapture() { Api->TriggerCapture(); } + /// + /// Gets the details of a particular frame capture, as specified by an index from 0 to - 1. + /// + /// specifies which capture to return the details of. Must be less than the value returned by . + /// A struct representing a RenderDoc Capture. [RenderDocApiVersion(1, 0)] public static Capture? GetCapture(int index) { @@ -162,8 +258,14 @@ namespace Ryujinx.Graphics.RenderDocApi return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime); } + /// + /// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it. + /// + /// if the UI should immediately connect to the application. + /// string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified. + /// true if the UI was successfully launched; false otherwise. [RenderDocApiVersion(1, 0)] - public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine) + public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null) { if (commandLine == null) { @@ -176,24 +278,48 @@ namespace Ryujinx.Graphics.RenderDocApi } } + /// + /// Explicitly sets which window is considered active.
+ /// The active window is the one that will be captured when the keybind to trigger a capture is pressed. + ///
+ /// a handle to the API ‘device’ object that will be set active. Must be valid. + /// a handle to the platform window handle that will be set active. Must be valid. [RenderDocApiVersion(1, 0)] public static void SetActiveWindow(nint hDevice, nint hWindow) { Api->SetActiveWindow((void*)hDevice, (void*)hWindow); } + /// + /// Immediately begin a capture for the specified device/window combination. + /// + /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. [RenderDocApiVersion(1, 0)] public static void StartFrameCapture(nint hDevice, nint hWindow) { Api->StartFrameCapture((void*)hDevice, (void*)hWindow); } + /// + /// Immediately end an active capture for the specified device/window combination. + /// + /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. + /// true if the capture succeeded; false otherwise. [RenderDocApiVersion(1, 0)] public static bool EndFrameCapture(nint hDevice, nint hWindow) { return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0; } + /// + /// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.
+ /// The captures will be taken from the next frames presented to whichever window is considered current.
+ /// Each capture will be taken independently and saved to a separate file, with no reference to the other frames. + ///
+ /// the number of frames to capture. + /// Requires RenderDoc API version 1.1 [RenderDocApiVersion(1, 1)] public static void TriggerMultiFrameCapture(int numFrames) { @@ -201,28 +327,62 @@ namespace Ryujinx.Graphics.RenderDocApi Api->TriggerMultiFrameCapture(numFrames); } + /// + /// Adds an arbitrary comments field to an existing capture on disk, + /// which will then be displayed in the UI to anyone opening the capture. + /// + /// the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used. + /// the comments to set in the capture file. + /// Requires RenderDoc API version 1.2 [RenderDocApiVersion(1, 2)] - public static void SetCaptureFileComments(string fileName, string comments) + 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); + if (fileName is null) + { + Api->SetCaptureFileComments((byte*)nint.Zero, pcomment); + } + else + { + byte[] fileBytes = Encoding.UTF8.GetBytes(fileName + '\0'); + + fixed (byte* pfile = fileBytes) + { + Api->SetCaptureFileComments(pfile, pcomment); + } + } } } + /// + /// Similar to , but the capture contents will be discarded immediately, and not processed and written to disk.
+ /// This will be more efficient than if the frame capture is not needed. + ///
+ /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. + /// Requires RenderDoc API version 1.3 [RenderDocApiVersion(1, 3)] - public static void DiscardFrameCapture(nint hdevice, nint hWindow) + public static void DiscardFrameCapture(nint hDevice, nint hWindow) { AssertAtLeast(1, 3); - Api->DiscardFrameCapture((void*)hdevice, (void*)hWindow); + Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow); } + + /// + /// Requests that the currently connected replay UI raise its window to the top.
+ /// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.
+ /// This can be used in conjunction with and ,
to intelligently handle showing the UI after making a capture.

+ /// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.
+ /// On some systems it may only be highlighted or otherwise indicated to the user. + ///
+ /// true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred. + /// Requires RenderDoc API version 1.5 [RenderDocApiVersion(1, 5)] public static bool ShowReplayUI() { @@ -230,6 +390,19 @@ namespace Ryujinx.Graphics.RenderDocApi return Api->ShowReplayUI() != 0; } + /// + /// Sets a given title for the currently in-progress capture, which will be displayed in the UI.
+ /// This can be used either with a user-defined capture using a manual start and end, + /// or an automatic capture triggered by or a keypress.
+ /// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.
+ /// Any subsequent captures will not get any title unless the function is called again. + /// This function can only be called while a capture is in-progress, + /// after and before .
+ /// If it is called elsewhere it will have no effect. + /// If it is called multiple times within a capture, only the last title will have any effect. + ///
+ /// The title to set for the in-progress capture. + /// Requires RenderDoc API version 1.6 [RenderDocApiVersion(1, 6)] public static void SetCaptureTitle(string title) { @@ -238,6 +411,12 @@ namespace Ryujinx.Graphics.RenderDocApi Api->SetCaptureTitle(ptr); } + #region Dynamic Library loading + + /// + /// Reload the internal RenderDoc API structure. Useful for manually refreshing while using process injection. + /// + /// Ignores the existing API function structure and overwrites it with a re-request. public static void ReloadApi(bool ignoreAlreadyLoaded = false) { if (_loaded && !ignoreAlreadyLoaded) @@ -303,7 +482,7 @@ namespace Ryujinx.Graphics.RenderDocApi if (Version!.Major < major) goto fail; - + if (Version.Major > major) goto success; if (Version.Minor < minor) @@ -331,5 +510,7 @@ namespace Ryujinx.Graphics.RenderDocApi private static RenderDocVersion SystemVersionToRenderdocVersion(Version version) => (RenderDocVersion)(version.Major * 10000 + version.Minor * 100 + version.Build); + + #endregion } } From d17025a4ab8e62e2b4d1d814370babdf367d2958 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 19:29:42 -0600 Subject: [PATCH 03/30] DiscardFrameCapture was actually added in 1.4, according to RenderDoc API docs --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index e5c1b5ca5..600e43f43 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -365,11 +365,11 @@ namespace Ryujinx.Graphics.RenderDocApi /// /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. /// a handle to the platform window handle that will be set active. May be to wildcard match. - /// Requires RenderDoc API version 1.3 - [RenderDocApiVersion(1, 3)] + /// Requires RenderDoc API version 1.4 + [RenderDocApiVersion(1, 4)] public static void DiscardFrameCapture(nint hDevice, nint hWindow) { - AssertAtLeast(1, 3); + AssertAtLeast(1, 4); Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow); } From 3b82cc5ccdb6b8bb4096b0582e1a0f1c3ab6599a Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 19:36:57 -0600 Subject: [PATCH 04/30] use a helper for creating null-terminated byte arrays out of System.String instead of duplicating the logic --- .../RenderDoc.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 600e43f43..0782b4a90 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Graphics.RenderDocApi } set { - fixed (byte* ptr = Encoding.UTF8.GetBytes(value + '\0')) + fixed (byte* ptr = value.ToNullTerminatedByteArray()) { Api->SetCaptureFilePathTemplate(ptr); } @@ -254,7 +254,7 @@ namespace Ryujinx.Graphics.RenderDocApi fixed (byte* ptr = bytes) Api->GetCapture(index, ptr, &length, ×tamp); - string fileName = Encoding.UTF8.GetString(bytes.Slice(length)); + string fileName = Encoding.UTF8.GetString(bytes[length..]); return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime); } @@ -272,7 +272,7 @@ namespace Ryujinx.Graphics.RenderDocApi return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, null) != 0; } - fixed (byte* ptr = Encoding.UTF8.GetBytes(commandLine + '\0')) + fixed (byte* ptr = commandLine.ToNullTerminatedByteArray()) { return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, ptr) != 0; } @@ -339,7 +339,7 @@ namespace Ryujinx.Graphics.RenderDocApi { AssertAtLeast(1, 2); - byte[] commentBytes = Encoding.UTF8.GetBytes(comments + '\0'); + byte[] commentBytes = comments.ToNullTerminatedByteArray(); fixed (byte* pcomment = commentBytes) { @@ -349,7 +349,7 @@ namespace Ryujinx.Graphics.RenderDocApi } else { - byte[] fileBytes = Encoding.UTF8.GetBytes(fileName + '\0'); + byte[] fileBytes = fileName.ToNullTerminatedByteArray(); fixed (byte* pfile = fileBytes) { @@ -407,7 +407,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void SetCaptureTitle(string title) { AssertAtLeast(1, 6); - fixed (byte* ptr = Encoding.UTF8.GetBytes(title + '\0')) + fixed (byte* ptr = title.ToNullTerminatedByteArray()) Api->SetCaptureTitle(ptr); } @@ -511,6 +511,13 @@ namespace Ryujinx.Graphics.RenderDocApi private static RenderDocVersion SystemVersionToRenderdocVersion(Version version) => (RenderDocVersion)(version.Major * 10000 + version.Minor * 100 + version.Build); + private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + + return encoding.GetBytes(str + '\0'); + } + #endregion } } From 0c1338b7d30bcedabb82b742ce61b9dbbab0d022 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 22:21:39 -0600 Subject: [PATCH 05/30] use nint --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 0782b4a90..36f4f0f8f 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -71,7 +71,7 @@ namespace Ryujinx.Graphics.RenderDocApi get { byte* ptr = Api->GetCaptureFilePathTemplate(); - return Marshal.PtrToStringUTF8((IntPtr)ptr)!; + return Marshal.PtrToStringUTF8((nint)ptr)!; } set { @@ -460,10 +460,10 @@ namespace Ryujinx.Graphics.RenderDocApi if (!re.IsMatch(moduleName)) continue; - if (!NativeLibrary.TryLoad(moduleName, out IntPtr moduleHandle)) + if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle)) return null; - if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out IntPtr procAddress)) + if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress)) return null; var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl])procAddress; From 82ab8132f60d01a7d1ee3751fc5d0948a1b90023 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 23:43:55 -0600 Subject: [PATCH 06/30] use RenderDocVersion enum for MinimumRequired instead of System.Version, additionally added .NET 10 extension properties instead of helper methods in RenderDoc --- src/Ryujinx.Graphics.RenderDocApi/Helpers.cs | 2 +- .../RenderDoc.cs | 27 +++++++++-------- .../RenderDocVersion.cs | 30 ++++++++++++++++++- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs b/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs index 2c4e04c74..ebd30d113 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.RenderDocApi { - public static class Helpers + public static partial class Helpers { extension(Vk api) { diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 36f4f0f8f..6574dbb69 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -20,7 +20,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// 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); + public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0; /// /// Set to true to assert versions. @@ -417,7 +417,8 @@ namespace Ryujinx.Graphics.RenderDocApi /// Reload the internal RenderDoc API structure. Useful for manually refreshing while using process injection. /// /// Ignores the existing API function structure and overwrites it with a re-request. - public static void ReloadApi(bool ignoreAlreadyLoaded = false) + /// The version of the RenderDoc API required by your application. + public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null) { if (_loaded && !ignoreAlreadyLoaded) return; @@ -428,11 +429,14 @@ namespace Ryujinx.Graphics.RenderDocApi if (_loaded && !ignoreAlreadyLoaded) return; + if (requiredVersion.HasValue) + MinimumRequired = requiredVersion.Value; + _loaded = true; - _api = GetApi(SystemVersionToRenderdocVersion(MinimumRequired)); + _api = GetApi(MinimumRequired); if (_api != null) - AssertAtLeast(MinimumRequired.Major, MinimumRequired.Minor, MinimumRequired.Build); + AssertAtLeast(MinimumRequired); } } @@ -475,6 +479,12 @@ namespace Ryujinx.Graphics.RenderDocApi return null; } + private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "") + { + Version ver = rdv.SystemVersion; + AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee); + } + private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "") { if (!AssertVersionEnabled) @@ -502,15 +512,6 @@ namespace Ryujinx.Graphics.RenderDocApi $"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); - private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null) { encoding ??= Encoding.UTF8; diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs index f5aa8fed7..1b386c435 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs @@ -1,6 +1,8 @@ +using System; + namespace Ryujinx.Graphics.RenderDocApi { - internal enum RenderDocVersion : int + public enum RenderDocVersion { Version_1_0_0 = 10000, Version_1_0_1 = 10001, @@ -16,4 +18,30 @@ namespace Ryujinx.Graphics.RenderDocApi Version_1_5_0 = 10500, Version_1_6_0 = 10600, } + + public static partial class Helpers + { + extension(RenderDocVersion rdv) + { + public Version SystemVersion + { + get + { + int i = (int)rdv; + return new (i / 10000, (i % 10000) / 100, i % 100); + } + } + } + + extension(Version sv) + { + public RenderDocVersion RenderDocVersion + { + get + { + return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build); + } + } + } + } } From 7a1c7b714e1a07465cfe467567e88ee96256bb30 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sat, 27 Dec 2025 23:48:30 -0600 Subject: [PATCH 07/30] use GeneratedRegexAttribute --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 6574dbb69..594b1e9be 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -9,7 +9,7 @@ using System.Text.RegularExpressions; namespace Ryujinx.Graphics.RenderDocApi { - public static unsafe class RenderDoc + public static unsafe partial class RenderDoc { /// /// True if the API is available. @@ -452,16 +452,15 @@ namespace Ryujinx.Graphics.RenderDocApi } } + private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex(); + 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)) + if (!_dynamicLibraryPattern.IsMatch(moduleName)) continue; if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle)) @@ -509,7 +508,7 @@ namespace Ryujinx.Graphics.RenderDocApi Version minVersion = typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute()!.MinVersion; throw new NotSupportedException( - $"This API was introduced in RenderdocAPI {minVersion}. Current API version is {Version}."); + $"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}."); } private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null) @@ -519,6 +518,9 @@ namespace Ryujinx.Graphics.RenderDocApi return encoding.GetBytes(str + '\0'); } + [GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex RenderDocApiDynamicLibraryRegex(); + #endregion } } From 801dcd5237f0de69d1afdede9dd13b3d60bae19a Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 00:38:55 -0600 Subject: [PATCH 08/30] Log start/stop capture, give captures a title and make the RenderDoc API more resistant to being called without an underlying Api struct --- .../RenderDoc.cs | 41 ++++++++++++++++--- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 15 ++++--- .../UI/ViewModels/MainWindowViewModel.cs | 9 +++- src/Ryujinx/Utilities/TitleHelper.cs | 13 ++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 594b1e9be..f49b1b737 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -92,13 +92,13 @@ namespace Ryujinx.Graphics.RenderDocApi /// Checks if the RenderDoc UI is currently connected to this process. /// [RenderDocApiVersion(1, 0)] - public static bool IsTargetControlConnected => Api->IsTargetControlConnected() != 0; + public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0; /// /// Checks if the current frame is capturing. /// [RenderDocApiVersion(1, 0)] - public static bool IsFrameCapturing => Api->IsFrameCapturing() != 0; + public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0; /// /// Set one of the options for tweaking some behaviors of capturing. @@ -113,7 +113,7 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static bool SetCaptureOption(CaptureOption option, int integer) { - return Api->SetCaptureOptionU32(option, integer) != 0; + return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0; } /// @@ -129,7 +129,7 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static bool SetCaptureOption(CaptureOption option, float single) { - return Api->SetCaptureOptionF32(option, single) != 0; + return Api is not null && Api->SetCaptureOptionF32(option, single) != 0; } /// @@ -183,6 +183,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void SetFocusToggleKeys(ReadOnlySpan buttons) { + if (Api is null) return; + fixed (InputButton* ptr = buttons) { Api->SetFocusToggleKeys(ptr, buttons.Length); @@ -196,6 +198,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void SetCaptureKeys(ReadOnlySpan buttons) { + if (Api is null) return; + fixed (InputButton* ptr = buttons) { Api->SetCaptureKeys(ptr, buttons.Length); @@ -210,6 +214,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void RemoveHooks() { + if (Api is null) return; + Api->RemoveHooks(); } @@ -221,6 +227,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void UnloadCrashHandler() { + if (Api is null) return; + Api->UnloadCrashHandler(); } @@ -231,6 +239,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void TriggerCapture() { + if (Api is null) return; + Api->TriggerCapture(); } @@ -242,6 +252,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static Capture? GetCapture(int index) { + if (Api is null) return null; + int length = 0; if (Api->GetCapture(index, null, &length, null) == 0) { @@ -267,6 +279,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null) { + if (Api is null) return false; + if (commandLine == null) { return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, null) != 0; @@ -287,6 +301,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void SetActiveWindow(nint hDevice, nint hWindow) { + if (Api is null) return; + Api->SetActiveWindow((void*)hDevice, (void*)hWindow); } @@ -298,6 +314,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static void StartFrameCapture(nint hDevice, nint hWindow) { + if (Api is null) return; + Api->StartFrameCapture((void*)hDevice, (void*)hWindow); } @@ -310,6 +328,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 0)] public static bool EndFrameCapture(nint hDevice, nint hWindow) { + if (Api is null) return false; + return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0; } @@ -323,6 +343,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 1)] public static void TriggerMultiFrameCapture(int numFrames) { + if (Api is null) return; + AssertAtLeast(1, 1); Api->TriggerMultiFrameCapture(numFrames); } @@ -337,6 +359,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 2)] public static void SetCaptureFileComments(string? fileName, string comments) { + if (Api is null) return; + AssertAtLeast(1, 2); byte[] commentBytes = comments.ToNullTerminatedByteArray(); @@ -369,6 +393,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 4)] public static void DiscardFrameCapture(nint hDevice, nint hWindow) { + if (Api is null) return; + AssertAtLeast(1, 4); Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow); } @@ -386,6 +412,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 5)] public static bool ShowReplayUI() { + if (Api is null) return false; + AssertAtLeast(1, 5); return Api->ShowReplayUI() != 0; } @@ -406,6 +434,8 @@ namespace Ryujinx.Graphics.RenderDocApi [RenderDocApiVersion(1, 6)] public static void SetCaptureTitle(string title) { + if (Api is null) return; + AssertAtLeast(1, 6); fixed (byte* ptr = title.ToNullTerminatedByteArray()) Api->SetCaptureTitle(ptr); @@ -518,7 +548,8 @@ namespace Ryujinx.Graphics.RenderDocApi return encoding.GetBytes(str + '\0'); } - [GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + [GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex RenderDocApiDynamicLibraryRegex(); #endregion diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index bd9cfae51..a199b8d03 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -2,9 +2,12 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Platform; using Ryujinx.Ava.Systems.Configuration; +using Ryujinx.Ava.Utilities; using Ryujinx.Common.Configuration; using Ryujinx.Common.Helper; +using Ryujinx.Common.Logging; using Ryujinx.Graphics.RenderDocApi; +using Ryujinx.HLE; using SPB.Graphics; using SPB.Platform; using SPB.Platform.GLX; @@ -48,21 +51,21 @@ namespace Ryujinx.Ava.UI.Renderer protected virtual void OnWindowDestroyed() { } - public void StartRenderDocCapture() + public void StartRenderDocCapture(Switch device) { if (!RenderDoc.IsAvailable) return; if (RenderDoc.IsFrameCapturing) return; - try - { - RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); - } catch {} + RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); + RenderDoc.SetCaptureTitle(TitleHelper.TruncatedApplicationTitle(device.Processes.ActiveApplication, Program.Version)); + + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); } public bool EndRenderDocCapture() { - return RenderDoc.IsAvailable && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); + return RenderDoc.IsAvailable && RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); } protected virtual void OnWindowDestroying() diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 23037af2f..c0678e6cf 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -2500,7 +2500,7 @@ namespace Ryujinx.Ava.UI.ViewModels viewModel => { if (!RenderDoc.IsFrameCapturing) - viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture(); + viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device); viewModel.RenderDocIsCapturing = true; }); @@ -2510,7 +2510,12 @@ namespace Ryujinx.Ava.UI.ViewModels viewModel => { if (RenderDoc.IsFrameCapturing) - viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture(); + { + if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture."); + } + } viewModel.RenderDocIsCapturing = false; }); diff --git a/src/Ryujinx/Utilities/TitleHelper.cs b/src/Ryujinx/Utilities/TitleHelper.cs index 5e0916c27..b03ce33f4 100644 --- a/src/Ryujinx/Utilities/TitleHelper.cs +++ b/src/Ryujinx/Utilities/TitleHelper.cs @@ -22,5 +22,18 @@ namespace Ryujinx.Ava.Utilities ? appTitle + $" ({pauseString})" : appTitle; } + + public static string TruncatedApplicationTitle(ProcessResult activeProcess, string applicationVersion) + { + if (activeProcess == null) + return string.Empty; + + string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}"; + string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}"; + string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})"; + string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)"; + + return $"{applicationVersion}\n{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; + } } } From 854df7c347ba635e8e9e64c5acc22e517a87a635 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 00:51:05 -0600 Subject: [PATCH 09/30] consistency --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index f49b1b737..4f337528d 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -160,7 +160,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// specifies which capture option should be retrieved. /// /// the value of the capture option, if the option is a valid enum. - /// Otherwise, returns + /// Otherwise, returns . /// [RenderDocApiVersion(1, 0)] public static int GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); @@ -171,7 +171,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// specifies which capture option should be retrieved. /// /// the value of the capture option, if the option is a valid enum. - /// Otherwise, returns - + /// Otherwise, returns -. /// [RenderDocApiVersion(1, 0)] public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option); @@ -184,7 +184,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void SetFocusToggleKeys(ReadOnlySpan buttons) { if (Api is null) return; - + fixed (InputButton* ptr = buttons) { Api->SetFocusToggleKeys(ptr, buttons.Length); @@ -199,7 +199,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void SetCaptureKeys(ReadOnlySpan buttons) { if (Api is null) return; - + fixed (InputButton* ptr = buttons) { Api->SetCaptureKeys(ptr, buttons.Length); @@ -215,7 +215,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void RemoveHooks() { if (Api is null) return; - + Api->RemoveHooks(); } @@ -228,7 +228,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void UnloadCrashHandler() { if (Api is null) return; - + Api->UnloadCrashHandler(); } @@ -240,7 +240,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static void TriggerCapture() { if (Api is null) return; - + Api->TriggerCapture(); } From f234825588361d058313d9bc4c07acacb03d7bdd Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 02:24:13 -0600 Subject: [PATCH 10/30] give enums XMLdocs as per the api header --- .../CaptureOption.cs | 78 +++++++++++++++++++ .../OverlayBits.cs | 22 +++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs index dbb8494a8..cd3f860d2 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs @@ -4,19 +4,97 @@ namespace Ryujinx.Graphics.RenderDocApi { public enum CaptureOption { + /// + /// specifies whether the application is allowed to enable vsync. Default is on. + /// AllowVsync = 0, + /// + /// specifies whether the application is allowed to enter exclusive fullscreen. Default is on. + /// AllowFullscreen = 1, + /// + /// specifies whether (where possible) API-specific debugging is enabled. Default is off. + /// ApiValidation = 2, + /// + /// specifies whether each API call should save a callstack. Default is off. + /// CaptureCallstacks = 3, + /// + /// specifies whether, if is enabled, callstacks are only saved on actions. Default is off. + /// CaptureCallstacksOnlyDraws = 4, + /// + /// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach.
+ /// This will only apply to child processes since the delay happens at process startup. Default is 0. + ///
DelayForDebugger = 5, + /// + /// specifies whether any mapped memory updates should be bounds-checked for overruns, + /// and uninitialised buffers are initialized to 0xDDDDDDDD to catch use of uninitialised data. + /// Only supported on D3D11 and OpenGL. Default is off. + /// + /// + /// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do + /// not do the same kind of interception & checking, and undefined contents are really undefined. + /// VerifyBufferAccess = 6, + /// + /// Hooks any system API calls that create child processes, and injects + /// RenderDoc into them recursively with the same options. + /// HookIntoChildren = 7, + /// + /// specifies whether all live resources at the time of capture should be included in the capture, + /// even if they are not referenced by the frame. Default is off. + /// RefAllSources = 8, + /// + /// By default, RenderDoc skips saving initial states for resources where the + /// previous contents don't appear to be used, assuming that writes before + /// reads indicate previous contents aren't used. + /// + /// + /// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or + /// getting it will be ignored, to allow compatibility with older versions. + /// In v1.1 the option acts as if it's always enabled. + /// SaveAllInitials = 9, + /// + /// In APIs that allow for the recording of command lists to be replayed later, + /// RenderDoc may choose to not capture command lists before a frame capture is + /// triggered, to reduce overheads. This means any command lists recorded once + /// and replayed many times will not be available and may cause a failure to + /// capture. + /// + /// + /// NOTE: This is only true for APIs where multithreading is difficult or + /// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + /// and always capture all command lists since the API is heavily oriented + /// around it and the overheads have been reduced by API design. + /// CaptureAllCmdLists = 10, + /// + /// Mute API debugging output when the option is enabled. + /// DebugOutputMute = 11, + /// + /// Allow vendor extensions to be used even when they may be + /// incompatible with RenderDoc and cause corrupted replays or crashes. + /// AllowUnsupportedVendorExtensions = 12, + /// + /// Define a soft memory limit which some APIs may aim to keep overhead under where + /// possible. Anything above this limit will where possible be saved directly to disk during + /// capture.
+ /// This will cause increased disk space use (which may cause a capture to fail if disk space is + /// exhausted) as well as slower capture times. + ///

+ /// Not all memory allocations may be deferred like this so it is not a guarantee of a memory + /// limit. + ///

+ /// Units are in MBs, suggested values would range from 200MB to 1000MB. + ///
SoftMemoryLimit = 13, } } diff --git a/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs index 1b5e41829..e973e403c 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs @@ -7,13 +7,33 @@ namespace Ryujinx.Graphics.RenderDocApi [Flags] public enum OverlayBits { + /// + /// This single bit controls whether the overlay is enabled or disabled globally + /// Enabled = 1 << 0, + /// + /// Show the average framerate over several seconds as well as min/max + /// FrameRate = 1 << 1, + /// + /// Show the current frame number + /// FrameNumber = 1 << 2, + /// + /// Show a list of recent captures, and how many captures have been made + /// CaptureList = 1 << 3, - + /// + /// Default values for the overlay mask + /// Default = Enabled | FrameRate | FrameNumber | CaptureList, + /// + /// Enable all bits + /// All = ~0, + /// + /// Disable all bits + /// None = 0 } } From b63a4edb3f35414e34c52dfb76e79f380f357b3d Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 02:56:40 -0600 Subject: [PATCH 11/30] make unsigned integers actually unsigned --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 4f337528d..061d81ccb 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -111,7 +111,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// false, if the option is not a , or the value is not valid for the option. /// [RenderDocApiVersion(1, 0)] - public static bool SetCaptureOption(CaptureOption option, int integer) + public static bool SetCaptureOption(CaptureOption option, uint integer) { return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0; } @@ -138,7 +138,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// specifies which capture option should be retrieved. /// the value of the capture option, if the option is a valid enum. Otherwise, . [RenderDocApiVersion(1, 0)] - public static void GetCaptureOption(CaptureOption option, out int integer) + public static void GetCaptureOption(CaptureOption option, out uint integer) { integer = Api->GetCaptureOptionU32(option); } @@ -163,7 +163,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// Otherwise, returns . /// [RenderDocApiVersion(1, 0)] - public static int GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); + public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); /// /// Gets the current value of one of the different options in . From cab7cf784eb9d4b9879be3cf52ebf92d0c484afa Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 02:56:54 -0600 Subject: [PATCH 12/30] bool CaptureOption helpers --- .../RenderDoc.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 061d81ccb..ba4bc8f65 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -116,6 +116,20 @@ namespace Ryujinx.Graphics.RenderDocApi return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0; } + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the value to set for the option, converted to a 0 or 1 before setting. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
+ [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, bool boolean) + => SetCaptureOption(option, boolean ? 1 : 0); + /// /// Set one of the options for tweaking some behaviors of capturing. /// @@ -154,6 +168,27 @@ namespace Ryujinx.Graphics.RenderDocApi single = Api->GetCaptureOptionF32(option); } + /// + /// Gets the current value of one of the different options in , + /// converted to a boolean. + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, converted to bool, if the option is a valid enum. + /// Otherwise, returns null. + /// + [RenderDocApiVersion(1, 0)] + public static bool? GetCaptureOptionBool(CaptureOption option) + { + if (Api is null) return false; + + uint returnVal = GetCaptureOptionU32(option); + if (returnVal == uint.MaxValue) + return null; + + return returnVal is not 0; + } + /// /// Gets the current value of one of the different options in . /// From cca4da9927266e68264dc8512205c1ff6fdce1ad Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 02:57:26 -0600 Subject: [PATCH 13/30] oops, this was meant to be in the commit before last --- src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs index 539679932..598bd58e2 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs @@ -5,9 +5,9 @@ namespace Ryujinx.Graphics.RenderDocApi { public delegate* unmanaged[Cdecl] GetApiVersion; - public delegate* unmanaged[Cdecl] SetCaptureOptionU32; + public delegate* unmanaged[Cdecl] SetCaptureOptionU32; public delegate* unmanaged[Cdecl] SetCaptureOptionF32; - public delegate* unmanaged[Cdecl] GetCaptureOptionU32; + public delegate* unmanaged[Cdecl] GetCaptureOptionU32; public delegate* unmanaged[Cdecl] GetCaptureOptionF32; public delegate* unmanaged[Cdecl] SetFocusToggleKeys; From ac02cdb1d3a8b5d02348bd5918ba2c2e524397da Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 02:57:34 -0600 Subject: [PATCH 14/30] cleanup --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 8 ++++---- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index ba4bc8f65..6115cada7 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -131,7 +131,7 @@ namespace Ryujinx.Graphics.RenderDocApi => SetCaptureOption(option, boolean ? 1 : 0); /// - /// Set one of the options for tweaking some behaviors of capturing. + /// Set one of the options for tweaking some behaviors of capturing. /// /// specifies which capture option should be set. /// the floating point value to set for the option. @@ -147,7 +147,7 @@ namespace Ryujinx.Graphics.RenderDocApi } /// - /// Gets the current value of one of the different options in , writing it to an out parameter. + /// Gets the current value of one of the different options in , writing it to an out parameter. /// /// specifies which capture option should be retrieved. /// the value of the capture option, if the option is a valid enum. Otherwise, . @@ -190,7 +190,7 @@ namespace Ryujinx.Graphics.RenderDocApi } /// - /// Gets the current value of one of the different options in . + /// Gets the current value of one of the different options in . /// /// specifies which capture option should be retrieved. /// @@ -201,7 +201,7 @@ namespace Ryujinx.Graphics.RenderDocApi public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); /// - /// Gets the current value of one of the different options in . + /// Gets the current value of one of the different options in . /// /// specifies which capture option should be retrieved. /// diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index a199b8d03..5d1c7147a 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -65,7 +65,9 @@ namespace Ryujinx.Ava.UI.Renderer public bool EndRenderDocCapture() { - return RenderDoc.IsAvailable && RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); + if (!RenderDoc.IsAvailable) return false; + + return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); } protected virtual void OnWindowDestroying() From da41b493b0d972b1ab8182d221de888c6d2335cd Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 16:25:36 -0600 Subject: [PATCH 15/30] user-defined capture start/stop keybind (Ctrl + Shift + C) --- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 30 ++++++++++++++++--- .../UI/ViewModels/MainWindowViewModel.cs | 8 +++++ src/Ryujinx/UI/Windows/MainWindow.axaml | 1 + 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index 5d1c7147a..fb74d46bd 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -51,21 +51,43 @@ namespace Ryujinx.Ava.UI.Renderer protected virtual void OnWindowDestroyed() { } - public void StartRenderDocCapture(Switch device) + public bool ToggleRenderDocCapture(Switch device) { - if (!RenderDoc.IsAvailable) return; + if (!RenderDoc.IsAvailable) return false; - if (RenderDoc.IsFrameCapturing) return; + if (RenderDoc.IsFrameCapturing) + { + if (EndRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture."); + return true; + } + } + else if (StartRenderDocCapture(device)) + { + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + return true; + } + + return false; + } + + public bool StartRenderDocCapture(Switch device) + { + if (!RenderDoc.IsAvailable) return false; + + if (RenderDoc.IsFrameCapturing) return false; RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); RenderDoc.SetCaptureTitle(TitleHelper.TruncatedApplicationTitle(device.Processes.ActiveApplication, Program.Version)); - Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + return true; } public bool EndRenderDocCapture() { if (!RenderDoc.IsAvailable) return false; + if (!RenderDoc.IsFrameCapturing) return false; return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index c0678e6cf..2f9d170cf 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1880,6 +1880,11 @@ namespace Ryujinx.Ava.UI.ViewModels ); } + public void ToggleCapture() + { + AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device); + } + public void ToggleFullscreen() { if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) @@ -2500,7 +2505,10 @@ namespace Ryujinx.Ava.UI.ViewModels viewModel => { if (!RenderDoc.IsFrameCapturing) + { viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device); + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + } viewModel.RenderDocIsCapturing = true; }); diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml index ef47bf4dc..b7385c9cb 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -42,6 +42,7 @@ + From a89bca578bfd3271a8287d85c856595c7a652f21 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 16:30:48 -0600 Subject: [PATCH 16/30] set RenderDocIsCapturing to RenderDoc.IsFrameCapturing after starting/stopping, instead of manually setting it to true/false --- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 2f9d170cf..7d7d2a036 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -2510,7 +2510,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); } - viewModel.RenderDocIsCapturing = true; + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; }); public static RelayCommand EndRenderDocCapture { get; } = @@ -2525,7 +2525,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - viewModel.RenderDocIsCapturing = false; + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; }); #endregion From 074d14477ac566572f6b980f54fc95f046602876 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 28 Dec 2025 16:31:17 -0600 Subject: [PATCH 17/30] respect the new return value of StartRenderDocCapture for logging --- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 7d7d2a036..dbaa13124 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -2506,8 +2506,11 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!RenderDoc.IsFrameCapturing) { - viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device); - Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + if (viewModel.AppHost.RendererHost + .EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device)) + { + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + } } viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; From e28354cfa5912cfbdb83215f8bb33fbeec6769bd Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 29 Dec 2025 00:33:45 -0600 Subject: [PATCH 18/30] RenderDoc capture title format CLI flag --- src/Ryujinx/Utilities/CommandLineState.cs | 17 +++++++++++++++++ src/Ryujinx/Utilities/TitleHelper.cs | 16 +++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/Utilities/CommandLineState.cs b/src/Ryujinx/Utilities/CommandLineState.cs index 28f302e9d..a4e6cd811 100644 --- a/src/Ryujinx/Utilities/CommandLineState.cs +++ b/src/Ryujinx/Utilities/CommandLineState.cs @@ -19,6 +19,9 @@ namespace Ryujinx.Ava.Utilities public static string OverrideSystemLanguage { get; private set; } public static string OverrideHideCursor { get; private set; } public static string BaseDirPathArg { get; private set; } + + public static string RenderDocCaptureTitleFormat { get; private set; } = + "{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}"; public static Optional FirmwareToInstallPathArg { get; set; } public static string Profile { get; private set; } public static string LaunchPathArg { get; private set; } @@ -54,6 +57,20 @@ namespace Ryujinx.Ava.Utilities BaseDirPathArg = args[++i]; + arguments.Add(arg); + arguments.Add(args[i]); + break; + case "-rdct": + case "--rd-capture-title-format": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + RenderDocCaptureTitleFormat = args[++i]; + arguments.Add(arg); arguments.Add(args[i]); break; diff --git a/src/Ryujinx/Utilities/TitleHelper.cs b/src/Ryujinx/Utilities/TitleHelper.cs index b03ce33f4..1ded61dbd 100644 --- a/src/Ryujinx/Utilities/TitleHelper.cs +++ b/src/Ryujinx/Utilities/TitleHelper.cs @@ -1,3 +1,4 @@ +using Gommon; using Ryujinx.HLE.Loaders.Processes; namespace Ryujinx.Ava.Utilities @@ -28,12 +29,17 @@ namespace Ryujinx.Ava.Utilities if (activeProcess == null) return string.Empty; - string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}"; - string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}"; - string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})"; - string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)"; + string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : activeProcess.Name; + string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $"v{activeProcess.DisplayVersion}"; + string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})"; + string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)"; - return $"{applicationVersion}\n{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; + return CommandLineState.RenderDocCaptureTitleFormat + .ReplaceIgnoreCase("{EmuVersion}", applicationVersion) + .ReplaceIgnoreCase("{GuestName}", titleNameSection) + .ReplaceIgnoreCase("{GuestVersion}", titleVersionSection) + .ReplaceIgnoreCase("{GuestTitleId}", titleIdSection) + .ReplaceIgnoreCase("{GuestArch}", titleArchSection); } } } From e419d2ebda5a84294b3f5cdc2d51d7667f23b5a3 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 29 Dec 2025 01:07:01 -0600 Subject: [PATCH 19/30] Expose the return value of DiscardFrameCapture. --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 6115cada7..83a37e0a7 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -424,14 +424,15 @@ namespace Ryujinx.Graphics.RenderDocApi ///
/// a handle to the API ‘device’ object that will be set active. May be to wildcard match. /// a handle to the platform window handle that will be set active. May be to wildcard match. + /// true if the capture was discarded; false if there was an error or no capture was in progress. /// Requires RenderDoc API version 1.4 [RenderDocApiVersion(1, 4)] - public static void DiscardFrameCapture(nint hDevice, nint hWindow) + public static bool DiscardFrameCapture(nint hDevice, nint hWindow) { - if (Api is null) return; + if (Api is null) return false; AssertAtLeast(1, 4); - Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow); + return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0; } From 08af8d8cf8e170f9bf48494de0e6dc482e0c1137 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 29 Dec 2025 01:07:30 -0600 Subject: [PATCH 20/30] Use uint in more places where the C++ API uses uint8_t --- src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs | 6 +++--- .../RenderDocApi.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 83a37e0a7..c6550fb48 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -318,12 +318,12 @@ namespace Ryujinx.Graphics.RenderDocApi if (commandLine == null) { - return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, null) != 0; + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0; } fixed (byte* ptr = commandLine.ToNullTerminatedByteArray()) { - return Api->LaunchReplayUI(connectTargetControl ? 1 : 0, ptr) != 0; + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0; } } @@ -376,7 +376,7 @@ namespace Ryujinx.Graphics.RenderDocApi /// the number of frames to capture. /// Requires RenderDoc API version 1.1 [RenderDocApiVersion(1, 1)] - public static void TriggerMultiFrameCapture(int numFrames) + public static void TriggerMultiFrameCapture(uint numFrames) { if (Api is null) return; diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs index 598bd58e2..70565b55a 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs @@ -22,27 +22,27 @@ namespace Ryujinx.Graphics.RenderDocApi public delegate* unmanaged[Cdecl] GetCaptureFilePathTemplate; public delegate* unmanaged[Cdecl] GetNumCaptures; - public delegate* unmanaged[Cdecl] GetCapture; + public delegate* unmanaged[Cdecl] GetCapture; public delegate* unmanaged[Cdecl] TriggerCapture; - public delegate* unmanaged[Cdecl] IsTargetControlConnected; - public delegate* unmanaged[Cdecl] LaunchReplayUI; + 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; + public delegate* unmanaged[Cdecl] IsFrameCapturing; + public delegate* unmanaged[Cdecl] EndFrameCapture; // 1.1 - public delegate* unmanaged[Cdecl] TriggerMultiFrameCapture; + public delegate* unmanaged[Cdecl] TriggerMultiFrameCapture; // 1.2 public delegate* unmanaged[Cdecl] SetCaptureFileComments; // 1.3 - public delegate* unmanaged[Cdecl] DiscardFrameCapture; + public delegate* unmanaged[Cdecl] DiscardFrameCapture; // 1.5 - public delegate* unmanaged[Cdecl] ShowReplayUI; + public delegate* unmanaged[Cdecl] ShowReplayUI; // 1.6 public delegate* unmanaged[Cdecl] SetCaptureTitle; From 1910ab363a8ab60bad181c1936389b4d3ffd1d2b Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 29 Dec 2025 01:18:48 -0600 Subject: [PATCH 21/30] Add Discord Capture option under End Capture. --- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 8 +++++++ .../UI/ViewModels/MainWindowViewModel.cs | 21 +++++++++++++++++-- .../UI/Views/Main/MainMenuBarView.axaml | 7 +++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index fb74d46bd..b251ee882 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -92,6 +92,14 @@ namespace Ryujinx.Ava.UI.Renderer return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); } + public bool DiscardRenderDocCapture() + { + if (!RenderDoc.IsAvailable) return false; + if (!RenderDoc.IsFrameCapturing) return false; + + return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle); + } + protected virtual void OnWindowDestroying() { WindowHandle = nint.Zero; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index dbaa13124..d6f81e107 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1882,6 +1882,8 @@ namespace Ryujinx.Ava.UI.ViewModels public void ToggleCapture() { + if (ShowLoadProgress) return; + AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device); } @@ -2501,7 +2503,7 @@ namespace Ryujinx.Ava.UI.ViewModels } public static RelayCommand StartRenderDocCapture { get; } = - Commands.CreateConditional(_ => RenderDoc.IsAvailable, + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, viewModel => { if (!RenderDoc.IsFrameCapturing) @@ -2517,7 +2519,7 @@ namespace Ryujinx.Ava.UI.ViewModels }); public static RelayCommand EndRenderDocCapture { get; } = - Commands.CreateConditional(_ => RenderDoc.IsAvailable, + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, viewModel => { if (RenderDoc.IsFrameCapturing) @@ -2531,6 +2533,21 @@ namespace Ryujinx.Ava.UI.ViewModels viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; }); + public static RelayCommand DiscardRenderDocCapture { get; } = + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture."); + } + } + + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + }); + #endregion } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 5b567fa46..9b4a28b57 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -216,6 +216,13 @@ Header="End RenderDoc Frame Capture" Icon="{ext:Icon fa-solid fa-video-slash}" IsEnabled="{Binding IsGameRunning}" /> + From 31cb0a6a1362bd31891ebd8de8b59405cfce88fc Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 30 Dec 2025 15:43:31 -0600 Subject: [PATCH 22/30] rename TruncatedApplicationTitle --- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 2 +- src/Ryujinx/Utilities/TitleHelper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index b251ee882..687047672 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -79,7 +79,7 @@ namespace Ryujinx.Ava.UI.Renderer if (RenderDoc.IsFrameCapturing) return false; RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); - RenderDoc.SetCaptureTitle(TitleHelper.TruncatedApplicationTitle(device.Processes.ActiveApplication, Program.Version)); + RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version)); return true; } diff --git a/src/Ryujinx/Utilities/TitleHelper.cs b/src/Ryujinx/Utilities/TitleHelper.cs index 1ded61dbd..3d1e53fd7 100644 --- a/src/Ryujinx/Utilities/TitleHelper.cs +++ b/src/Ryujinx/Utilities/TitleHelper.cs @@ -24,7 +24,7 @@ namespace Ryujinx.Ava.Utilities : appTitle; } - public static string TruncatedApplicationTitle(ProcessResult activeProcess, string applicationVersion) + public static string FormatRenderDocCaptureTitle(ProcessResult activeProcess, string applicationVersion) { if (activeProcess == null) return string.Empty; From 069c19b2d5a5bd65dbde14e035867fe083993321 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 13:38:21 -0600 Subject: [PATCH 23/30] localize RenderDoc menu items --- assets/Locales/RenderDoc.json | 79 +++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml | 6 +- 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 assets/Locales/RenderDoc.json diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json new file mode 100644 index 000000000..2549a8af7 --- /dev/null +++ b/assets/Locales/RenderDoc.json @@ -0,0 +1,79 @@ +{ + "Locales": [ + { + "ID": "MenuBarActions_StartCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Start RenderDoc Frame Capture", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "MenuBarActions_EndCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "End RenderDoc Frame Capture", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "MenuBarActions_DiscardCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Discard RenderDoc Frame Capture", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + } + ] +} diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 9b4a28b57..3e111ac59 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -206,21 +206,21 @@ IsVisible="{Binding ShowStartCaptureButton}" Command="{Binding StartRenderDocCapture}" CommandParameter="{Binding}" - Header="Start RenderDoc Frame Capture" + Header="{ext:Locale RenderDoc_MenuBarActions_StartCapture}" Icon="{ext:Icon fa-solid fa-video}" IsEnabled="{Binding IsGameRunning}" /> From 4f5ff9f3c6ba1b8a51ca2e37f6e53284655cdc2d Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 14:35:23 -0600 Subject: [PATCH 24/30] fix renderdocapi readme title --- src/Ryujinx.Graphics.RenderDocApi/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/README.md b/src/Ryujinx.Graphics.RenderDocApi/README.md index d134c57d5..51f568b28 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/README.md +++ b/src/Ryujinx.Graphics.RenderDocApi/README.md @@ -1,5 +1,5 @@ -# Ryujinx.Graphics.RenderDoc +# Ryujinx.Graphics.RenderDocApi 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 +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. From f88d1533a8aa39d9eba84eefd5ade6937e975805 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 15:07:00 -0600 Subject: [PATCH 25/30] static RenderDoc.GetCaptures() --- .../RenderDoc.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index c6550fb48..51bc7c12c 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -279,6 +280,29 @@ namespace Ryujinx.Graphics.RenderDocApi Api->TriggerCapture(); } + + /// + /// Gets the details of all frame capture in the current session. + /// This simply calls for each index available as specified by . + /// + /// An immutable array of structs representing RenderDoc Captures. + public static ImmutableArray GetCaptures() + { + if (Api is null) return []; + int captureCount = CaptureCount; + if (captureCount is 0) return []; + + ImmutableArray.Builder captures = ImmutableArray.CreateBuilder(captureCount); + + for (int captureIndex = 0; captureIndex < captureCount; captureIndex++) + { + if (GetCapture(captureIndex) is { } capture) + captures.Add(capture); + } + + return captures.DrainToImmutable(); + } + /// /// Gets the details of a particular frame capture, as specified by an index from 0 to - 1. /// From 5200afca02d59b3faac471a2b1c3f5ea42b11c60 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 19:38:22 -0600 Subject: [PATCH 26/30] move VkInstance extension to vulkan project --- .../Helpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/{Ryujinx.Graphics.RenderDocApi => Ryujinx.Graphics.Vulkan}/Helpers.cs (94%) diff --git a/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs b/src/Ryujinx.Graphics.Vulkan/Helpers.cs similarity index 94% rename from src/Ryujinx.Graphics.RenderDocApi/Helpers.cs rename to src/Ryujinx.Graphics.Vulkan/Helpers.cs index ebd30d113..d29ac3440 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/Helpers.cs +++ b/src/Ryujinx.Graphics.Vulkan/Helpers.cs @@ -1,9 +1,9 @@ using Silk.NET.Vulkan; using System.Runtime.CompilerServices; -namespace Ryujinx.Graphics.RenderDocApi +namespace Ryujinx.Graphics.Vulkan { - public static partial class Helpers + public static class Helpers { extension(Vk api) { From 42a5a216a719d1f7799f5b9820325075c7aa867f Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 19:38:31 -0600 Subject: [PATCH 27/30] remove dependency on OpenGL/Vulkan backends in Ryujinx.Graphics.RenderDocApi --- .../Ryujinx.Graphics.RenderDocApi.csproj | 6 ------ src/Ryujinx/Ryujinx.csproj | 2 ++ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj index 4598335eb..29c1d818a 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj +++ b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj @@ -6,10 +6,4 @@ enable true - - - - - - diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index a8b4b8628..28aec175b 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -79,6 +79,8 @@ + + From c61a508bea336a9cc38a7969324b5d414418f3dc Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 19:58:11 -0600 Subject: [PATCH 28/30] tooltip for discard (all renderdoc texts are broken it seems, will fix before merge) --- assets/Locales/RenderDoc.json | 25 +++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml | 1 + 2 files changed, 26 insertions(+) diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json index 2549a8af7..132e24067 100644 --- a/assets/Locales/RenderDoc.json +++ b/assets/Locales/RenderDoc.json @@ -74,6 +74,31 @@ "zh_CN": "", "zh_TW": "" } + }, + { + "ID": "MenuBarActions_DiscardCapture_ToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } } ] } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 3e111ac59..13a5d4a40 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -221,6 +221,7 @@ Command="{Binding DiscardRenderDocCapture}" CommandParameter="{Binding}" Header="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture}" + ToolTip.Tip="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture_ToolTip}" Icon="{ext:Icon fa-solid fa-video-slash}" IsEnabled="{Binding IsGameRunning}" /> From 9099004b5c1090b969c7a9a6f13af21452d6c572 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 20:33:35 -0600 Subject: [PATCH 29/30] fix toggle keybind not updating start/end & discard buttons being visible --- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index d6f81e107..96159a1ea 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1885,6 +1885,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (ShowLoadProgress) return; AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device); + RenderDocIsCapturing = RenderDoc.IsFrameCapturing; } public void ToggleFullscreen() From f5c66a52210770e778c05d581541c629ec2f24d2 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 31 Dec 2025 21:19:52 -0600 Subject: [PATCH 30/30] RenderDoc.SetMostRecentCaptureFileComments --- .../RenderDoc.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs index 51bc7c12c..9d1f53957 100644 --- a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -408,6 +408,28 @@ namespace Ryujinx.Graphics.RenderDocApi Api->TriggerMultiFrameCapture(numFrames); } + /// + /// Adds an arbitrary comments field to the most recent capture, + /// which will then be displayed in the UI to anyone opening the capture. + ///

+ /// This is equivalent to calling with a null first (fileName) parameter. + ///
+ /// the comments to set in the capture file. + /// Requires RenderDoc API version 1.2 + public static void SetMostRecentCaptureFileComments(string comments) + { + if (Api is null) return; + + AssertAtLeast(1, 2); + + byte[] commentBytes = comments.ToNullTerminatedByteArray(); + + fixed (byte* pcomment = commentBytes) + { + Api->SetCaptureFileComments((byte*)nint.Zero, pcomment); + } + } + /// /// Adds an arbitrary comments field to an existing capture on disk, /// which will then be displayed in the UI to anyone opening the capture. @@ -531,7 +553,7 @@ namespace Ryujinx.Graphics.RenderDocApi } private static RenderDocApi* _api = null; - private static bool _loaded = false; + private static bool _loaded; private static RenderDocApi* Api {