RenderDoc integration (start/stop capture menu bar items)

This commit is contained in:
GreemDev 2025-12-27 17:28:58 -06:00
parent 4c64300576
commit 938831a901
19 changed files with 730 additions and 14 deletions

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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
}
}

View file

@ -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.

View file

@ -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
{
/// <summary>
/// True if the API is available.
/// </summary>
public static bool IsAvailable => Api != null;
/// <summary>
/// Set the minimum version of the API you require.
/// </summary>
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
public static Version MinimumRequired { get; set; } = new Version(1, 0, 0);
/// <summary>
/// Set to true to assert versions.
/// </summary>
public static bool AssertVersionEnabled { get; set; } = true;
/// <summary>
/// Version of the API available.
/// </summary>
[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<InputButton> buttons)
{
fixed (InputButton* ptr = buttons)
{
Api->SetFocusToggleKeys(ptr, buttons.Length);
}
}
[RenderDocApiVersion(1, 0)]
public static void SetCaptureKeys(ReadOnlySpan<InputButton> 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<byte> bytes = stackalloc byte[length + 1];
long timestamp;
fixed (byte* ptr = bytes)
Api->GetCapture(index, ptr, &length, &timestamp);
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]<RenderDocVersion, RenderDocApi**, int>)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<RenderDocApiVersionAttribute>()!.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);
}
}

View file

@ -0,0 +1,51 @@
namespace Ryujinx.Graphics.RenderDocApi
{
#pragma warning disable CS0649
internal unsafe struct RenderDocApi
{
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
public delegate* unmanaged[Cdecl]<CaptureOption, int, int> SetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<CaptureOption, int> GetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, int> GetCapture;
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
public delegate* unmanaged[Cdecl]<int> IsTargetControlConnected;
public delegate* unmanaged[Cdecl]<int, byte*, int> LaunchReplayUI;
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
public delegate* unmanaged[Cdecl]<int> IsFrameCapturing;
public delegate* unmanaged[Cdecl]<void*, void*, int> EndFrameCapture;
// 1.1
public delegate* unmanaged[Cdecl]<int, void> TriggerMultiFrameCapture;
// 1.2
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
// 1.3
public delegate* unmanaged[Cdecl]<void*, void*, int> DiscardFrameCapture;
// 1.5
public delegate* unmanaged[Cdecl]<int> ShowReplayUI;
// 1.6
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
}
#pragma warning restore CS0649
}

View file

@ -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);
}
}
}

View file

@ -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,
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="../Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using Silk.NET.Vulkan;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Vulkan
{
public static class Helpers
{
extension(Vk api)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer() =>
api.CurrentInstance is not null
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
: null;
}
extension(Instance instance)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer()
=> (*((void**)(instance.Handle)));
}
}
}

View file

@ -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;

View file

@ -78,7 +78,7 @@
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
@ -86,7 +86,6 @@
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
<ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

View file

@ -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<nint> 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);

View file

@ -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;

View file

@ -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<ApplicationData> _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<MainWindowViewModel> StartRenderDocCapture { get; } =
Commands.CreateConditional<MainWindowViewModel>(_ => RenderDoc.IsAvailable,
viewModel =>
{
if (!RenderDoc.IsFrameCapturing)
viewModel.AppHost.RendererHost.EmbeddedWindow.StartRenderDocCapture();
viewModel.RenderDocIsCapturing = true;
});
public static RelayCommand<MainWindowViewModel> EndRenderDocCapture { get; } =
Commands.CreateConditional<MainWindowViewModel>(_ => RenderDoc.IsAvailable,
viewModel =>
{
if (RenderDoc.IsFrameCapturing)
viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture();
viewModel.RenderDocIsCapturing = false;
});
#endregion
}
}

View file

@ -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">
<Design.DataContext>
@ -200,6 +201,21 @@
Header="{ext:Locale GameListContextMenuManageCheat}"
Icon="{ext:Icon fa-solid fa-code}"
IsEnabled="{Binding IsGameRunning}" />
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
<MenuItem
IsVisible="{Binding ShowStartCaptureButton}"
Command="{Binding StartRenderDocCapture}"
CommandParameter="{Binding}"
Header="Start RenderDoc Frame Capture"
Icon="{ext:Icon fa-solid fa-video}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
IsVisible="{Binding ShowEndCaptureButton}"
Command="{Binding EndRenderDocCapture}"
CommandParameter="{Binding}"
Header="End RenderDoc Frame Capture"
Icon="{ext:Icon fa-solid fa-video-slash}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">

View file

@ -41,6 +41,7 @@
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
</Window.KeyBindings>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*">
<helpers:OffscreenTextBox IsEnabled="False" Opacity="0" Name="HiddenTextBox" IsHitTestVisible="False" IsTabStop="False" />