From 453b246faa9e6d4c7de1625c62f7ed2f200c4749 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 31 Dec 2025 09:15:40 -0600 Subject: [PATCH 01/13] fix (ryubing/ryujinx!243) See merge request ryubing/ryujinx!243 --- src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 6c9f4f367..16cfb34d6 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -102,7 +102,7 @@ namespace Ryujinx.Ava.UI.Views.Main Margin = new Thickness(3, 0, 3, 0), HorizontalAlignment = HorizontalAlignment.Stretch, Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName, - Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(language)) + Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code)) }; yield return menuItem; From 0a3db19b28f76f3959088c9b7b8a3a796c61f084 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 31 Dec 2025 10:30:35 -0600 Subject: [PATCH 02/13] fix language switching 2 (ryubing/ryujinx!244) See merge request ryubing/ryujinx!244 --- src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 16cfb34d6..f35d72b6f 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -101,7 +101,7 @@ namespace Ryujinx.Ava.UI.Views.Main Padding = new Thickness(15, 0, 0, 0), Margin = new Thickness(3, 0, 3, 0), HorizontalAlignment = HorizontalAlignment.Stretch, - Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName, + Header = code == currentLanguageCode ? $"{languageName} ✔" : languageName, Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code)) }; From 4c64300576b6e08beeac58cf86bd85d62b8ada03 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 31 Dec 2025 20:21:35 -0600 Subject: [PATCH 03/13] fix new locale files data loading (ryubing/ryujinx!245) See merge request ryubing/ryujinx!245 --- src/Ryujinx/Common/LocaleManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index 330ef4f18..4433f9859 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -190,7 +190,7 @@ namespace Ryujinx.Ava.Common.Locale } - foreach (LocalesJson file in _localeData.Value.LocalesFiles.Values) + foreach ((string fileName, LocalesJson file) in _localeData.Value.LocalesFiles) { foreach (LocalesEntry locale in file.Locales) { @@ -206,7 +206,7 @@ namespace Ryujinx.Ava.Common.Locale $"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); } - if (!Enum.TryParse(locale.ID, out LocaleKeys localeKey)) + if (!Enum.TryParse(fileName == "Root.json" ? locale.ID : $"{fileName[..^".json".Length]}_{locale.ID}" , out LocaleKeys localeKey)) continue; string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) From fa55608587686c2d9335ce4338405f569552566d Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 1 Jan 2026 00:10:21 -0600 Subject: [PATCH 04/13] RenderDoc API support (ryubing/ryujinx!242) See merge request ryubing/ryujinx!242 --- Ryujinx.sln | 14 + assets/Locales/RenderDoc.json | 104 +++ src/Ryujinx.Graphics.RenderDocApi/Capture.cs | 12 + .../CaptureOption.cs | 100 +++ .../InputButton.cs | 83 +++ .../OverlayBits.cs | 39 ++ src/Ryujinx.Graphics.RenderDocApi/README.md | 5 + .../RenderDoc.cs | 639 ++++++++++++++++++ .../RenderDocApi.cs | 51 ++ .../RenderDocApiVersionAttribute.cs | 16 + .../RenderDocVersion.cs | 47 ++ .../Ryujinx.Graphics.RenderDocApi.csproj | 9 + src/Ryujinx.Graphics.Vulkan/Helpers.cs | 32 + src/Ryujinx/Program.cs | 1 + src/Ryujinx/Ryujinx.csproj | 5 +- src/Ryujinx/UI/Renderer/EmbeddedWindow.cs | 63 +- src/Ryujinx/UI/RyujinxApp.axaml.cs | 1 + .../UI/ViewModels/MainWindowViewModel.cs | 105 ++- .../UI/Views/Main/MainMenuBarView.axaml | 24 + src/Ryujinx/UI/Windows/MainWindow.axaml | 2 + src/Ryujinx/Utilities/CommandLineState.cs | 17 + src/Ryujinx/Utilities/TitleHelper.cs | 19 + 22 files changed, 1374 insertions(+), 14 deletions(-) create mode 100644 assets/Locales/RenderDoc.json 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/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json new file mode 100644 index 000000000..132e24067 --- /dev/null +++ b/assets/Locales/RenderDoc.json @@ -0,0 +1,104 @@ +{ + "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": "" + } + }, + { + "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.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..cd3f860d2 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs @@ -0,0 +1,100 @@ +// ReSharper disable UnusedMember.Global + +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/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..e973e403c --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs @@ -0,0 +1,39 @@ +// ReSharper disable UnusedMember.Global + +using System; + +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 + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/README.md b/src/Ryujinx.Graphics.RenderDocApi/README.md new file mode 100644 index 000000000..51f568b28 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/README.md @@ -0,0 +1,5 @@ +# 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. diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs new file mode 100644 index 000000000..9d1f53957 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -0,0 +1,639 @@ +using System; +using System.Collections.Immutable; +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 partial 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 RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.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); + } + } + + /// + /// The current mask which determines what sections of the overlay render on each window. + /// + [RenderDocApiVersion(1, 0)] + public static OverlayBits OverlayBits + { + get => Api->GetOverlayBits(); + set + { + Api->MaskOverlayBits(~value, value); + } + } + + /// + /// 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 + { + get + { + byte* ptr = Api->GetCaptureFilePathTemplate(); + return Marshal.PtrToStringUTF8((nint)ptr)!; + } + set + { + fixed (byte* ptr = value.ToNullTerminatedByteArray()) + { + Api->SetCaptureFilePathTemplate(ptr); + } + } + } + + /// + /// The amount of frame captures that have been made. + /// + [RenderDocApiVersion(1, 0)] + public static int CaptureCount => Api->GetNumCaptures(); + + /// + /// Checks if the RenderDoc UI is currently connected to this process. + /// + [RenderDocApiVersion(1, 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 is not null && 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, uint integer) + { + 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. + /// + /// 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 is not null && 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 uint 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 , + /// 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 . + /// + /// 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 uint 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) + { + if (Api is null) return; + + fixed (InputButton* ptr = buttons) + { + Api->SetFocusToggleKeys(ptr, buttons.Length); + } + } + + /// + /// 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) + { + if (Api is null) return; + + fixed (InputButton* ptr = buttons) + { + Api->SetCaptureKeys(ptr, buttons.Length); + } + } + + /// + /// 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() + { + if (Api is null) return; + + 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() + { + if (Api is null) return; + + 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() + { + if (Api is null) return; + + 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. + /// + /// 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) + { + if (Api is null) return null; + + 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[length..]); + 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 = null) + { + if (Api is null) return false; + + if (commandLine == null) + { + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0; + } + + fixed (byte* ptr = commandLine.ToNullTerminatedByteArray()) + { + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0; + } + } + + /// + /// 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) + { + if (Api is null) return; + + 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) + { + if (Api is null) return; + + 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) + { + if (Api is null) return false; + + 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(uint numFrames) + { + if (Api is null) return; + + AssertAtLeast(1, 1); + 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. + /// + /// 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) + { + if (Api is null) return; + + AssertAtLeast(1, 2); + + byte[] commentBytes = comments.ToNullTerminatedByteArray(); + + fixed (byte* pcomment = commentBytes) + { + if (fileName is null) + { + Api->SetCaptureFileComments((byte*)nint.Zero, pcomment); + } + else + { + byte[] fileBytes = fileName.ToNullTerminatedByteArray(); + + 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. + /// 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 bool DiscardFrameCapture(nint hDevice, nint hWindow) + { + if (Api is null) return false; + + AssertAtLeast(1, 4); + return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0; + } + + + /// + /// 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() + { + if (Api is null) return false; + + AssertAtLeast(1, 5); + 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) + { + if (Api is null) return; + + AssertAtLeast(1, 6); + fixed (byte* ptr = title.ToNullTerminatedByteArray()) + 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. + /// The version of the RenderDoc API required by your application. + public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null) + { + if (_loaded && !ignoreAlreadyLoaded) + return; + + lock (typeof(RenderDoc)) + { + // Prevent double loads. + if (_loaded && !ignoreAlreadyLoaded) + return; + + if (requiredVersion.HasValue) + MinimumRequired = requiredVersion.Value; + + _loaded = true; + _api = GetApi(MinimumRequired); + + if (_api != null) + AssertAtLeast(MinimumRequired); + } + } + + private static RenderDocApi* _api = null; + private static bool _loaded; + + private static RenderDocApi* Api + { + get + { + ReloadApi(); + return _api; + } + } + + private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex(); + + private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0) + { + foreach (ProcessModule module in Process.GetCurrentProcess().Modules) + { + string moduleName = module.FileName ?? string.Empty; + + if (!_dynamicLibraryPattern.IsMatch(moduleName)) + continue; + + if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle)) + return null; + + if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint 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(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) + 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 RenderDoc API {minVersion}. Current API version is {Version}."); + } + + private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + + return encoding.GetBytes(str + '\0'); + } + + [GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex RenderDocApiDynamicLibraryRegex(); + + #endregion + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs new file mode 100644 index 000000000..70565b55a --- /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..1b386c435 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs @@ -0,0 +1,47 @@ +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum RenderDocVersion + { + 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, + } + + 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); + } + } + } + } +} 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..29c1d818a --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj @@ -0,0 +1,9 @@ + + + + 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..28aec175b 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -78,7 +78,9 @@ - + + + @@ -86,7 +88,6 @@ - diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index e360d42f7..687047672 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -2,8 +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; @@ -30,6 +34,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 +51,55 @@ namespace Ryujinx.Ava.UI.Renderer protected virtual void OnWindowDestroyed() { } + public bool ToggleRenderDocCapture(Switch device) + { + if (!RenderDoc.IsAvailable) return false; + + 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.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version)); + + return true; + } + + public bool EndRenderDocCapture() + { + if (!RenderDoc.IsAvailable) return false; + if (!RenderDoc.IsFrameCapturing) return false; + + 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; @@ -124,7 +178,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 +194,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 +217,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..96159a1ea 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,29 @@ 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 ToggleCapture() + { + if (ShowLoadProgress) return; + + AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device); + RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + } + public void ToggleFullscreen() { if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) @@ -1955,7 +1978,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 +1988,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 +2001,9 @@ namespace Ryujinx.Ava.UI.ViewModels NotificationHelper.ShowWarning( LocaleManager.Instance.UpdateAndGetDynamicValue( - LocaleKeys.NotificationLaunchCheckDramSizeTitle, + LocaleKeys.NotificationLaunchCheckDramSizeTitle, LocaleManager.Instance[memoryConfigurationLocaleKey] - ), + ), LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]); } } @@ -2462,6 +2488,67 @@ 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(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (!RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost + .EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device)) + { + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + } + } + + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + }); + + public static RelayCommand EndRenderDocCapture { get; } = + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture."); + } + } + + 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 47f79725c..13a5d4a40 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,29 @@ 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..b7385c9cb 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -41,6 +41,8 @@ + + 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 5e0916c27..3d1e53fd7 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 @@ -22,5 +23,23 @@ namespace Ryujinx.Ava.Utilities ? appTitle + $" ({pauseString})" : appTitle; } + + public static string FormatRenderDocCaptureTitle(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 CommandLineState.RenderDocCaptureTitleFormat + .ReplaceIgnoreCase("{EmuVersion}", applicationVersion) + .ReplaceIgnoreCase("{GuestName}", titleNameSection) + .ReplaceIgnoreCase("{GuestVersion}", titleVersionSection) + .ReplaceIgnoreCase("{GuestTitleId}", titleIdSection) + .ReplaceIgnoreCase("{GuestArch}", titleArchSection); + } } } From 99feaafbe622e110a765d7c155987ab91755eabe Mon Sep 17 00:00:00 2001 From: Babib3l Date: Sat, 3 Jan 2026 14:23:11 +0100 Subject: [PATCH 05/13] French and Spanish Translations updates on RenderDoc (ryubing/ryujinx!246) See merge request ryubing/ryujinx!246 --- assets/Locales/RenderDoc.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json index 132e24067..894ff07ca 100644 --- a/assets/Locales/RenderDoc.json +++ b/assets/Locales/RenderDoc.json @@ -7,8 +7,8 @@ "de_DE": "", "el_GR": "", "en_US": "Start RenderDoc Frame Capture", - "es_ES": "", - "fr_FR": "", + "es_ES": "Iniciar una captura de fotograma de RenderDoc", + "fr_FR": "Démarrer une capture de trame RenderDoc", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -32,8 +32,8 @@ "de_DE": "", "el_GR": "", "en_US": "End RenderDoc Frame Capture", - "es_ES": "", - "fr_FR": "", + "es_ES": "Detener la captura de fotograma de RenderDoc", + "fr_FR": "Arrêter la capture de trame RenderDoc", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -57,8 +57,8 @@ "de_DE": "", "el_GR": "", "en_US": "Discard RenderDoc Frame Capture", - "es_ES": "", - "fr_FR": "", + "es_ES": "Descartar la captura de fotograma de RenderDoc", + "fr_FR": "Supprimer la capture de trame RenderDoc", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -82,8 +82,8 @@ "de_DE": "", "el_GR": "", "en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.", - "es_ES": "", - "fr_FR": "", + "es_ES": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado.", + "fr_FR": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat.", "he_IL": "", "it_IT": "", "ja_JP": "", From f556e8b8fb32a2f6c4bb98063df4d093e49e8d87 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 20 Jan 2026 13:19:35 -0600 Subject: [PATCH 06/13] add offline update server catch branch --- src/Ryujinx/Systems/Updater/Updater.GitLab.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs index deb515797..73fe9f66b 100644 --- a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs +++ b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs @@ -7,6 +7,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Systems.Update.Client; using Ryujinx.Systems.Update.Common; using System; +using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -46,6 +47,12 @@ namespace Ryujinx.Ava.Systems return Return.Failure( new MessageError("DNS resolution error occurred. Is your internet down?")); } + catch (HttpRequestException hre) + when (hre.StatusCode is HttpStatusCode.BadGateway) + { + return Return.Failure( + new MessageError("Could not connect to the update server, but it appears like you have internet. It seems like the update server is offline, try again later.")); + } } public static async Task> CheckVersionAsync(bool showVersionUpToDate = false) From c154f66f26b66af461f075a1a50dcd1e7f60f1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Wed, 21 Jan 2026 18:23:34 -0600 Subject: [PATCH 07/13] Update Korean translation (ryubing/ryujinx!251) See merge request ryubing/ryujinx!251 --- assets/Locales/RenderDoc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json index 894ff07ca..b3f9462eb 100644 --- a/assets/Locales/RenderDoc.json +++ b/assets/Locales/RenderDoc.json @@ -12,7 +12,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 시작", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -37,7 +37,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 종료", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -62,7 +62,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 폐기", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -87,7 +87,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", From d271abe19aa308b3ba12f51c6eb21b288e2ea63b Mon Sep 17 00:00:00 2001 From: Stossy11 Date: Wed, 28 Jan 2026 10:03:59 +1100 Subject: [PATCH 08/13] [ci skip] Add macOS native Audio Backend (ryubing/ryujinx!252) See merge request ryubing/ryujinx!252 THIS IS CURRENTLY NOT EXPOSED BY THE UI OR HANDLED BY THE EMULATOR. Expect a commit later to add it to configs, UI, etc. --- .../AppleAudioBuffer.cs | 16 + .../AppleHardwareDeviceDriver.cs | 241 +++++++++++++++ .../AppleHardwareDeviceSession.cs | 288 ++++++++++++++++++ .../Native/AudioToolbox.cs | 103 +++++++ .../Ryujinx.Audio.Backends.Apple.csproj | 13 + 5 files changed, 661 insertions(+) create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs b/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs new file mode 100644 index 000000000..995236889 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Audio.Backends.Apple +{ + class AppleAudioBuffer + { + public readonly ulong DriverIdentifier; + public readonly ulong SampleCount; + public ulong SamplePlayed; + + public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount) + { + DriverIdentifier = driverIdentifier; + SampleCount = sampleCount; + SamplePlayed = 0; + } + } +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs new file mode 100644 index 000000000..62d81c6cc --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -0,0 +1,241 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.Versioning; +using Ryujinx.Audio.Backends.Apple.Native; +using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.Apple +{ + [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] + public class AppleHardwareDeviceDriver : IHardwareDeviceDriver + { + private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; + private readonly ConcurrentDictionary _sessions; + private readonly bool _supportSurroundConfiguration; + + public float Volume { get; set; } + + public AppleHardwareDeviceDriver() + { + _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); + _sessions = new ConcurrentDictionary(); + + _supportSurroundConfiguration = TestSurroundSupport(); + + Volume = 1f; + } + + private bool TestSurroundSupport() + { + try + { + var format = GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); + + int result = AudioQueueNewOutput( + ref format, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + 0, + out IntPtr testQueue); + + if (result == 0) + { + AudioChannelLayout layout = new AudioChannelLayout + { + AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A, + AudioChannelBitmap = 0, + NumberChannelDescriptions = 0 + }; + + int layoutResult = AudioQueueSetProperty( + testQueue, + kAudioQueueProperty_ChannelLayout, + ref layout, + (uint)Marshal.SizeOf()); + + if (layoutResult == 0) + { + AudioQueueDispose(testQueue, true); + return true; + } + + AudioQueueDispose(testQueue, true); + } + + return false; + } + catch + { + return false; + } + } + + public static bool IsSupported => IsSupportedInternal(); + + private static bool IsSupportedInternal() + { + try + { + var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); + int result = AudioQueueNewOutput( + ref format, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + 0, + out IntPtr testQueue); + + if (result == 0) + { + AudioQueueDispose(testQueue, true); + return true; + } + + return false; + } + catch + { + return false; + } + } + + public ManualResetEvent GetUpdateRequiredEvent() + { + return _updateRequiredEvent; + } + + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) + { + if (channelCount == 0) + { + channelCount = 2; + } + + if (sampleRate == 0) + { + sampleRate = Constants.TargetSampleRate; + } + + if (direction != Direction.Output) + { + throw new NotImplementedException("Input direction is currently not implemented on Apple backend!"); + } + + AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount); + + _sessions.TryAdd(session, 0); + + return session; + } + + internal bool Unregister(AppleHardwareDeviceSession session) + { + return _sessions.TryRemove(session, out _); + } + + internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, uint channelCount) + { + uint formatFlags; + uint bitsPerChannel; + + switch (sampleFormat) + { + case SampleFormat.PcmInt8: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 8; + break; + case SampleFormat.PcmInt16: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 16; + break; + case SampleFormat.PcmInt32: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 32; + break; + case SampleFormat.PcmFloat: + formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + bitsPerChannel = 32; + break; + default: + throw new ArgumentException($"Unsupported sample format {sampleFormat}"); + } + + uint bytesPerFrame = (bitsPerChannel / 8) * channelCount; + + return new AudioStreamBasicDescription + { + SampleRate = sampleRate, + FormatID = kAudioFormatLinearPCM, + FormatFlags = formatFlags, + BytesPerPacket = bytesPerFrame, + FramesPerPacket = 1, + BytesPerFrame = bytesPerFrame, + ChannelsPerFrame = channelCount, + BitsPerChannel = bitsPerChannel, + Reserved = 0 + }; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (AppleHardwareDeviceSession session in _sessions.Keys) + { + session.Dispose(); + } + + _pauseEvent.Dispose(); + } + } + + public bool SupportsSampleRate(uint sampleRate) + { + return true; + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + return sampleFormat != SampleFormat.PcmInt24; + } + + public bool SupportsChannelCount(uint channelCount) + { + if (channelCount == 6) + { + return _supportSurroundConfiguration; + } + + return true; + } + + public bool SupportsDirection(Direction direction) + { + return direction != Direction.Input; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs new file mode 100644 index 000000000..95826b4f4 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -0,0 +1,288 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.Versioning; +using Ryujinx.Audio.Backends.Apple.Native; +using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; +using static Ryujinx.Audio.Backends.Apple.AppleHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.Apple +{ + [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] + class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private const int NumBuffers = 3; + + private readonly AppleHardwareDeviceDriver _driver; + private readonly ConcurrentQueue _queuedBuffers = new(); + private readonly DynamicRingBuffer _ringBuffer = new(); + private readonly ManualResetEvent _updateRequiredEvent; + + private readonly AudioQueueOutputCallback _callbackDelegate; + private readonly GCHandle _gcHandle; + + private IntPtr _audioQueue; + private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers]; + private readonly int[] _bufferBytesFilled = new int[NumBuffers]; + + private readonly int _bytesPerFrame; + + private ulong _playedSampleCount; + private bool _started; + private float _volume = 1f; + + private readonly object _lock = new(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void AudioQueueOutputCallback( + IntPtr userData, + IntPtr audioQueue, + IntPtr buffer); + + public AppleHardwareDeviceSession( + AppleHardwareDeviceDriver driver, + IVirtualMemoryManager memoryManager, + SampleFormat requestedSampleFormat, + uint requestedSampleRate, + uint requestedChannelCount) + : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount) + { + _driver = driver; + _updateRequiredEvent = driver.GetUpdateRequiredEvent(); + _callbackDelegate = OutputCallback; + _bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount; + + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + + SetupAudioQueue(); + } + + private void SetupAudioQueue() + { + lock (_lock) + { + var format = AppleHardwareDeviceDriver.GetAudioFormat( + RequestedSampleFormat, + RequestedSampleRate, + RequestedChannelCount); + + IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); + IntPtr userData = GCHandle.ToIntPtr(_gcHandle); + + int result = AudioQueueNewOutput( + ref format, + callbackPtr, + userData, + IntPtr.Zero, + IntPtr.Zero, + 0, + out _audioQueue); + + if (result != 0) + { + throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}"); + } + + uint framesPerBuffer = RequestedSampleRate / 100; + uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame; + + for (int i = 0; i < NumBuffers; i++) + { + AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]); + _bufferBytesFilled[i] = 0; + + PrimeBuffer(_audioQueueBuffers[i], i); + } + } + } + + private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex) + { + AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr; + + int capacityBytes = (int)buffer->AudioDataBytesCapacity; + int framesPerBuffer = capacityBytes / _bytesPerFrame; + + int availableFrames = _ringBuffer.Length / _bytesPerFrame; + int framesToRead = Math.Min(availableFrames, framesPerBuffer); + int bytesToRead = framesToRead * _bytesPerFrame; + + Span dst = new((void*)buffer->AudioData, capacityBytes); + dst.Clear(); + + if (bytesToRead > 0) + { + Span audio = dst.Slice(0, bytesToRead); + _ringBuffer.Read(audio, 0, bytesToRead); + ApplyVolume(buffer->AudioData, bytesToRead); + } + + buffer->AudioDataByteSize = (uint)capacityBytes; + _bufferBytesFilled[bufferIndex] = bytesToRead; + + AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero); + } + + private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr) + { + if (!_started || bufferPtr == IntPtr.Zero) + return; + + int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr); + if (bufferIndex < 0) + return; + + int bytesPlayed = _bufferBytesFilled[bufferIndex]; + if (bytesPlayed > 0) + { + ProcessPlayedSamples(bytesPlayed); + } + + PrimeBuffer(bufferPtr, bufferIndex); + } + + private void ProcessPlayedSamples(int bytesPlayed) + { + ulong samplesPlayed = GetSampleCount(bytesPlayed); + ulong remaining = samplesPlayed; + bool needUpdate = false; + + while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer)) + { + ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed); + ulong take = Math.Min(needed, remaining); + + ulong played = Interlocked.Add(ref buffer.SamplePlayed, take); + remaining -= take; + + if (played == buffer.SampleCount) + { + _queuedBuffers.TryDequeue(out _); + needUpdate = true; + } + + Interlocked.Add(ref _playedSampleCount, take); + } + + if (needUpdate) + { + _updateRequiredEvent.Set(); + } + } + + private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize) + { + float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f); + if (volume >= 0.999f) + return; + + int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat); + + switch (RequestedSampleFormat) + { + case SampleFormat.PcmInt16: + short* s16 = (short*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s16[i] = (short)(s16[i] * volume); + break; + + case SampleFormat.PcmFloat: + float* f32 = (float*)dataPtr; + for (int i = 0; i < sampleCount; i++) + f32[i] *= volume; + break; + + case SampleFormat.PcmInt32: + int* s32 = (int*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s32[i] = (int)(s32[i] * volume); + break; + + case SampleFormat.PcmInt8: + sbyte* s8 = (sbyte*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s8[i] = (sbyte)(s8[i] * volume); + break; + } + } + + public override void QueueBuffer(AudioBuffer buffer) + { + _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); + _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.HostTag, GetSampleCount(buffer))); + } + + public override void Start() + { + lock (_lock) + { + if (_started) + return; + + _started = true; + AudioQueueStart(_audioQueue, IntPtr.Zero); + } + } + + public override void Stop() + { + lock (_lock) + { + if (!_started) + return; + + _started = false; + AudioQueuePause(_audioQueue); + } + } + + public override ulong GetPlayedSampleCount() + => Interlocked.Read(ref _playedSampleCount); + + public override float GetVolume() => _volume; + public override void SetVolume(float volume) => _volume = volume; + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer)) + return true; + + return driverBuffer.DriverIdentifier != buffer.HostTag; + } + + public override void PrepareToClose() { } + public override void UnregisterBuffer(AudioBuffer buffer) { } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Stop(); + + if (_audioQueue != IntPtr.Zero) + { + AudioQueueStop(_audioQueue, true); + AudioQueueDispose(_audioQueue, true); + _audioQueue = IntPtr.Zero; + } + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + } + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs new file mode 100644 index 000000000..9a6e8e189 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs @@ -0,0 +1,103 @@ +using Ryujinx.Common.Memory; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Backends.Apple.Native +{ + public static partial class AudioToolbox + { + [StructLayout(LayoutKind.Sequential)] + internal struct AudioStreamBasicDescription + { + public double SampleRate; + public uint FormatID; + public uint FormatFlags; + public uint BytesPerPacket; + public uint FramesPerPacket; + public uint BytesPerFrame; + public uint ChannelsPerFrame; + public uint BitsPerChannel; + public uint Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AudioChannelLayout + { + public uint AudioChannelLayoutTag; + public uint AudioChannelBitmap; + public uint NumberChannelDescriptions; + } + + internal const uint kAudioFormatLinearPCM = 0x6C70636D; + internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c; + internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006; + internal const uint kAudioFormatFlagIsFloat = (1 << 0); + internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2); + internal const uint kAudioFormatFlagIsPacked = (1 << 3); + internal const uint kAudioFormatFlagIsBigEndian = (1 << 1); + internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4); + internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueNewOutput( + ref AudioStreamBasicDescription format, + nint callback, + nint userData, + nint callbackRunLoop, + nint callbackRunLoopMode, + uint flags, + out nint audioQueue); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueSetProperty( + nint audioQueue, + uint propertyID, + ref AudioChannelLayout layout, + uint layoutSize); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueAllocateBuffer( + nint audioQueue, + uint bufferByteSize, + out nint buffer); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueStart(nint audioQueue, nint startTime); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueuePause(nint audioQueue); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueSetParameter( + nint audioQueue, + uint parameterID, + float value); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueEnqueueBuffer( + nint audioQueue, + nint buffer, + uint numPacketDescs, + nint packetDescs); + + [StructLayout(LayoutKind.Sequential)] + internal struct AudioQueueBuffer + { + public uint AudioDataBytesCapacity; + public nint AudioData; + public uint AudioDataByteSize; + public nint UserData; + public uint PacketDescriptionCapacity; + public nint PacketDescriptions; + public uint PacketDescriptionCount; + } + + internal const uint kAudioQueueParam_Volume = 1; + } +} \ No newline at end of file diff --git a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj new file mode 100644 index 000000000..b7e1b6d84 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + true + + + + + + + + From bd388cf4f9421febac45060d63f9ac3754222464 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:28:59 -0600 Subject: [PATCH 09/13] Expose AudioToolkit in UI --- Ryujinx.sln | 4 +++ assets/Locales/Root.json | 27 ++++++++++++++++++- .../AppleHardwareDeviceDriver.cs | 4 ++- .../AppleHardwareDeviceSession.cs | 4 +-- .../Ryujinx.Audio.Backends.Apple.csproj | 2 +- src/Ryujinx/Ryujinx.csproj | 3 ++- src/Ryujinx/Systems/AppHost.cs | 7 +++++ .../Systems/Configuration/AudioBackend.cs | 1 + .../UI/ViewModels/SettingsViewModel.cs | 4 +++ .../UI/Views/Settings/SettingsAudioView.axaml | 3 +++ 10 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Ryujinx.sln b/Ryujinx.sln index b89d5da0a..deddb97a0 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -47,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vic", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Apple", "src\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj", "{AC26EFF0-8593-4184-9A09-98E37EFFB32E}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}" @@ -569,6 +571,8 @@ Global {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 + {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index aa8937247..3578f689d 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -4850,6 +4850,31 @@ "zh_TW": null } }, + { + "ID": "SettingsTabSystemAudioBackendAudioToolbox", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Apple Audio (macOS only)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, { "ID": "SettingsTabSystemHacks", "Translations": { @@ -24776,4 +24801,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 62d81c6cc..2c659f6a0 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -86,6 +86,8 @@ namespace Ryujinx.Audio.Backends.Apple private static bool IsSupportedInternal() { + if (!OperatingSystem.IsMacOS()) return false; + try { var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); @@ -238,4 +240,4 @@ namespace Ryujinx.Audio.Backends.Apple return direction != Direction.Input; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 95826b4f4..08aefe4a5 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -215,7 +215,7 @@ namespace Ryujinx.Audio.Backends.Apple public override void QueueBuffer(AudioBuffer buffer) { _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); - _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.HostTag, GetSampleCount(buffer))); + _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer))); } public override void Start() @@ -253,7 +253,7 @@ namespace Ryujinx.Audio.Backends.Apple if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer)) return true; - return driverBuffer.DriverIdentifier != buffer.HostTag; + return driverBuffer.DriverIdentifier != buffer.DataPointer; } public override void PrepareToClose() { } diff --git a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj index b7e1b6d84..c27fdee5b 100644 --- a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj +++ b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 true diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 28aec175b..5da152501 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -77,12 +77,13 @@ - + + diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 2eba0d26b..4b1e9cdb5 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -6,6 +6,7 @@ using Avalonia.Threading; using DiscordRPC; using LibHac.Common; using LibHac.Ns; +using Ryujinx.Audio.Backends.Apple; using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL3; @@ -949,6 +950,9 @@ namespace Ryujinx.Ava.Systems AudioBackend.Dummy ]; + if (OperatingSystem.IsMacOS()) + availableBackends.Insert(0, AudioBackend.AudioToolbox); + AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value; if (preferredBackend is AudioBackend.SDL2) @@ -985,6 +989,9 @@ namespace Ryujinx.Ava.Systems deviceDriver = currentBackend switch { +#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run. + AudioBackend.AudioToolbox => InitializeAudioBackend(AudioBackend.AudioToolbox, nextBackend), +#pragma warning restore CA1416 AudioBackend.SDL3 => InitializeAudioBackend(AudioBackend.SDL3, nextBackend), AudioBackend.SoundIo => InitializeAudioBackend(AudioBackend.SoundIo, nextBackend), AudioBackend.OpenAl => InitializeAudioBackend(AudioBackend.OpenAl, nextBackend), diff --git a/src/Ryujinx/Systems/Configuration/AudioBackend.cs b/src/Ryujinx/Systems/Configuration/AudioBackend.cs index da75c9f7c..12d87151d 100644 --- a/src/Ryujinx/Systems/Configuration/AudioBackend.cs +++ b/src/Ryujinx/Systems/Configuration/AudioBackend.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Ava.Systems.Configuration OpenAl, SoundIo, SDL3, + AudioToolbox, SDL2 = SDL3 } } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index d5d9b8218..abb284960 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.Apple; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL3; using Ryujinx.Audio.Backends.SoundIo; @@ -277,6 +278,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenAlEnabled { get; set; } public bool IsSoundIoEnabled { get; set; } public bool IsSDL3Enabled { get; set; } + public bool IsAudioToolboxEnabled { get; set; } public bool IsCustomResolutionScaleActive => _resolutionScale == 4; public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; @@ -524,12 +526,14 @@ namespace Ryujinx.Ava.UI.ViewModels IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported; + IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported; await Dispatcher.UIThread.InvokeAsync(() => { OnPropertyChanged(nameof(IsOpenAlEnabled)); OnPropertyChanged(nameof(IsSoundIoEnabled)); OnPropertyChanged(nameof(IsSDL3Enabled)); + OnPropertyChanged(nameof(IsAudioToolboxEnabled)); }); } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml index 22dfc57ac..e9b4e7acc 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml @@ -46,6 +46,9 @@ + From 82074eb19121fb96c82454a2a63e8e3bedff5137 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:34:51 -0600 Subject: [PATCH 10/13] audio backend projects code cleanup --- .../AppleHardwareDeviceDriver.cs | 66 +++++++------------ .../AppleHardwareDeviceSession.cs | 2 +- .../Native/AudioToolbox.cs | 5 +- .../OpenALHardwareDeviceDriver.cs | 5 +- .../OpenALHardwareDeviceSession.cs | 5 +- .../SDL3HardwareDeviceDriver.cs | 4 +- .../SDL3HardwareDeviceSession.cs | 7 +- .../SoundIoHardwareDeviceDriver.cs | 4 +- .../SoundIoHardwareDeviceSession.cs | 4 +- 9 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 2c659f6a0..80e55b9e5 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Audio.Backends.Apple { [SupportedOSPlatform("macos")] [SupportedOSPlatform("ios")] - public class AppleHardwareDeviceDriver : IHardwareDeviceDriver + public sealed class AppleHardwareDeviceDriver : IHardwareDeviceDriver { private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _pauseEvent; @@ -38,9 +38,10 @@ namespace Ryujinx.Audio.Backends.Apple private bool TestSurroundSupport() { try - { - var format = GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); - + { + AudioStreamBasicDescription format = + GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); + int result = AudioQueueNewOutput( ref format, IntPtr.Zero, @@ -60,9 +61,9 @@ namespace Ryujinx.Audio.Backends.Apple }; int layoutResult = AudioQueueSetProperty( - testQueue, - kAudioQueueProperty_ChannelLayout, - ref layout, + testQueue, + kAudioQueueProperty_ChannelLayout, + ref layout, (uint)Marshal.SizeOf()); if (layoutResult == 0) @@ -70,7 +71,7 @@ namespace Ryujinx.Audio.Backends.Apple AudioQueueDispose(testQueue, true); return true; } - + AudioQueueDispose(testQueue, true); } @@ -90,7 +91,8 @@ namespace Ryujinx.Audio.Backends.Apple try { - var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); + AudioStreamBasicDescription format = + GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); int result = AudioQueueNewOutput( ref format, IntPtr.Zero, @@ -115,16 +117,13 @@ namespace Ryujinx.Audio.Backends.Apple } public ManualResetEvent GetUpdateRequiredEvent() - { - return _updateRequiredEvent; - } + => _updateRequiredEvent; public ManualResetEvent GetPauseEvent() - { - return _pauseEvent; - } + => _pauseEvent; - public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, + SampleFormat sampleFormat, uint sampleRate, uint channelCount) { if (channelCount == 0) { @@ -149,11 +148,10 @@ namespace Ryujinx.Audio.Backends.Apple } internal bool Unregister(AppleHardwareDeviceSession session) - { - return _sessions.TryRemove(session, out _); - } + => _sessions.TryRemove(session, out _); - internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, uint channelCount) + internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, + uint channelCount) { uint formatFlags; uint bitsPerChannel; @@ -202,7 +200,7 @@ namespace Ryujinx.Audio.Backends.Apple Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { @@ -215,29 +213,15 @@ namespace Ryujinx.Audio.Backends.Apple } } - public bool SupportsSampleRate(uint sampleRate) - { - return true; - } + public bool SupportsDirection(Direction direction) + => direction != Direction.Input; + + public bool SupportsSampleRate(uint sampleRate) => true; public bool SupportsSampleFormat(SampleFormat sampleFormat) - { - return sampleFormat != SampleFormat.PcmInt24; - } + => sampleFormat != SampleFormat.PcmInt24; public bool SupportsChannelCount(uint channelCount) - { - if (channelCount == 6) - { - return _supportSurroundConfiguration; - } - - return true; - } - - public bool SupportsDirection(Direction direction) - { - return direction != Direction.Input; - } + => channelCount != 6 || _supportSurroundConfiguration; } } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 08aefe4a5..c9443dcd3 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -67,7 +67,7 @@ namespace Ryujinx.Audio.Backends.Apple { lock (_lock) { - var format = AppleHardwareDeviceDriver.GetAudioFormat( + AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat( RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount); diff --git a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs index 9a6e8e189..ea2a7867a 100644 --- a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs +++ b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs @@ -1,6 +1,5 @@ -using Ryujinx.Common.Memory; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming namespace Ryujinx.Audio.Backends.Apple.Native { @@ -100,4 +99,4 @@ namespace Ryujinx.Audio.Backends.Apple.Native internal const uint kAudioQueueParam_Volume = 1; } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs index 8be6197f6..911b131ed 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs @@ -10,7 +10,8 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.OpenAL { - public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver + // ReSharper disable once InconsistentNaming + public sealed class OpenALHardwareDeviceDriver : IHardwareDeviceDriver { private readonly ALDevice _device; private readonly ALContext _context; @@ -148,7 +149,7 @@ namespace Ryujinx.Audio.Backends.OpenAL Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs index 7292450a6..61fb4a369 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs @@ -9,7 +9,8 @@ using System.Threading; namespace Ryujinx.Audio.Backends.OpenAL { - class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase + // ReSharper disable once InconsistentNaming + sealed class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly OpenALHardwareDeviceDriver _driver; private readonly int _sourceId; @@ -190,7 +191,7 @@ namespace Ryujinx.Audio.Backends.OpenAL } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs index bdc9f02f4..598de8835 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Backends.SDL3 using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]; - public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver + public sealed class SDL3HardwareDeviceDriver : IHardwareDeviceDriver { private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _pauseEvent; @@ -162,7 +162,7 @@ namespace Ryujinx.Audio.Backends.SDL3 Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs index 377d86d2b..ca7b131dd 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs @@ -12,10 +12,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.Audio.Backends.SDL3 { - - - - unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase + sealed unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly SDL3HardwareDeviceDriver _driver; private readonly ConcurrentQueue _queuedBuffers; @@ -226,7 +223,7 @@ namespace Ryujinx.Audio.Backends.SDL3 return driverBuffer.DriverIdentifier != buffer.DataPointer; } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs index e3e5d2913..1aed0744c 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs @@ -10,7 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.SoundIo { - public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver + public sealed class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver { private readonly SoundIoContext _audioContext; private readonly SoundIoDeviceContext _audioDevice; @@ -227,7 +227,7 @@ namespace Ryujinx.Audio.Backends.SoundIo } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs index 1540cd0e3..39ceac08a 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs @@ -11,7 +11,7 @@ using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; namespace Ryujinx.Audio.Backends.SoundIo { - class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase + sealed class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly SoundIoHardwareDeviceDriver _driver; private readonly ConcurrentQueue _queuedBuffers; @@ -428,7 +428,7 @@ namespace Ryujinx.Audio.Backends.SoundIo } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { From fef93a453ac6a2032155a80feaf77d8f5db9cc5c Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:41:46 -0600 Subject: [PATCH 11/13] [ci skip] replace all usages of IntPtr with nint --- src/ARMeilleure/Common/EntryTable.cs | 2 +- .../AppleHardwareDeviceDriver.cs | 20 +++++------ .../AppleHardwareDeviceSession.cs | 34 +++++++++---------- .../Native/SoundIoOutStreamContext.cs | 2 +- src/Ryujinx.Cpu/AddressTable.cs | 32 ++++++++--------- .../Image/TextureGroup.cs | 2 +- .../MoltenVK/MVKInitialization.cs | 2 +- .../VulkanInitialization.cs | 4 +-- .../Sockets/Bsd/Impl/ManagedSocket.cs | 2 +- .../WindowsShared/PlaceholderManager.cs | 2 +- src/Ryujinx.Tests/Memory/PartialUnmaps.cs | 2 +- 11 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/ARMeilleure/Common/EntryTable.cs b/src/ARMeilleure/Common/EntryTable.cs index 7b8c1e134..1c154570a 100644 --- a/src/ARMeilleure/Common/EntryTable.cs +++ b/src/ARMeilleure/Common/EntryTable.cs @@ -168,7 +168,7 @@ namespace ARMeilleure.Common { _allocated.Dispose(); - foreach (IntPtr page in _pages.Values) + foreach (nint page in _pages.Values) { NativeAllocator.Instance.Free((void*)page); } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 80e55b9e5..f20da5557 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -44,12 +44,12 @@ namespace Ryujinx.Audio.Backends.Apple int result = AudioQueueNewOutput( ref format, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero, 0, - out IntPtr testQueue); + out nint testQueue); if (result == 0) { @@ -95,12 +95,12 @@ namespace Ryujinx.Audio.Backends.Apple GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); int result = AudioQueueNewOutput( ref format, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero, 0, - out IntPtr testQueue); + out nint testQueue); if (result == 0) { diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index c9443dcd3..05f9e2a3f 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -27,8 +27,8 @@ namespace Ryujinx.Audio.Backends.Apple private readonly AudioQueueOutputCallback _callbackDelegate; private readonly GCHandle _gcHandle; - private IntPtr _audioQueue; - private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers]; + private nint _audioQueue; + private readonly nint[] _audioQueueBuffers = new nint[NumBuffers]; private readonly int[] _bufferBytesFilled = new int[NumBuffers]; private readonly int _bytesPerFrame; @@ -41,9 +41,9 @@ namespace Ryujinx.Audio.Backends.Apple [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void AudioQueueOutputCallback( - IntPtr userData, - IntPtr audioQueue, - IntPtr buffer); + nint userData, + nint audioQueue, + nint buffer); public AppleHardwareDeviceSession( AppleHardwareDeviceDriver driver, @@ -72,15 +72,15 @@ namespace Ryujinx.Audio.Backends.Apple RequestedSampleRate, RequestedChannelCount); - IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); - IntPtr userData = GCHandle.ToIntPtr(_gcHandle); + nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); + nint userData = GCHandle.ToIntPtr(_gcHandle); int result = AudioQueueNewOutput( ref format, callbackPtr, userData, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, 0, out _audioQueue); @@ -102,7 +102,7 @@ namespace Ryujinx.Audio.Backends.Apple } } - private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex) + private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex) { AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr; @@ -126,12 +126,12 @@ namespace Ryujinx.Audio.Backends.Apple buffer->AudioDataByteSize = (uint)capacityBytes; _bufferBytesFilled[bufferIndex] = bytesToRead; - AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero); + AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero); } - private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr) + private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr) { - if (!_started || bufferPtr == IntPtr.Zero) + if (!_started || bufferPtr == nint.Zero) return; int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr); @@ -176,7 +176,7 @@ namespace Ryujinx.Audio.Backends.Apple } } - private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize) + private unsafe void ApplyVolume(nint dataPtr, int byteSize) { float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f); if (volume >= 0.999f) @@ -226,7 +226,7 @@ namespace Ryujinx.Audio.Backends.Apple return; _started = true; - AudioQueueStart(_audioQueue, IntPtr.Zero); + AudioQueueStart(_audioQueue, nint.Zero); } } @@ -265,11 +265,11 @@ namespace Ryujinx.Audio.Backends.Apple { Stop(); - if (_audioQueue != IntPtr.Zero) + if (_audioQueue != nint.Zero) { AudioQueueStop(_audioQueue, true); AudioQueueDispose(_audioQueue, true); - _audioQueue = IntPtr.Zero; + _audioQueue = nint.Zero; } if (_gcHandle.IsAllocated) diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs index 072e49d8c..56bd65e6d 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs @@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native unsafe { int* frameCountPtr = &nativeFrameCount; - IntPtr* arenasPtr = &arenas; + nint* arenasPtr = &arenas; CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr)); frameCount = *frameCountPtr; diff --git a/src/Ryujinx.Cpu/AddressTable.cs b/src/Ryujinx.Cpu/AddressTable.cs index 91a65e899..19e405491 100644 --- a/src/Ryujinx.Cpu/AddressTable.cs +++ b/src/Ryujinx.Cpu/AddressTable.cs @@ -30,9 +30,9 @@ namespace ARMeilleure.Common /// /// Base address for the page. /// - public readonly IntPtr Address; + public readonly nint Address; - public AddressTablePage(bool isSparse, IntPtr address) + public AddressTablePage(bool isSparse, nint address) { IsSparse = isSparse; Address = address; @@ -47,20 +47,20 @@ namespace ARMeilleure.Common public readonly SparseMemoryBlock Block; private readonly TrackingEventDelegate _trackingEvent; - public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) + public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) { SparseMemoryBlock block = new(size, pageInit, null); _trackingEvent = (address, size, write) => { ulong pointer = (ulong)block.Block.Pointer + address; - ensureMapped((IntPtr)pointer); + ensureMapped((nint)pointer); return pointer; }; bool added = NativeSignalHandler.AddTrackedRegion( (nuint)block.Block.Pointer, - (nuint)(block.Block.Pointer + (IntPtr)block.Block.Size), + (nuint)(block.Block.Pointer + (nint)block.Block.Size), Marshal.GetFunctionPointerForDelegate(_trackingEvent)); if (!added) @@ -116,7 +116,7 @@ namespace ARMeilleure.Common } /// - public IntPtr Base + public nint Base { get { @@ -124,7 +124,7 @@ namespace ARMeilleure.Common lock (_pages) { - return (IntPtr)GetRootPage(); + return (nint)GetRootPage(); } } } @@ -240,7 +240,7 @@ namespace ARMeilleure.Common long index = Levels[^1].GetValue(address); - EnsureMapped((IntPtr)(page + index)); + EnsureMapped((nint)(page + index)); return ref page[index]; } @@ -284,7 +284,7 @@ namespace ARMeilleure.Common /// Ensure the given pointer is mapped in any overlapping sparse reservations. /// /// Pointer to be mapped - private void EnsureMapped(IntPtr ptr) + private void EnsureMapped(nint ptr) { if (Sparse) { @@ -299,7 +299,7 @@ namespace ARMeilleure.Common { SparseMemoryBlock sparse = reserved.Block; - if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size) + if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size) { sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer)); @@ -319,15 +319,15 @@ namespace ARMeilleure.Common /// /// Level to get the fill value for /// The fill value - private IntPtr GetFillValue(int level) + private nint GetFillValue(int level) { if (_fillBottomLevel != null && level == Levels.Length - 2) { - return (IntPtr)_fillBottomLevelPtr; + return (nint)_fillBottomLevelPtr; } else { - return IntPtr.Zero; + return nint.Zero; } } @@ -379,7 +379,7 @@ namespace ARMeilleure.Common /// Fill value /// if leaf; otherwise /// Allocated block - private IntPtr Allocate(int length, T fill, bool leaf) where T : unmanaged + private nint Allocate(int length, T fill, bool leaf) where T : unmanaged { int size = sizeof(T) * length; @@ -405,7 +405,7 @@ namespace ARMeilleure.Common } } - page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset); + page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset); _sparseReservedOffset += (ulong)size; @@ -413,7 +413,7 @@ namespace ARMeilleure.Common } else { - IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size); + nint address = (nint)NativeAllocator.Instance.Allocate((uint)size); page = new AddressTablePage(false, address); Span span = new((void*)page.Address, length); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index e7a1afe1a..bfb4b839e 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel; - IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; + nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size)) { diff --git a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs index c1c59939e..4f5dbf6b9 100644 --- a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK public static void Initialize() { - IntPtr configSize = (nint)Marshal.SizeOf(); + nint configSize = (nint)Marshal.SizeOf(); vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index c4dbf41f6..02c4e6873 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray(); } - IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); + nint appName = Marshal.StringToHGlobalAnsi(AppName); ApplicationInfo applicationInfo = new() { @@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api) { - IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); + nint appName = Marshal.StringToHGlobalAnsi(AppName); ApplicationInfo applicationInfo = new() { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index d99488d85..0624fcf91 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public nint Handle => IntPtr.Zero; + public nint Handle => nint.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; diff --git a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs index 344a48be6..4d85fa400 100644 --- a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs +++ b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs @@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared { SplitForMap((ulong)location, (ulong)size, srcOffset); - IntPtr ptr = WindowsApi.MapViewOfFile3( + nint ptr = WindowsApi.MapViewOfFile3( sharedMemory, WindowsApi.CurrentProcessHandle, location, diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs index 73a7f7dfc..313a85a41 100644 --- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs +++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs @@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory // Create some info to be used for managing the native writing loop. int stateSize = Unsafe.SizeOf(); - IntPtr statePtr = Marshal.AllocHGlobal(stateSize); + nint statePtr = Marshal.AllocHGlobal(stateSize); Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize); ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef((void*)statePtr); From 5ed94c365bd5792e4234d4533d08e499bb11b1a4 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:52:45 -0600 Subject: [PATCH 12/13] add a stack trace for the catch branch of AppleHardwareDeviceDriver.IsSupported --- src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs | 3 ++- src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index f20da5557..2e3b97517 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -110,8 +110,9 @@ namespace Ryujinx.Audio.Backends.Apple return false; } - catch + catch (Exception e) { + Logger.Error?.Print(LogClass.Audio, $"Failed to check if AudioToolbox is supported: {e.Message}\n{e.StackTrace}"); return false; } } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 05f9e2a3f..1606e9954 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -1,15 +1,12 @@ using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Common; -using Ryujinx.Common.Logging; using Ryujinx.Memory; using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; using System.Runtime.Versioning; -using Ryujinx.Audio.Backends.Apple.Native; using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; -using static Ryujinx.Audio.Backends.Apple.AppleHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.Apple { From 3067db1884836fd8128020111e680511ce2c1746 Mon Sep 17 00:00:00 2001 From: Shyanne Date: Tue, 25 Nov 2025 21:31:34 -0500 Subject: [PATCH 13/13] HLE: Implement IHidServer IsSixAxisSensorAtRest Fixes the actually insane amount of log spam in games that check for this, such as Luigi's Mansion 3. Values in HidDevices.NpadDevices.isAtRest may need to be tuned to a better range for resting detection. I originally set them to 0, and my controller rests at definitely NOT 0. Will need to be revisited when implementing functionality for the global SixAxisActive bool, IHidServer.StartSixAxisTracking, and IHidServer.StopSixAxisTracking. --- .../Services/Hid/HidDevices/NpadDevices.cs | 18 +++++++ .../HOS/Services/Hid/IHidServer.cs | 47 ++++++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs index b9235b033..8fe9d3e62 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs @@ -56,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid _activeCount = 0; JoyHold = NpadJoyHoldType.Vertical; + SixAxisActive = false; } internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player) @@ -580,6 +581,23 @@ namespace Ryujinx.HLE.HOS.Services.Hid return needUpdateRight; } + + public bool isAtRest(int playerNumber) + { + + ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState; + ref SixAxisSensorState storage = ref GetSixAxisSensorLifo(ref currentNpad, false).GetCurrentEntryRef(); + + float acceleration = Math.Abs(storage.Acceleration.X) + + Math.Abs(storage.Acceleration.Y) + + Math.Abs(storage.Acceleration.Z); + + float angularVelocity = Math.Abs(storage.AngularVelocity.X) + + Math.Abs(storage.AngularVelocity.Y) + + Math.Abs(storage.AngularVelocity.Z); + + return ((acceleration <= 1.5F) && (angularVelocity <= 1.1F)); + } private void UpdateDisconnectedInputSixAxis(PlayerIndex index) { diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index 28db75663..a92e1656f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -602,19 +602,52 @@ namespace Ryujinx.HLE.HOS.Services.Hid } [CommandCmif(82)] - // IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAsRest + // IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAtRest public ResultCode IsSixAxisSensorAtRest(ServiceCtx context) { int sixAxisSensorHandle = context.RequestData.ReadInt32(); + + // 4 byte struct w/ 4-byte alignment + // 0x0 0x4 TypeValue + // 0x0 0x1 NpadStyleIndex + // 0x1 0x1 PlayerNumber + // 0x2 0x1 DeviceIdx + + // uint typeValue = (uint) sixAxisSensorHandle; + // uint npadStyleIndex = (uint) sixAxisSensorHandle & 0xff; + int playerNumber = (sixAxisSensorHandle << 8) & 0xff; + // uint deviceIdx= ((uint) sixAxisSensorHandle << 16) & 0xff; + // uint unknown = ((uint) sixAxisSensorHandle << 24) & 0xff; + + // 32bit sign extension padding + // if = 0, + offset, else - offset + // npadStyleIndex = ((npadStyleIndex & 0x8000) == 0) ? npadStyleIndex | 0xFFFF0000 : npadStyleIndex & 0xFFFF0000; + // playerNumber = ((playerNumber & 0x8000) == 0) ? playerNumber | 0xFFFF0000 : playerNumber & 0xFFFF0000; + // deviceIdx = ((deviceIdx & 0x8000) == 0) ? deviceIdx | 0xFFFF0000 : deviceIdx & 0xFFFF0000; + // unknown = ((unknown & 0x8000) == 0) ? unknown | 0xFFFF0000 : unknown & 0xFFFF0000; + context.RequestData.BaseStream.Position += 4; // Padding long appletResourceUserId = context.RequestData.ReadInt64(); - bool isAtRest = true; - + bool isAtRest; + + // TODO: link to context.Device.Hid.Npads.SixAxisActive when properly implemented + // We currently do not support stopping or starting SixAxisTracking. + // It is just always tracking, the bool is unused. + // See Ryujinx.HLE.HOS.Services.Hid.NpadDevices + + // common cases: if + // controller is keyboard (keyboards don't have gyroscopes, silly goose!) + // controller has no gyroscope + // SixAxisActive == false + // SixAxisTracking == false + // then isAtRest = true + // else check gyroscopic activity + + // check gyroscopic activity + isAtRest = context.Device.Hid.Npads.isAtRest(playerNumber); + context.ResponseData.Write(isAtRest); - - Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, isAtRest }); - return ResultCode.Success; } @@ -629,7 +662,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid context.ResponseData.Write(_isFirmwareUpdateAvailableForSixAxisSensor); Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _isFirmwareUpdateAvailableForSixAxisSensor }); - + return ResultCode.Success; }