Author SHA1 Message Date
_Neo_ e03fc738cd Refactor GetIcdPath 2026-09-13 21:38:20 -05:00
KeatonTheBot b6a9311394 Move JSON files to root directory 2026-09-13 21:38:20 -05:00
KeatonTheBot 3abe9a0285 Add icd.d JSON files for local, non-bundled builds 2026-09-13 21:38:20 -05:00
KeatonTheBot 6dc85e7f40 Add KosmicKrisp translation layer for macOS 26+
- Mesa 26.0 introduced KosmicKrisp, a Vulkan-on-Metal translation layer and the eventual successor to MoltenVK
2026-09-13 21:38:20 -05:00
GreemDev a0ad6f2246 misc: chore: add direct error code tuple to DisplayErrorAppletDialog
for use when i find the list of error codes -> causes
2026-09-13 21:24:52 -05:00
KeatonTheBot a412999eeb Fix Skip User Profiles
This and `Add the player select applet` fixes commit c9c78841b9.
2026-09-13 20:49:31 -05:00
JacobandGreemDev 87605ebce2 Add the player select applet
This introduces the somewhat completed version of the Player Select
Applet, allowing users to select either a user or a guest from the UI.
Note: Selecting the guest more then once currently does not work.

closes https://github.com/Ryubing/Ryujinx/issues/532

- misc: chore: optimize UserSelectorDialog closed handler

- misc: chore: Rename UserSelectorDialog to ProfileSelectorDialog

Co-authored-by: GreemDev <greemdev@ryujinx.app>
2026-09-13 20:48:20 -05:00
GreemDev b67e8df788 RenderDoc API support 2026-09-13 15:08:11 -05:00
63 changed files with 2002 additions and 261 deletions
+2 -1
View File
@@ -36,8 +36,10 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Version="5.0.3-build14" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.KosmicKrisp" Version="26.2.1-ryu.1" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.3-ryujinx.1" /> <PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.3-ryujinx.1" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" /> <PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
<PackageVersion Include="Ryujinx.SPB" Version="0.0.5-ryujinx.4" />
<PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.707.0" /> <PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.707.0" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" /> <PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
@@ -47,7 +49,6 @@
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" /> <PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.4" /> <PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.12" /> <PackageVersion Include="System.IO.Hashing" Version="10.0.12" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.0" /> <PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.0" />
</ItemGroup> </ItemGroup>
+1
View File
@@ -21,6 +21,7 @@
<Project Path="src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj" /> <Project Path="src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj" />
<Project Path="src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj" /> <Project Path="src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj" />
<Project Path="src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" /> <Project Path="src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
<Project Path="src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj" />
<Project Path="src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj" /> <Project Path="src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj" />
<Project Path="src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj" /> <Project Path="src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj" />
<Project Path="src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj" /> <Project Path="src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj" />
@@ -48,7 +48,7 @@ def get_new_name(
return Path(os.path.join(output_directory, input_component)) return Path(os.path.join(output_directory, input_component))
def get_archs(dylib_path: Path) -> list[str]: def get_archs(dylib_path: Path) -> list[str]:
res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8") res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode("utf-8")
if res.startswith("Non-fat file"): if res.startswith("Non-fat file"):
return [res.split(":")[-1].strip()] return [res.split(":")[-1].strip()]
else: else:
+5 -1
View File
@@ -18,14 +18,18 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx" cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx" chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
# List all dylibs in PUBLISH_DIRECTORY
ls -la "$PUBLISH_DIRECTORY"/*.dylib
# Then all libraries # Then all libraries
cp "$PUBLISH_DIRECTORY"/*.dylib "$APP_BUNDLE_DIRECTORY/Contents/Frameworks" find "$PUBLISH_DIRECTORY" -name "*.dylib" -exec cp {} "$APP_BUNDLE_DIRECTORY/Contents/Frameworks" \;
# Then resources # Then resources
cp Info.plist "$APP_BUNDLE_DIRECTORY/Contents" cp Info.plist "$APP_BUNDLE_DIRECTORY/Contents"
cp Ryujinx.icns "$APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns" cp Ryujinx.icns "$APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns"
cp updater.sh "$APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh" cp updater.sh "$APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh"
cp Assets.car "$APP_BUNDLE_DIRECTORY/Contents/Resources/Assets.car" cp Assets.car "$APP_BUNDLE_DIRECTORY/Contents/Resources/Assets.car"
cp -r vulkan "$APP_BUNDLE_DIRECTORY/Contents/Resources"
cp -r "$PUBLISH_DIRECTORY/THIRDPARTY.md" "$APP_BUNDLE_DIRECTORY/Contents/Resources" cp -r "$PUBLISH_DIRECTORY/THIRDPARTY.md" "$APP_BUNDLE_DIRECTORY/Contents/Resources"
echo -n "APPL????" > "$APP_BUNDLE_DIRECTORY/Contents/PkgInfo" echo -n "APPL????" > "$APP_BUNDLE_DIRECTORY/Contents/PkgInfo"
+4 -1
View File
@@ -47,7 +47,7 @@ EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
rm -rf "$TEMP_DIRECTORY" rm -rf "$TEMP_DIRECTORY"
mkdir -p "$TEMP_DIRECTORY" mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS) DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" -p:UseSharedCompilation=false --self-contained true $EXTRA_ARGS)
dotnet restore dotnet restore
dotnet build -c "$CONFIGURATION" src/Ryujinx dotnet build -c "$CONFIGURATION" src/Ryujinx
@@ -76,6 +76,9 @@ rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
# Make it libraries universal # Make it libraries universal
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib" python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib"
# Copy arm64-only libraries to UNIVERSAL_APP_BUNDLE
cp "$ARM64_APP_BUNDLE/Contents/Frameworks/libvulkan_kosmickrisp.dylib" "$UNIVERSAL_APP_BUNDLE/Contents/Frameworks/libvulkan_kosmickrisp.dylib"
if ! [ -x "$(command -v lipo)" ]; if ! [ -x "$(command -v lipo)" ];
then then
if ! [ -x "$(command -v llvm-lipo-17)" ]; if ! [ -x "$(command -v llvm-lipo-17)" ];
@@ -0,0 +1,8 @@
{
"file_format_version" : "1.0.0",
"ICD": {
"library_path": "./libMoltenVK.dylib",
"api_version" : "1.4.0",
"is_portability_driver" : true
}
}
@@ -0,0 +1,8 @@
{
"file_format_version" : "1.0.0",
"ICD": {
"library_path": "../../../Frameworks/libMoltenVK.dylib",
"api_version" : "1.4.0",
"is_portability_driver" : true
}
}
@@ -0,0 +1,7 @@
{
"ICD": {
"api_version": "1.3.0",
"library_path": "./libvulkan_kosmickrisp.dylib"
},
"file_format_version": "1.0.1"
}
@@ -0,0 +1,7 @@
{
"ICD": {
"api_version": "1.3.0",
"library_path": "../../../Frameworks/libvulkan_kosmickrisp.dylib"
},
"file_format_version": "1.0.1"
}
+4
View File
@@ -2,6 +2,7 @@
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="local" value="nuget" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- Only needed when using pre-release versions of Ryujinx.LibHac. --> <!-- Only needed when using pre-release versions of Ryujinx.LibHac. -->
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/packages/projects/nuget/index.json" /> <add key="LibHacAlpha" value="https://git.ryujinx.app/api/packages/projects/nuget/index.json" />
@@ -10,6 +11,9 @@
<packageSourceMapping> <packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element --> <!-- key value for <packageSource> should match key values from <packageSources> element -->
<!-- These are defined and .NET still yells about multiple package sources with no mappings. Not sure what to do, this is in the docs lol --> <!-- These are defined and .NET still yells about multiple package sources with no mappings. Not sure what to do, this is in the docs lol -->
<packageSource key="local">
<package pattern="Ryujinx.Graphics.Vulkan.KosmicKrisp" />
</packageSource>
<packageSource key="nuget.org"> <packageSource key="nuget.org">
<package pattern="*" /> <package pattern="*" />
</packageSource> </packageSource>
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<TranslationLayer>))]
public enum TranslationLayer
{
MoltenVK,
KosmicKrisp,
}
}
@@ -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);
}
}
}
@@ -0,0 +1,100 @@
// ReSharper disable UnusedMember.Global
namespace Ryujinx.Graphics.RenderDocApi
{
public enum CaptureOption
{
/// <summary>
/// specifies whether the application is allowed to enable vsync. Default is on.
/// </summary>
AllowVsync = 0,
/// <summary>
/// specifies whether the application is allowed to enter exclusive fullscreen. Default is on.
/// </summary>
AllowFullscreen = 1,
/// <summary>
/// specifies whether (where possible) API-specific debugging is enabled. Default is off.
/// </summary>
ApiValidation = 2,
/// <summary>
/// specifies whether each API call should save a callstack. Default is off.
/// </summary>
CaptureCallstacks = 3,
/// <summary>
/// specifies whether, if <see cref="CaptureCallstacks"/> is enabled, callstacks are only saved on actions. Default is off.
/// </summary>
CaptureCallstacksOnlyDraws = 4,
/// <summary>
/// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach. <br/>
/// This will only apply to child processes since the delay happens at process startup. Default is 0.
/// </summary>
DelayForDebugger = 5,
/// <summary>
/// specifies whether any mapped memory updates should be bounds-checked for overruns,
/// and uninitialised buffers are initialized to <code>0xDDDDDDDD</code> to catch use of uninitialised data.
/// Only supported on D3D11 and OpenGL. Default is off.
/// </summary>
/// <remarks>
/// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
/// not do the same kind of interception &amp; checking, and undefined contents are really undefined.
/// </remarks>
VerifyBufferAccess = 6,
/// <summary>
/// Hooks any system API calls that create child processes, and injects
/// RenderDoc into them recursively with the same options.
/// </summary>
HookIntoChildren = 7,
/// <summary>
/// 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.
/// </summary>
RefAllSources = 8,
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// **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.
/// </remarks>
SaveAllInitials = 9,
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
CaptureAllCmdLists = 10,
/// <summary>
/// Mute API debugging output when the <see cref="ApiValidation"/> option is enabled.
/// </summary>
DebugOutputMute = 11,
/// <summary>
/// Allow vendor extensions to be used even when they may be
/// incompatible with RenderDoc and cause corrupted replays or crashes.
/// </summary>
AllowUnsupportedVendorExtensions = 12,
/// <summary>
/// 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.<br/>
/// 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.
/// <br/><br/>
/// Not all memory allocations may be deferred like this so it is not a guarantee of a memory
/// limit.
/// <br/><br/>
/// Units are in MBs, suggested values would range from 200MB to 1000MB.
/// </summary>
SoftMemoryLimit = 13,
}
}
@@ -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,
}
}
@@ -0,0 +1,39 @@
// ReSharper disable UnusedMember.Global
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
[Flags]
public enum OverlayBits
{
/// <summary>
/// This single bit controls whether the overlay is enabled or disabled globally
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// Show the average framerate over several seconds as well as min/max
/// </summary>
FrameRate = 1 << 1,
/// <summary>
/// Show the current frame number
/// </summary>
FrameNumber = 1 << 2,
/// <summary>
/// Show a list of recent captures, and how many captures have been made
/// </summary>
CaptureList = 1 << 3,
/// <summary>
/// Default values for the overlay mask
/// </summary>
Default = Enabled | FrameRate | FrameNumber | CaptureList,
/// <summary>
/// Enable all bits
/// </summary>
All = ~0,
/// <summary>
/// Disable all bits
/// </summary>
None = 0
}
}
@@ -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.
@@ -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
{
/// <summary>
/// True if the API is available.
/// </summary>
public static bool IsAvailable => Api != null;
/// <summary>
/// Set the minimum version of the API you require.
/// </summary>
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0;
/// <summary>
/// Set to true to assert versions.
/// </summary>
public static bool AssertVersionEnabled { get; set; } = true;
/// <summary>
/// Version of the API available.
/// </summary>
[MemberNotNullWhen(true, nameof(IsAvailable))]
public static Version? Version
{
get
{
if (!IsAvailable)
return null;
int major, minor, build;
Api->GetApiVersion(&major, &minor, &build);
return new Version(major, minor, build);
}
}
/// <summary>
/// The current mask which determines what sections of the overlay render on each window.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static OverlayBits OverlayBits
{
get => Api->GetOverlayBits();
set
{
Api->MaskOverlayBits(~value, value);
}
}
/// <summary>
/// The template for new captures.<br/>
/// 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'.<br/>
/// Relative paths will be saved relative to the process’s current working directory.<br/>
/// </summary>
/// <remarks>The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename.</remarks>
[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);
}
}
}
/// <summary>
/// The amount of frame captures that have been made.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static int CaptureCount => Api->GetNumCaptures();
/// <summary>
/// Checks if the RenderDoc UI is currently connected to this process.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0;
/// <summary>
/// Checks if the current frame is capturing.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0;
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="integer">the unsigned integer value to set for the option.</param>
/// <remarks>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.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, uint integer)
{
return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0;
}
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="boolean">the value to set for the option, converted to a 0 or 1 before setting.</param>
/// <remarks>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.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, bool boolean)
=> SetCaptureOption(option, boolean ? 1 : 0);
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="single">the floating point value to set for the option.</param>
/// <remarks>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.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, float single)
{
return Api is not null && Api->SetCaptureOptionF32(option, single) != 0;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="integer">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, <see cref="int.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out uint integer)
{
integer = Api->GetCaptureOptionU32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="single">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, -<see cref="float.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out float single)
{
single = Api->GetCaptureOptionF32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>,
/// converted to a boolean.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, converted to bool, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns null.
/// </returns>
[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;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns <see cref="int.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option);
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns -<see cref="float.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option);
/// <summary>
/// Changes the key bindings in-application for changing the focussed window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetFocusToggleKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetFocusToggleKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Changes the key bindings in-application for triggering a capture on the current window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetCaptureKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetCaptureKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Attempts to remove RenderDoc and its hooks from the target process.<br/>
/// It must be called as early as possible in the process, and will have undefined results
/// if any graphics API functions have been called.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void RemoveHooks()
{
if (Api is null) return;
Api->RemoveHooks();
}
/// <summary>
/// Remove RenderDoc’s crash handler from the target process.<br/>
/// If you have your own crash handler that you want to handle any exceptions,
/// RenderDoc’s handler could interfere; so it can be disabled.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void UnloadCrashHandler()
{
if (Api is null) return;
Api->UnloadCrashHandler();
}
/// <summary>
/// Trigger a capture as if the user had pressed one of the capture hotkeys.<br/>
/// The capture will be taken from the next frame presented to whichever window is considered current.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void TriggerCapture()
{
if (Api is null) return;
Api->TriggerCapture();
}
/// <summary>
/// Gets the details of all frame capture in the current session.
/// This simply calls <see cref="GetCapture"/> for each index available as specified by <see cref="CaptureCount"/>.
/// </summary>
/// <returns>An immutable array of structs representing RenderDoc Captures.</returns>
public static ImmutableArray<Capture> GetCaptures()
{
if (Api is null) return [];
int captureCount = CaptureCount;
if (captureCount is 0) return [];
ImmutableArray<Capture>.Builder captures = ImmutableArray.CreateBuilder<Capture>(captureCount);
for (int captureIndex = 0; captureIndex < captureCount; captureIndex++)
{
if (GetCapture(captureIndex) is { } capture)
captures.Add(capture);
}
return captures.DrainToImmutable();
}
/// <summary>
/// Gets the details of a particular frame capture, as specified by an index from 0 to <see cref="CaptureCount"/> - 1.
/// </summary>
/// <param name="index">specifies which capture to return the details of. Must be less than the value returned by <see cref="CaptureCount"/>.</param>
/// <returns>A struct representing a RenderDoc Capture.</returns>
[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<byte> bytes = stackalloc byte[length + 1];
long timestamp;
fixed (byte* ptr = bytes)
Api->GetCapture(index, ptr, &length, &timestamp);
string fileName = Encoding.UTF8.GetString(bytes[length..]);
return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime);
}
/// <summary>
/// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it.
/// </summary>
/// <param name="connectTargetControl">if the UI should immediately connect to the application.</param>
/// <param name="commandLine">string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified.</param>
/// <returns>true if the UI was successfully launched; false otherwise.</returns>
[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;
}
}
/// <summary>
/// Explicitly sets which window is considered active.<br/>
/// The active window is the one that will be captured when the keybind to trigger a capture is pressed.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. Must be valid.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. Must be valid.</param>
[RenderDocApiVersion(1, 0)]
public static void SetActiveWindow(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->SetActiveWindow((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately begin a capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
[RenderDocApiVersion(1, 0)]
public static void StartFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->StartFrameCapture((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately end an active capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture succeeded; false otherwise.</returns>
[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;
}
/// <summary>
/// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.<br/>
/// The captures will be taken from the next frames presented to whichever window is considered current.<br/>
/// Each capture will be taken independently and saved to a separate file, with no reference to the other frames.
/// </summary>
/// <param name="numFrames">the number of frames to capture.</param>
/// <remarks>Requires RenderDoc API version 1.1</remarks>
[RenderDocApiVersion(1, 1)]
public static void TriggerMultiFrameCapture(uint numFrames)
{
if (Api is null) return;
AssertAtLeast(1, 1);
Api->TriggerMultiFrameCapture(numFrames);
}
/// <summary>
/// Adds an arbitrary comments field to the most recent capture,
/// which will then be displayed in the UI to anyone opening the capture.
/// <br/><br/>
/// This is equivalent to calling <see cref="SetCaptureFileComments"/> with a null first (fileName) parameter.
/// </summary>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
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);
}
}
/// <summary>
/// Adds an arbitrary comments field to an existing capture on disk,
/// which will then be displayed in the UI to anyone opening the capture.
/// </summary>
/// <param name="fileName">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.</param>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
[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);
}
}
}
}
/// <summary>
/// Similar to <see cref="EndFrameCapture"/>, but the capture contents will be discarded immediately, and not processed and written to disk.<br/>
/// This will be more efficient than <see cref="EndFrameCapture"/> if the frame capture is not needed.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture was discarded; false if there was an error or no capture was in progress.</returns>
/// <remarks>Requires RenderDoc API version 1.4</remarks>
[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;
}
/// <summary>
/// Requests that the currently connected replay UI raise its window to the top.<br/>
/// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.<br/>
/// This can be used in conjunction with <see cref="IsTargetControlConnected"/> and <see cref="LaunchReplayUI"/>,<br/> to intelligently handle showing the UI after making a capture.<br/><br/>
/// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.<br/>
/// On some systems it may only be highlighted or otherwise indicated to the user.
/// </summary>
/// <returns>true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred.</returns>
/// <remarks>Requires RenderDoc API version 1.5</remarks>
[RenderDocApiVersion(1, 5)]
public static bool ShowReplayUI()
{
if (Api is null) return false;
AssertAtLeast(1, 5);
return Api->ShowReplayUI() != 0;
}
/// <summary>
/// Sets a given title for the currently in-progress capture, which will be displayed in the UI.<br/>
/// This can be used either with a user-defined capture using a manual start and end,
/// or an automatic capture triggered by <see cref="TriggerCapture"/> or a keypress.<br/>
/// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.<br/>
/// 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 <see cref="StartFrameCapture"/> and before <see cref="EndFrameCapture"/>.<br/>
/// 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.
/// </summary>
/// <param name="title">The title to set for the in-progress capture.</param>
/// <remarks>Requires RenderDoc API version 1.6</remarks>
[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
/// <summary>
/// Reload the internal RenderDoc API structure. Useful for manually refreshing <see cref="Api"/> while using process injection.
/// </summary>
/// <param name="ignoreAlreadyLoaded">Ignores the existing API function structure and overwrites it with a re-request.</param>
/// <param name="requiredVersion">The version of the RenderDoc API required by your application.</param>
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]<RenderDocVersion, RenderDocApi**, int>)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<RenderDocApiVersionAttribute>()!.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
}
}
@@ -0,0 +1,51 @@
namespace Ryujinx.Graphics.RenderDocApi
{
#pragma warning disable CS0649
internal unsafe struct RenderDocApi
{
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
public delegate* unmanaged[Cdecl]<CaptureOption, uint, int> SetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<CaptureOption, uint> GetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, uint> GetCapture;
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
public delegate* unmanaged[Cdecl]<uint> IsTargetControlConnected;
public delegate* unmanaged[Cdecl]<uint, byte*, uint> LaunchReplayUI;
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
public delegate* unmanaged[Cdecl]<uint> IsFrameCapturing;
public delegate* unmanaged[Cdecl]<void*, void*, uint> EndFrameCapture;
// 1.1
public delegate* unmanaged[Cdecl]<uint, void> TriggerMultiFrameCapture;
// 1.2
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
// 1.3
public delegate* unmanaged[Cdecl]<void*, void*, uint> DiscardFrameCapture;
// 1.5
public delegate* unmanaged[Cdecl]<uint> ShowReplayUI;
// 1.6
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
}
#pragma warning restore CS0649
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
+32
View File
@@ -0,0 +1,32 @@
using Silk.NET.Vulkan;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Vulkan
{
public static class Helpers
{
extension(Vk api)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer() =>
api.CurrentInstance is not null
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
: null;
}
extension(Instance instance)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer()
=> (*((void**)(instance.Handle)));
}
}
}
@@ -1,104 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan.MoltenVK
{
enum MVKConfigLogLevel
{
None = 0,
Error = 1,
Warning = 2,
Info = 3,
Debug = 4,
}
enum MVKConfigTraceVulkanCalls
{
None = 0,
Enter = 1,
EnterExit = 2,
Duration = 3,
}
enum MVKConfigAutoGPUCaptureScope
{
None = 0,
Device = 1,
Frame = 2,
}
[Flags]
enum MVKConfigAdvertiseExtensions
{
All = 0x00000001,
MoltenVK = 0x00000002,
WSI = 0x00000004,
Portability = 0x00000008,
}
enum MVKVkSemaphoreSupportStyle
{
MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_SINGLE_QUEUE = 0,
MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_METAL_EVENTS_WHERE_SAFE = 1,
MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_METAL_EVENTS = 2,
MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_CALLBACK = 3,
MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_MAX_ENUM = 0x7FFFFFFF,
}
readonly struct Bool32
{
uint Value { get; }
public Bool32(uint value)
{
Value = value;
}
public Bool32(bool value)
{
Value = value ? 1u : 0u;
}
public static implicit operator bool(Bool32 val) => val.Value == 1;
public static implicit operator Bool32(bool val) => new(val);
}
[StructLayout(LayoutKind.Sequential)]
struct MVKConfiguration
{
public Bool32 DebugMode;
public Bool32 ShaderConversionFlipVertexY;
public Bool32 SynchronousQueueSubmits;
public Bool32 PrefillMetalCommandBuffers;
public uint MaxActiveMetalCommandBuffersPerQueue;
public Bool32 SupportLargeQueryPools;
public Bool32 PresentWithCommandBuffer;
public Bool32 SwapchainMagFilterUseNearest;
public ulong MetalCompileTimeout;
public Bool32 PerformanceTracking;
public uint PerformanceLoggingFrameCount;
public Bool32 DisplayWatermark;
public Bool32 SpecializedQueueFamilies;
public Bool32 SwitchSystemGPU;
public Bool32 FullImageViewSwizzle;
public uint DefaultGPUCaptureScopeQueueFamilyIndex;
public uint DefaultGPUCaptureScopeQueueIndex;
public Bool32 FastMathEnabled;
public MVKConfigLogLevel LogLevel;
public MVKConfigTraceVulkanCalls TraceVulkanCalls;
public Bool32 ForceLowPowerGPU;
public Bool32 SemaphoreUseMTLFence;
public MVKVkSemaphoreSupportStyle SemaphoreSupportStyle;
public MVKConfigAutoGPUCaptureScope AutoGPUCaptureScope;
public nint AutoGPUCaptureOutputFilepath;
public Bool32 Texture1DAs2D;
public Bool32 PreallocateDescriptors;
public Bool32 UseCommandPooling;
public Bool32 UseMTLHeap;
public Bool32 LogActivityPerformanceInline;
public uint ApiVersionToAdvertise;
public MVKConfigAdvertiseExtensions AdvertiseExtensions;
public Bool32 ResumeLostDevice;
public Bool32 UseMetalArgumentBuffers;
}
}
@@ -1,51 +0,0 @@
using Silk.NET.Core.Loader;
using Silk.NET.Vulkan;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Graphics.Vulkan.MoltenVK
{
[SupportedOSPlatform("macos")]
public static partial class MVKInitialization
{
private const string VulkanLib = "libvulkan.dylib";
[LibraryImport("libMoltenVK.dylib")]
private static partial Result vkGetMoltenVKConfigurationMVK(nint unusedInstance, out MVKConfiguration config, in nint configSize);
[LibraryImport("libMoltenVK.dylib")]
private static partial Result vkSetMoltenVKConfigurationMVK(nint unusedInstance, in MVKConfiguration config, in nint configSize);
public static void Initialize()
{
IntPtr configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize);
config.UseMetalArgumentBuffers = true;
config.SemaphoreSupportStyle = MVKVkSemaphoreSupportStyle.MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_SINGLE_QUEUE;
config.SynchronousQueueSubmits = false;
config.ResumeLostDevice = true;
vkSetMoltenVKConfigurationMVK(nint.Zero, config, configSize);
}
private static string[] Resolver(string path)
{
if (path.EndsWith(VulkanLib))
{
path = path[..^VulkanLib.Length] + "libMoltenVK.dylib";
return [path];
}
return [];
}
public static void InitializeResolver()
{
((DefaultPathResolver)PathResolver.Default).Resolvers.Insert(0, Resolver);
}
}
}
@@ -44,6 +44,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Ryujinx.SPB" />
<PackageReference Include="Silk.NET.Shaderc" ExcludeAssets="native" /> <PackageReference Include="Silk.NET.Shaderc" ExcludeAssets="native" />
<PackageReference Include="Silk.NET.Vulkan" /> <PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
+1
View File
@@ -99,6 +99,7 @@ namespace Ryujinx.Graphics.Vulkan
DriverId.MesaNvk => "NVK", DriverId.MesaNvk => "NVK",
DriverId.ImaginationOpenSourceMesa => "Imagination (Open)", DriverId.ImaginationOpenSourceMesa => "Imagination (Open)",
DriverId.MesaHoneykrisp => "Honeykrisp", DriverId.MesaHoneykrisp => "Honeykrisp",
DriverId.MesaKosmickrisp => "KosmicKrisp",
_ => id.ToString(), _ => id.ToString(),
}; };
} }
@@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Runtime.Versioning;
namespace Ryujinx.Graphics.Vulkan
{
[SupportedOSPlatform("macos")]
public static class VulkanDriver
{
private static string GetIcdPath(string fileName)
{
string baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string contentsDir = Path.GetDirectoryName(baseDir)!;
string[] searchPaths =
[
// .app bundle:
Path.Combine(contentsDir, "Resources", "vulkan", "icd.d", fileName),
// Local build:
Path.Combine(contentsDir, fileName)
];
foreach (string icdPath in searchPaths)
{
if (File.Exists(icdPath))
{
return icdPath;
}
}
throw new FileNotFoundException(
$"{fileName} not found. Searched:\n{string.Join(Environment.NewLine, searchPaths)}");
}
public static void MoltenVK()
{
Environment.SetEnvironmentVariable("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1");
Environment.SetEnvironmentVariable("MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE", "0");
Environment.SetEnvironmentVariable("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0");
Environment.SetEnvironmentVariable("MVK_CONFIG_RESUME_LOST_DEVICE", "1");
Environment.SetEnvironmentVariable("VK_DRIVER_FILES", GetIcdPath("MoltenVK_icd.json"));
Console.WriteLine($"[MoltenVK] VK_DRIVER_FILES just set to: {Environment.GetEnvironmentVariable("VK_DRIVER_FILES")}");
}
public static void KosmicKrisp()
{
Environment.SetEnvironmentVariable("VK_DRIVER_FILES", GetIcdPath("libkosmickrisp_icd.json"));
Console.WriteLine($"[KosmicKrisp] VK_DRIVER_FILES just set to: {Environment.GetEnvironmentVariable("VK_DRIVER_FILES")}");
}
}
}
@@ -5,7 +5,6 @@ using Silk.NET.Core;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT; using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Vulkan.Extensions.KHR; using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -14,10 +13,11 @@ namespace Ryujinx.Graphics.Vulkan
{ {
public unsafe static class VulkanInitialization public unsafe static class VulkanInitialization
{ {
private static Version32 Version14 => new(1, 4, 0);
private const uint InvalidIndex = uint.MaxValue; private const uint InvalidIndex = uint.MaxValue;
private static readonly uint _minimalVulkanVersion = Vk.Version11.Value; private static readonly uint _minimalVulkanVersion = Vk.Version11.Value;
private static readonly uint _minimalInstanceVulkanVersion = Vk.Version12.Value; private static readonly uint _maximumVulkanVersion = Version14.Value;
private static readonly uint _maximumVulkanVersion = Vk.Version12.Value;
private const string AppName = "Ryujinx.Graphics.Vulkan"; private const string AppName = "Ryujinx.Graphics.Vulkan";
private const int QueuesCount = 2; private const int QueuesCount = 2;
@@ -59,7 +59,7 @@ namespace Ryujinx.Graphics.Vulkan
internal static VulkanInstance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions) internal static VulkanInstance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions)
{ {
List<string> enabledLayers = new(); List<string> enabledLayers = [];
IReadOnlySet<string> instanceExtensions = VulkanInstance.GetInstanceExtensions(api); IReadOnlySet<string> instanceExtensions = VulkanInstance.GetInstanceExtensions(api);
IReadOnlySet<string> instanceLayers = VulkanInstance.GetInstanceLayers(api); IReadOnlySet<string> instanceLayers = VulkanInstance.GetInstanceLayers(api);
@@ -88,7 +88,7 @@ namespace Ryujinx.Graphics.Vulkan
enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray(); enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray();
} }
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); nint appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new() ApplicationInfo applicationInfo = new()
{ {
@@ -168,7 +168,7 @@ namespace Ryujinx.Graphics.Vulkan
internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api) internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api)
{ {
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); nint appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new() ApplicationInfo applicationInfo = new()
{ {
@@ -197,13 +197,6 @@ namespace Ryujinx.Graphics.Vulkan
using VulkanInstance instance = rawInstance; using VulkanInstance instance = rawInstance;
// We currently assume that the instance is compatible with Vulkan 1.2
// TODO: Remove this once we relax our initialization codepaths.
if (instance.InstanceVersion < _minimalInstanceVulkanVersion)
{
return [];
}
instance.EnumeratePhysicalDevices(out VulkanPhysicalDevice[] physicalDevices).ThrowOnError(); instance.EnumeratePhysicalDevices(out VulkanPhysicalDevice[] physicalDevices).ThrowOnError();
List<DeviceInfo> deviceInfos = []; List<DeviceInfo> deviceInfos = [];
@@ -386,7 +379,7 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &supportedFeaturesDynamicAttachmentFeedbackLoopLayout; features2.PNext = &supportedFeaturesDynamicAttachmentFeedbackLoopLayout;
} }
PhysicalDevicePageableDeviceLocalMemoryFeaturesEXT supportedPageableDeviceLocalMemoryFeatures = new() PhysicalDevicePageableDeviceLocalMemoryFeaturesEXT supportedFeaturesPageableDeviceLocalMemory = new()
{ {
SType = StructureType.PhysicalDevicePageableDeviceLocalMemoryFeaturesExt, SType = StructureType.PhysicalDevicePageableDeviceLocalMemoryFeaturesExt,
PNext = features2.PNext, PNext = features2.PNext,
@@ -394,10 +387,10 @@ namespace Ryujinx.Graphics.Vulkan
if (physicalDevice.IsDeviceExtensionPresent(ExtPageableDeviceLocalMemory.ExtensionName)) if (physicalDevice.IsDeviceExtensionPresent(ExtPageableDeviceLocalMemory.ExtensionName))
{ {
features2.PNext = &supportedPageableDeviceLocalMemoryFeatures; features2.PNext = &supportedFeaturesPageableDeviceLocalMemory;
} }
PhysicalDeviceMemoryPriorityFeaturesEXT supportedDeviceMemoryPriorityFeatures = new() PhysicalDeviceMemoryPriorityFeaturesEXT supportedFeaturesDeviceMemoryPriority = new()
{ {
SType = StructureType.PhysicalDeviceMemoryPriorityFeaturesExt, SType = StructureType.PhysicalDeviceMemoryPriorityFeaturesExt,
PNext = features2.PNext, PNext = features2.PNext,
@@ -405,7 +398,7 @@ namespace Ryujinx.Graphics.Vulkan
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_memory_priority")) if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_memory_priority"))
{ {
features2.PNext = &supportedDeviceMemoryPriorityFeatures; features2.PNext = &supportedFeaturesDeviceMemoryPriority;
} }
PhysicalDeviceVulkan12Features supportedPhysicalDeviceVulkan12Features = new() PhysicalDeviceVulkan12Features supportedPhysicalDeviceVulkan12Features = new()
@@ -416,6 +409,22 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &supportedPhysicalDeviceVulkan12Features; features2.PNext = &supportedPhysicalDeviceVulkan12Features;
PhysicalDeviceVulkan13Features supportedPhysicalDeviceVulkan13Features = new()
{
SType = StructureType.PhysicalDeviceVulkan13Features,
PNext = features2.PNext,
};
features2.PNext = &supportedPhysicalDeviceVulkan13Features;
PhysicalDeviceVulkan14Features supportedPhysicalDeviceVulkan14Features = new()
{
SType = StructureType.PhysicalDeviceVulkan14Features,
PNext = features2.PNext,
};
features2.PNext = &supportedPhysicalDeviceVulkan14Features;
api.GetPhysicalDeviceFeatures2(physicalDevice.PhysicalDevice, &features2); api.GetPhysicalDeviceFeatures2(physicalDevice.PhysicalDevice, &features2);
PhysicalDeviceFeatures supportedFeatures = features2.Features; PhysicalDeviceFeatures supportedFeatures = features2.Features;
@@ -519,6 +528,24 @@ namespace Ryujinx.Graphics.Vulkan
pExtendedFeatures = &featuresVk12; pExtendedFeatures = &featuresVk12;
PhysicalDeviceVulkan13Features featuresVk13 = new()
{
SType = StructureType.PhysicalDeviceVulkan13Features,
PNext = pExtendedFeatures,
/* Vulkan 1.3 features here */
};
pExtendedFeatures = &featuresVk13;
PhysicalDeviceVulkan13Features featuresVk14 = new()
{
SType = StructureType.PhysicalDeviceVulkan14Features,
PNext = pExtendedFeatures,
/* Vulkan 1.4 features here */
};
pExtendedFeatures = &featuresVk14;
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_index_type_uint8")) if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_index_type_uint8"))
{ {
PhysicalDeviceIndexTypeUint8FeaturesEXT featuresIndexU8 = new() PhysicalDeviceIndexTypeUint8FeaturesEXT featuresIndexU8 = new()
@@ -598,7 +625,7 @@ namespace Ryujinx.Graphics.Vulkan
} }
if (physicalDevice.IsDeviceExtensionPresent(ExtPageableDeviceLocalMemory.ExtensionName) && if (physicalDevice.IsDeviceExtensionPresent(ExtPageableDeviceLocalMemory.ExtensionName) &&
supportedPageableDeviceLocalMemoryFeatures.PageableDeviceLocalMemory) supportedFeaturesPageableDeviceLocalMemory.PageableDeviceLocalMemory)
{ {
PhysicalDevicePageableDeviceLocalMemoryFeaturesEXT featuresPageableDeviceLocalMemory = new() PhysicalDevicePageableDeviceLocalMemoryFeaturesEXT featuresPageableDeviceLocalMemory = new()
{ {
@@ -611,16 +638,16 @@ namespace Ryujinx.Graphics.Vulkan
} }
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_memory_priority") && if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_memory_priority") &&
supportedDeviceMemoryPriorityFeatures.MemoryPriority) supportedFeaturesDeviceMemoryPriority.MemoryPriority)
{ {
PhysicalDeviceMemoryPriorityFeaturesEXT featuresDeviceMemoryPriority = new() PhysicalDeviceMemoryPriorityFeaturesEXT featuresMemoryPriority = new()
{ {
SType = StructureType.PhysicalDeviceMemoryPriorityFeaturesExt, SType = StructureType.PhysicalDeviceMemoryPriorityFeaturesExt,
PNext = pExtendedFeatures, PNext = pExtendedFeatures,
MemoryPriority = true, MemoryPriority = true,
}; };
pExtendedFeatures = &featuresDeviceMemoryPriority; pExtendedFeatures = &featuresMemoryPriority;
} }
string[] enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray(); string[] enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray();
@@ -13,7 +13,6 @@ namespace Ryujinx.Graphics.Vulkan
{ {
private readonly Vk _api; private readonly Vk _api;
public readonly Instance Instance; public readonly Instance Instance;
public readonly Version32 InstanceVersion;
private bool _disposed; private bool _disposed;
@@ -21,22 +20,6 @@ namespace Ryujinx.Graphics.Vulkan
{ {
_api = api; _api = api;
Instance = instance; Instance = instance;
if (api.GetInstanceProcAddr(instance, "vkEnumerateInstanceVersion") == nint.Zero)
{
InstanceVersion = Vk.Version10;
}
else
{
uint rawInstanceVersion = 0;
if (api.EnumerateInstanceVersion(ref rawInstanceVersion) != Result.Success)
{
rawInstanceVersion = Vk.Version11.Value;
}
InstanceVersion = (Version32)rawInstanceVersion;
}
} }
public static Result Create(Vk api, ref InstanceCreateInfo createInfo, out VulkanInstance instance) public static Result Create(Vk api, ref InstanceCreateInfo createInfo, out VulkanInstance instance)
+15 -7
View File
@@ -3,7 +3,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader; using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation; using Ryujinx.Graphics.Shader.Translation;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Graphics.Vulkan.Queries; using Ryujinx.Graphics.Vulkan.Queries;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT; using Silk.NET.Vulkan.Extensions.EXT;
@@ -97,6 +96,7 @@ namespace Ryujinx.Graphics.Vulkan
internal bool IsIntelArc { get; private set; } internal bool IsIntelArc { get; private set; }
internal bool IsQualcommProprietary { get; private set; } internal bool IsQualcommProprietary { get; private set; }
internal bool IsMoltenVk { get; private set; } internal bool IsMoltenVk { get; private set; }
internal bool IsKosmicKrisp { get; private set; }
internal bool SupportsMTL31 { get; private set; } internal bool SupportsMTL31 { get; private set; }
internal bool IsTBDR { get; private set; } internal bool IsTBDR { get; private set; }
internal bool IsSharedMemory { get; private set; } internal bool IsSharedMemory { get; private set; }
@@ -111,7 +111,7 @@ namespace Ryujinx.Graphics.Vulkan
public event EventHandler<ScreenCaptureImageInfo> ScreenCaptured; public event EventHandler<ScreenCaptureImageInfo> ScreenCaptured;
public VulkanRenderer(Vk api, Func<Instance, Vk, SurfaceKHR> surfaceFunc, Func<string[]> requiredExtensionsFunc, string preferredGpuId) public VulkanRenderer(Vk api, Func<Instance, Vk, SurfaceKHR> surfaceFunc, Func<string[]> requiredExtensionsFunc, string preferredGpuId, TranslationLayer translationLayer)
{ {
_getSurface = surfaceFunc; _getSurface = surfaceFunc;
_getRequiredExtensions = requiredExtensionsFunc; _getRequiredExtensions = requiredExtensionsFunc;
@@ -121,13 +121,21 @@ namespace Ryujinx.Graphics.Vulkan
Textures = []; Textures = [];
Samplers = []; Samplers = [];
// MoltenVK is required for any device running < macOS 26, even Intel and AMD vendors.
// KosmicKrisp, a Vulkan conformant implementation running on top of Metal 4, requires macOS 26.
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
{ {
MVKInitialization.Initialize(); if (translationLayer == TranslationLayer.MoltenVK)
{
// Any device running on MacOS is using MoltenVK, even Intel and AMD vendors. VulkanDriver.MoltenVK();
IsMoltenVk = true; IsMoltenVk = true;
} }
if (translationLayer == TranslationLayer.KosmicKrisp)
{
VulkanDriver.KosmicKrisp();
IsKosmicKrisp = true;
}
}
SupportsMTL31 = OperatingSystem.IsMacOSVersionAtLeast(14); SupportsMTL31 = OperatingSystem.IsMacOSVersionAtLeast(14);
} }
@@ -842,14 +850,14 @@ namespace Ryujinx.Graphics.Vulkan
/// <summary> /// <summary>
/// Gets the available Vulkan devices using the default Vulkan API /// Gets the available Vulkan devices using the default Vulkan API
/// object returned by <see cref="Vk.GetApi()"/> /// object returned by <see cref="VulkanSpbApi.GetApiFromSpb()"/>
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static DeviceInfo[] GetPhysicalDevices() public static DeviceInfo[] GetPhysicalDevices()
{ {
try try
{ {
return VulkanInitialization.GetSuitablePhysicalDevices(Vk.GetApi()); return VulkanInitialization.GetSuitablePhysicalDevices(VulkanSpbApi.GetApiFromSpb());
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -0,0 +1,34 @@
using Silk.NET.Core.Contexts;
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
namespace Ryujinx.Graphics.Vulkan
{
public static class VulkanSpbApi
{
public static Vk GetApiFromSpb()
{
LamdaNativeContext baseCtx = new(VulkanHelper.GetProcAddress);
MultiNativeContext ctx = new(baseCtx, null);
Vk ret = new(ctx);
ctx.Contexts[1] = new LamdaNativeContext(x =>
{
if (x.EndsWith("ProcAddr"))
{
return default;
}
nint ptr = ret.GetDeviceProcAddr(ret.CurrentDevice.GetValueOrDefault(), x);
if (ptr != default)
{
return ptr;
}
return ret.GetInstanceProcAddr(ret.CurrentInstance.GetValueOrDefault(), x);
});
return ret;
}
}
}
@@ -166,13 +166,15 @@ namespace Ryujinx.HLE.HOS.Applets.Error
string[] buttons = GetButtonsText(module, description, "DlgBtn"); string[] buttons = GetButtonsText(module, description, "DlgBtn");
bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); (uint Module, uint Description) errorCodeTuple = (module, uint.Parse(description.ToString("0000")));
bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons, errorCodeTuple);
if (showDetails) if (showDetails)
{ {
message = GetMessageText(module, description, "FlvMsg"); message = GetMessageText(module, description, "FlvMsg");
buttons = GetButtonsText(module, description, "FlvBtn"); buttons = GetButtonsText(module, description, "FlvBtn");
_horizon.Device.UIHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons, errorCodeTuple);
} }
} }
@@ -27,9 +27,19 @@ namespace Ryujinx.HLE.HOS.Applets
_normalSession = normalSession; _normalSession = normalSession;
_interactiveSession = interactiveSession; _interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data UserProfile selected = _system.Device.UIHandler.ShowPlayerSelectDialog();
if (selected == null)
{
_normalSession.Push(BuildResponse()); _normalSession.Push(BuildResponse());
}
else if (selected.UserId == new UserId("00000000000000000000000000000080"))
{
_normalSession.Push(BuildGuestResponse());
}
else
{
_normalSession.Push(BuildResponse(selected));
}
AppletStateChanged?.Invoke(this, null); AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus(); _system.ReturnFocus();
@@ -37,16 +47,34 @@ namespace Ryujinx.HLE.HOS.Applets
return ResultCode.Success; return ResultCode.Success;
} }
private byte[] BuildResponse() private byte[] BuildResponse(UserProfile selectedUser)
{ {
UserProfile currentUser = _system.AccountManager.LastOpenedUser;
using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream); using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Success); writer.Write((ulong)PlayerSelectResult.Success);
currentUser.UserId.Write(writer); selectedUser.UserId.Write(writer);
return stream.ToArray();
}
private byte[] BuildGuestResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write(new byte());
return stream.ToArray();
}
private byte[] BuildResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Failure);
return stream.ToArray(); return stream.ToArray();
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+1
View File
@@ -59,6 +59,7 @@
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnB.png" /> <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnB.png" />
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF6.png" /> <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF6.png" />
<EmbeddedResource Include="HOS\Services\Account\Acc\DefaultUserImage.jpg" /> <EmbeddedResource Include="HOS\Services\Account\Acc\DefaultUserImage.jpg" />
<EmbeddedResource Include="HOS\Services\Account\Acc\GuestUserImage.jpg" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+8 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
namespace Ryujinx.HLE.UI namespace Ryujinx.HLE.UI
@@ -48,7 +49,8 @@ namespace Ryujinx.HLE.UI
/// Displays a Message Dialog box specific to Error Applet and blocks until it is closed. /// Displays a Message Dialog box specific to Error Applet and blocks until it is closed.
/// </summary> /// </summary>
/// <returns>False when OK is pressed, True when another button (Details) is pressed.</returns> /// <returns>False when OK is pressed, True when another button (Details) is pressed.</returns>
bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText); // ReSharper disable once UnusedParameter.Global
bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText, (uint Module, uint Description)? errorCode = null);
/// <summary> /// <summary>
/// Creates a handler to process keyboard inputs into text strings. /// Creates a handler to process keyboard inputs into text strings.
@@ -65,5 +67,10 @@ namespace Ryujinx.HLE.UI
/// Takes a screenshot from the current renderer and saves it in the screenshots folder. /// Takes a screenshot from the current renderer and saves it in the screenshots folder.
/// </summary> /// </summary>
void TakeScreenshot(); void TakeScreenshot();
/// <summary>
/// Displays the player select dialog and returns the selected profile.
/// </summary>
UserProfile ShowPlayerSelectDialog();
} }
} }
@@ -439,6 +439,11 @@ namespace Ryujinx.UI.Common.Configuration
/// </summary> /// </summary>
public GraphicsBackend GraphicsBackend { get; set; } public GraphicsBackend GraphicsBackend { get; set; }
/// <summary>
/// Vulkan translation layer
/// </summary>
public TranslationLayer TranslationLayer { get; set; }
/// <summary> /// <summary>
/// Preferred GPU /// Preferred GPU
/// </summary> /// </summary>
@@ -587,6 +587,11 @@ namespace Ryujinx.UI.Common.Configuration
/// </summary> /// </summary>
public ReactiveObject<GraphicsBackend> GraphicsBackend { get; private set; } public ReactiveObject<GraphicsBackend> GraphicsBackend { get; private set; }
/// <summary>
/// Vulkan translation layer
/// </summary>
public ReactiveObject<TranslationLayer> TranslationLayer { get; private set; }
/// <summary> /// <summary>
/// Applies anti-aliasing to the renderer. /// Applies anti-aliasing to the renderer.
/// </summary> /// </summary>
@@ -639,6 +644,8 @@ namespace Ryujinx.UI.Common.Configuration
EnableTextureRecompression.Event += static (_, e) => LogValueChange(e, nameof(EnableTextureRecompression)); EnableTextureRecompression.Event += static (_, e) => LogValueChange(e, nameof(EnableTextureRecompression));
GraphicsBackend = new ReactiveObject<GraphicsBackend>(); GraphicsBackend = new ReactiveObject<GraphicsBackend>();
GraphicsBackend.Event += static (_, e) => LogValueChange(e, nameof(GraphicsBackend)); GraphicsBackend.Event += static (_, e) => LogValueChange(e, nameof(GraphicsBackend));
TranslationLayer = new ReactiveObject<TranslationLayer>();
TranslationLayer.Event += static (_, e) => LogValueChange(e, nameof(TranslationLayer));
PreferredGpu = new ReactiveObject<string>(); PreferredGpu = new ReactiveObject<string>();
PreferredGpu.Event += static (_, e) => LogValueChange(e, nameof(PreferredGpu)); PreferredGpu.Event += static (_, e) => LogValueChange(e, nameof(PreferredGpu));
EnableMacroHLE = new ReactiveObject<bool>(); EnableMacroHLE = new ReactiveObject<bool>();
@@ -935,6 +942,7 @@ namespace Ryujinx.UI.Common.Configuration
ControllerConfig = [], ControllerConfig = [],
InputConfig = Hid.InputConfig, InputConfig = Hid.InputConfig,
GraphicsBackend = Graphics.GraphicsBackend, GraphicsBackend = Graphics.GraphicsBackend,
TranslationLayer = Graphics.TranslationLayer,
PreferredGpu = Graphics.PreferredGpu, PreferredGpu = Graphics.PreferredGpu,
MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
MultiplayerMode = Multiplayer.Mode, MultiplayerMode = Multiplayer.Mode,
@@ -958,6 +966,7 @@ namespace Ryujinx.UI.Common.Configuration
Graphics.MaxAnisotropy.Value = -1.0f; Graphics.MaxAnisotropy.Value = -1.0f;
Graphics.AspectRatio.Value = AspectRatio.Fixed16x9; Graphics.AspectRatio.Value = AspectRatio.Fixed16x9;
Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); Graphics.GraphicsBackend.Value = DefaultGraphicsBackend();
Graphics.TranslationLayer.Value = TranslationLayer.MoltenVK;
Graphics.PreferredGpu.Value = ""; Graphics.PreferredGpu.Value = "";
Graphics.ShadersDumpPath.Value = ""; Graphics.ShadersDumpPath.Value = "";
Graphics.TexturesDumpPath.Value = ""; Graphics.TexturesDumpPath.Value = "";
@@ -1806,6 +1815,15 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
if (configurationFileFormat.Version < 62)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 62.");
configurationFileFormat.TranslationLayer = TranslationLayer.MoltenVK;
configurationFileUpdated = true;
}
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
@@ -1818,6 +1836,7 @@ namespace Ryujinx.UI.Common.Configuration
Graphics.EnableTextureRealTimeEdit.Value = configurationFileFormat.GraphicsEnableTextureRealTimeEdit; Graphics.EnableTextureRealTimeEdit.Value = configurationFileFormat.GraphicsEnableTextureRealTimeEdit;
Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading;
Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend;
Graphics.TranslationLayer.Value = configurationFileFormat.TranslationLayer;
Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu;
Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing;
Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter;
@@ -12,9 +12,13 @@ namespace Ryujinx.UI.Common.Helper
public static bool? OverrideDockedMode { get; private set; } public static bool? OverrideDockedMode { get; private set; }
public static bool? OverrideHardwareAcceleration { get; private set; } public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; } public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideTranslationLayer { get; private set; }
public static string OverrideBackendThreading { get; private set; } public static string OverrideBackendThreading { get; private set; }
public static string OverrideHideCursor { get; private set; } public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { 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 FilePath FirmwareToInstallPathArg { get; set; } public static FilePath FirmwareToInstallPathArg { get; set; }
public static string Profile { get; private set; } public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; } public static string LaunchPathArg { get; private set; }
@@ -45,6 +49,20 @@ namespace Ryujinx.UI.Common.Helper
BaseDirPathArg = args[++i]; 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(arg);
arguments.Add(args[i]); arguments.Add(args[i]);
break; break;
@@ -92,6 +110,16 @@ namespace Ryujinx.UI.Common.Helper
OverrideGraphicsBackend = args[++i]; OverrideGraphicsBackend = args[++i];
break; break;
case "--translation-layer":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideTranslationLayer = args[++i];
break;
case "--backend-threading": case "--backend-threading":
if (i + 1 >= args.Length) if (i + 1 >= args.Length)
{ {
@@ -1,3 +1,4 @@
using Gommon;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using System; using System;
@@ -26,5 +27,23 @@ namespace Ryujinx.UI.Common.Helper
return appTitle; return 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);
}
} }
} }
+1
View File
@@ -14,6 +14,7 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using System; using System;
+9 -4
View File
@@ -43,7 +43,6 @@ using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common; using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan;
using SkiaSharp; using SkiaSharp;
using SPB.Graphics.Exceptions; using SPB.Graphics.Exceptions;
using SPB.Graphics.Vulkan; using SPB.Graphics.Vulkan;
@@ -939,10 +938,11 @@ namespace Ryujinx.Ava
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan) if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
{ {
renderer = new VulkanRenderer( renderer = new VulkanRenderer(
Vk.GetApi(), VulkanSpbApi.GetApiFromSpb(),
((EmbeddedWindowVulkan)RendererHost.EmbeddedWindow).CreateSurface, ((EmbeddedWindowVulkan)RendererHost.EmbeddedWindow).CreateSurface,
VulkanHelper.GetRequiredInstanceExtensions, VulkanHelper.GetRequiredInstanceExtensions,
ConfigurationState.Instance.Graphics.PreferredGpu.Value); ConfigurationState.Instance.Graphics.PreferredGpu.Value,
ConfigurationState.Instance.Graphics.TranslationLayer.Value);
} }
else else
{ {
@@ -1183,7 +1183,12 @@ namespace Ryujinx.Ava
{ {
_viewModel.BackendText = ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch _viewModel.BackendText = ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch
{ {
GraphicsBackend.Vulkan => "Vulkan", GraphicsBackend.Vulkan when !OperatingSystem.IsMacOS() => "Vulkan",
GraphicsBackend.Vulkan when Environment.GetEnvironmentVariable("VK_DRIVER_FILES")!.Contains("MoltenVK")
=> "MoltenVK",
GraphicsBackend.Vulkan when
Environment.GetEnvironmentVariable("VK_DRIVER_FILES")!.Contains("kosmickrisp") => "KosmicKrisp",
GraphicsBackend.Vulkan => _viewModel.BackendText,
GraphicsBackend.OpenGl => "OpenGL", GraphicsBackend.OpenGl => "OpenGL",
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };
+7 -1
View File
@@ -189,6 +189,8 @@
"SettingsTabSystemIgnoreControllerApplet": "Ignore Controller Applet", "SettingsTabSystemIgnoreControllerApplet": "Ignore Controller Applet",
"SettingsTabGraphics": "Graphics", "SettingsTabGraphics": "Graphics",
"SettingsTabGraphicsAPI": "Graphics API", "SettingsTabGraphicsAPI": "Graphics API",
"SettingsTabTranslationLayer": "Translation Layer (Vulkan):",
"SettingsTabTranslationLayerTooltip": "MoltenVK is a Vulkan Portability implementation, enabling Vulkan applications to run on macOS.\n\nKosmicKrisp is a Vulkan (1.3) conformant Vulkan-on-Metal driver and the eventual successor to MoltenVK.\n\nKosmicKrisp is not yet feature-complete, meaning that games *may* not render correctly or possibly even crash.\n\nUse MoltenVK if unsure.",
"SettingsTabGraphicsEnableShaderCache": "Enable Shader Cache", "SettingsTabGraphicsEnableShaderCache": "Enable Shader Cache",
"SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:", "SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
@@ -934,5 +936,9 @@
"GameListContextMenuExtractDataAocRomFSToolTip": "Extract the RomFS from a selected DLC file", "GameListContextMenuExtractDataAocRomFSToolTip": "Extract the RomFS from a selected DLC file",
"ExtractAocListHeader": "Select a DLC to Extract", "ExtractAocListHeader": "Select a DLC to Extract",
"SettingsTabSystemSkipUserProfilesManager": "Skip Dialog 'Manage User Profiles'", "SettingsTabSystemSkipUserProfilesManager": "Skip Dialog 'Manage User Profiles'",
"SkipUserProfilesTooltip": "This option skips the 'Manage User Profiles' dialog during gameplay, using a pre-selected profile.\n\nProfile switching is found in 'Settings' - 'Manager User Profiles'. Select the desired profile before loading the game." "SkipUserProfilesTooltip": "This option skips the 'Manage User Profiles' dialog during gameplay, using a pre-selected profile.\n\nProfile switching is found in 'Settings' - 'Manager User Profiles'. Select the desired profile before loading the game.",
"MenuBarActions_StartCapture": "Start RenderDoc Frame Capture",
"MenuBarActions_EndCapture": "End RenderDoc Frame Capture",
"MenuBarActions_DiscardCapture": "Discard RenderDoc Frame Capture",
"MenuBarActions_DiscardCapture_ToolTip": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result."
} }
+5 -1
View File
@@ -807,5 +807,9 @@
"MultiplayerModeDisabled": "Deshabilitar", "MultiplayerModeDisabled": "Deshabilitar",
"MultiplayerModeLdnMitm": "ldn_mitm", "MultiplayerModeLdnMitm": "ldn_mitm",
"SettingsTabSystemSkipUserProfilesManager": "Omitir el Diálogo 'Gestionar Perfiles de Usuario'", "SettingsTabSystemSkipUserProfilesManager": "Omitir el Diálogo 'Gestionar Perfiles de Usuario'",
"SkipUserProfilesTooltip": "Esta opción omite el diálogo de 'Gestionar perfiles de usuario' durante el juego, utilizando un perfil preseleccionado.\n\nEl cambio de perfil se encuentra en 'Configuración' - 'Gestionar perfiles de usuario'. Seleccione el perfil deseado antes de cargar el juego." "SkipUserProfilesTooltip": "Esta opción omite el diálogo de 'Gestionar perfiles de usuario' durante el juego, utilizando un perfil preseleccionado.\n\nEl cambio de perfil se encuentra en 'Configuración' - 'Gestionar perfiles de usuario'. Seleccione el perfil deseado antes de cargar el juego.",
"MenuBarActions_StartCapture": "Iniciar una captura de fotograma de RenderDoc",
"MenuBarActions_EndCapture": "Detener la captura de fotograma de RenderDoc",
"MenuBarActions_DiscardCapture": "Descartar la captura de fotograma de RenderDoc",
"MenuBarActions_DiscardCapture_ToolTip": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado."
} }
+5 -1
View File
@@ -828,5 +828,9 @@
"GameListContextMenuExtractDataAocRomFSToolTip": "Extraire les RomFS d'un fichier DLC choisi", "GameListContextMenuExtractDataAocRomFSToolTip": "Extraire les RomFS d'un fichier DLC choisi",
"ExtractAocListHeader": "Choisissez un DLC à extraire", "ExtractAocListHeader": "Choisissez un DLC à extraire",
"SettingsTabSystemSkipUserProfilesManager": "Ignorer la Boîte de Dialogue « Gérer les Profils d'Utilisateurs »", "SettingsTabSystemSkipUserProfilesManager": "Ignorer la Boîte de Dialogue « Gérer les Profils d'Utilisateurs »",
"SkipUserProfilesTooltip": "Cette option permet d'éviter le dialogue du 'Gérer les profils d'utilisateurs' pendant le jeu, en utilisant un profil pré-sélectionné.\n\nLa sélection du profil se trouve dans 'Paramètres' - 'Gérer les profils d'utilisateurs'. Sélectionnez le profil souhaité avant de charger la partie." "SkipUserProfilesTooltip": "Cette option permet d'éviter le dialogue du 'Gérer les profils d'utilisateurs' pendant le jeu, en utilisant un profil pré-sélectionné.\n\nLa sélection du profil se trouve dans 'Paramètres' - 'Gérer les profils d'utilisateurs'. Sélectionnez le profil souhaité avant de charger la partie.",
"MenuBarActions_StartCapture": "Démarrer une capture de trame RenderDoc",
"MenuBarActions_EndCapture": "Arrêter la capture de trame RenderDoc",
"MenuBarActions_DiscardCapture": "Supprimer la capture de trame RenderDoc",
"MenuBarActions_DiscardCapture_ToolTip": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat."
} }
+3 -2
View File
@@ -289,7 +289,7 @@ namespace Ryujinx.Headless
if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow) if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
{ {
string preferredGpuId = string.Empty; string preferredGpuId = string.Empty;
Vk api = Vk.GetApi(); Vk api = VulkanSpbApi.GetApiFromSpb();
if (!string.IsNullOrEmpty(options.PreferredGPUVendor)) if (!string.IsNullOrEmpty(options.PreferredGPUVendor))
{ {
@@ -310,7 +310,8 @@ namespace Ryujinx.Headless
api, api,
(instance, _) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))), (instance, _) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
VulkanWindow.GetRequiredInstanceExtensions, VulkanWindow.GetRequiredInstanceExtensions,
preferredGpuId); preferredGpuId,
options.TranslationLayer);
} }
return new OpenGLRenderer(); return new OpenGLRenderer();
-6
View File
@@ -12,7 +12,6 @@ using Ryujinx.Cpu;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
@@ -74,11 +73,6 @@ namespace Ryujinx.Headless
}; };
} }
if (OperatingSystem.IsMacOS())
{
MVKInitialization.InitializeResolver();
}
Parser.Default.ParseArguments<Options>(args) Parser.Default.ParseArguments<Options>(args)
.WithParsed(options => Load(args, options)) .WithParsed(options => Load(args, options))
.WithNotParsed(errors => errors.Output()); .WithNotParsed(errors => errors.Output());
+6
View File
@@ -132,6 +132,9 @@ namespace Ryujinx.Headless
if (NeedsOverride(nameof(GraphicsBackend))) if (NeedsOverride(nameof(GraphicsBackend)))
GraphicsBackend = configurationState.Graphics.GraphicsBackend; GraphicsBackend = configurationState.Graphics.GraphicsBackend;
if (NeedsOverride(nameof(TranslationLayer)))
TranslationLayer = configurationState.Graphics.TranslationLayer;
if (NeedsOverride(nameof(AntiAliasing))) if (NeedsOverride(nameof(AntiAliasing)))
AntiAliasing = configurationState.Graphics.AntiAliasing; AntiAliasing = configurationState.Graphics.AntiAliasing;
@@ -370,6 +373,9 @@ namespace Ryujinx.Headless
[Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")] [Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")]
public GraphicsBackend GraphicsBackend { get; set; } public GraphicsBackend GraphicsBackend { get; set; }
[Option("translation-layer", Required = false, Default = TranslationLayer.MoltenVK, HelpText = "Change Translation Layer to use.")]
public TranslationLayer TranslationLayer { get; set; }
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")] [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
public string PreferredGPUVendor { get; set; } public string PreferredGPUVendor { get; set; }
+8 -1
View File
@@ -10,6 +10,7 @@ using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.HLE.UI; using Ryujinx.HLE.UI;
@@ -28,6 +29,7 @@ using static SDL.SDL3;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Switch = Ryujinx.HLE.Switch; using Switch = Ryujinx.HLE.Switch;
using UserProfile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Headless namespace Ryujinx.Headless
{ {
@@ -531,7 +533,7 @@ namespace Ryujinx.Headless
Exit(); Exit();
} }
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText) public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText, (uint Module, uint Description)? errorCode = null)
{ {
SDL_MessageBoxButtonData[] buttons = new SDL_MessageBoxButtonData[buttonsText.Length]; SDL_MessageBoxButtonData[] buttons = new SDL_MessageBoxButtonData[buttonsText.Length];
@@ -590,5 +592,10 @@ namespace Ryujinx.Headless
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public UserProfile ShowPlayerSelectDialog()
{
return AccountSaveDataManager.GetLastUsedUser();
}
} }
} }
+21 -15
View File
@@ -8,7 +8,6 @@ using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless; using Ryujinx.Headless;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.SDL3.Common; using Ryujinx.SDL3.Common;
@@ -26,7 +25,6 @@ namespace Ryujinx.Ava
{ {
internal partial class Program internal partial class Program
{ {
//
public static double WindowScaleFactor { get; set; } public static double WindowScaleFactor { get; set; }
public static double DesktopScaleFactor { get; set; } = 1.0; public static double DesktopScaleFactor { get; set; } = 1.0;
public static string Version { get; private set; } public static string Version { get; private set; }
@@ -114,11 +112,6 @@ namespace Ryujinx.Ava
// Parse arguments // Parse arguments
CommandLineState.ParseArguments(args); CommandLineState.ParseArguments(args);
if (OperatingSystem.IsMacOS())
{
MVKInitialization.InitializeResolver();
}
// Delete backup files after updating. // Delete backup files after updating.
Task.Run(Updater.CleanupUpdate); Task.Run(Updater.CleanupUpdate);
@@ -218,19 +211,32 @@ namespace Ryujinx.Ava
UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration.Value; UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration.Value;
// Check if graphics backend was overridden // Change graphics backend from OpenGL to Vulkan on macOS
if (CommandLineState.OverrideGraphicsBackend != null) if (OperatingSystem.IsMacOS() && (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl))
{
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
}
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
{ {
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan; ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
} }
// Check if graphics backend was overridden
if (CommandLineState.OverrideGraphicsBackend is not null)
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = CommandLineState.OverrideGraphicsBackend.ToLower() switch
{
"opengl" => GraphicsBackend.OpenGl,
"vulkan" => GraphicsBackend.Vulkan,
_ => ConfigurationState.Instance.Graphics.GraphicsBackend.Value
};
} }
// Check if translation layer was overridden
if (CommandLineState.OverrideTranslationLayer is not null)
ConfigurationState.Instance.Graphics.TranslationLayer.Value = CommandLineState.OverrideTranslationLayer.ToLower() switch
{
"moltenvk" => TranslationLayer.MoltenVK,
"kosmickrisp" => TranslationLayer.KosmicKrisp,
_ => ConfigurationState.Instance.Graphics.TranslationLayer.Value
};
// Check if backend threading was overridden // Check if backend threading was overridden
if (CommandLineState.OverrideBackendThreading is not null) if (CommandLineState.OverrideBackendThreading is not null)
ConfigurationState.Instance.Graphics.BackendThreading.Value = CommandLineState.OverrideBackendThreading.ToLower() switch ConfigurationState.Instance.Graphics.BackendThreading.Value = CommandLineState.OverrideBackendThreading.ToLower() switch
+23 -1
View File
@@ -47,15 +47,17 @@
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Condition="'$(RuntimeIdentifier)' == 'win-x64' OR '$(RuntimeIdentifier)' == 'win-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Condition="'$(RuntimeIdentifier)' == 'win-x64' OR '$(RuntimeIdentifier)' == 'win-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.KosmicKrisp" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="Ryujinx.SPB" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.MoltenVK" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Vulkan.MoltenVK" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" />
<PackageReference Include="Silk.NET.Vulkan" /> <PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SPB" />
<PackageReference Include="SharpZipLib" /> <PackageReference Include="SharpZipLib" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
@@ -89,6 +91,20 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64' OR '$(RuntimeIdentifier)' == 'osx-x64'">
<Content Include="..\..\distribution\macos\vulkan\icd.d\MoltenVK_icd-flat.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>MoltenVK_icd.json</TargetPath>
</Content>
</ItemGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
<Content Include="..\..\distribution\macos\vulkan\icd.d\libkosmickrisp_icd-flat.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>libkosmickrisp_icd.json</TargetPath>
</Content>
</ItemGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'" > <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'" >
<Content Include="..\..\distribution\linux\Ryujinx.sh"> <Content Include="..\..\distribution\linux\Ryujinx.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -168,4 +184,10 @@
<ItemGroup> <ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRootDescriptor.xml" /> <TrimmerRootDescriptor Include="TrimmerRootDescriptor.xml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="UI\Applet\UserSelectorDialog.axaml.cs">
<DependentUpon>UserSelectorDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project> </Project>
+59 -3
View File
@@ -1,17 +1,23 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI; using Ryujinx.HLE.UI;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using System; using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading; using System.Threading;
namespace Ryujinx.Ava.UI.Applet namespace Ryujinx.Ava.UI.Applet
@@ -215,7 +221,7 @@ namespace Ryujinx.Ava.UI.Applet
_parent.ViewModel.AppHost?.Stop(); _parent.ViewModel.AppHost?.Stop();
} }
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) public bool DisplayErrorAppletDialog(string title, string message, string[] buttons, (uint Module, uint Description)? errorCode = null)
{ {
ManualResetEvent dialogCloseEvent = new(false); ManualResetEvent dialogCloseEvent = new(false);
@@ -256,9 +262,59 @@ namespace Ryujinx.Ava.UI.Applet
return showDetails; return showDetails;
} }
public IDynamicTextInputHandler CreateDynamicTextInputHandler() public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent);
public UserProfile ShowPlayerSelectDialog()
{ {
return new AvaloniaDynamicTextInputHandler(_parent); UserId selected = UserId.Null;
byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg");
UserProfile guest = new UserProfile(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage);
ManualResetEvent dialogCloseEvent = new(false);
Dispatcher.UIThread.InvokeAsync(async () =>
{
ObservableCollection<BaseModel> profiles = [];
NavigationDialogHost nav = new();
_parent.AccountManager.GetAllUsers()
.OrderBy(x => x.Name)
.ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
profiles.Add(new Models.UserProfile(guest, nav));
ProfileSelectorDialogViewModel viewModel = new()
{
Profiles = profiles,
SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
};
(selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel);
dialogCloseEvent.Set();
});
dialogCloseEvent.WaitOne();
UserProfile profile = _parent.AccountManager.LastOpenedUser;
if (selected == guest.UserId)
{
profile = guest;
}
else if (selected == UserId.Null)
{
profile = null;
}
else
{
foreach (UserProfile p in _parent.AccountManager.GetAllUsers())
{
if (p.UserId == selected)
{
profile = p;
break;
}
}
}
return profile;
} }
public void TakeScreenshot() public void TakeScreenshot()
@@ -0,0 +1,121 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Applet.ProfileSelectorDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True"
x:DataType="viewModels:ProfileSelectorDialogViewModel">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:ProfileSelectorDialogViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="Transparent"
ItemsSource="{Binding Profiles}"
SelectionChanged="ProfilesList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Rectangle#SelectionIndicator">
<Setter Property="Opacity" Value="0" />
</Style>
</ListBox.Styles>
<ListBox.DataTemplates>
<DataTemplate
DataType="models:UserProfile">
<Grid
PointerEntered="Grid_PointerEntered"
PointerExited="Grid_OnPointerExited">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5"
Background="{Binding BackgroundColor}">
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
Width="96"
Height="96"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<TextBlock
HorizontalAlignment="Stretch"
MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel>
</Border>
</Grid>
</DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
<Panel
Height="118"
Width="96">
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</Panel.Styles>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
</Border>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="10">
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,125 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.UI.Common.Configuration;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Ava.UI.Applet
{
public partial class ProfileSelectorDialog : UserControl
{
public ProfileSelectorDialogViewModel ViewModel { get; set; }
public ProfileSelectorDialog(ProfileSelectorDialogViewModel viewModel)
{
DataContext = ViewModel = viewModel;
InitializeComponent();
}
private void Grid_PointerEntered(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = true;
}
}
private void Grid_OnPointerExited(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = false;
}
}
private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{
ViewModel.SelectedUserId = userProfile.UserId;
Logger.Info?.Print(LogClass.UI, $"Selected: {userProfile.UserId}", "ProfileSelector");
ObservableCollection<BaseModel> newProfiles = [];
foreach (BaseModel item in ViewModel.Profiles)
{
if (item is UserProfile originalItem)
{
UserProfileSft profile = new(originalItem.UserId, originalItem.Name, originalItem.Image);
if (profile.UserId == ViewModel.SelectedUserId)
{
profile.AccountState = AccountState.Open;
}
newProfiles.Add(new UserProfile(profile, new NavigationDialogHost()));
}
}
ViewModel.Profiles = newProfiles;
}
}
}
}
public static async Task<(UserId Id, bool Result)> ShowInputDialog(ProfileSelectorDialogViewModel viewModel)
{
if (ConfigurationState.Instance.System.SkipUserProfilesManager)
{
UserId defaultId = viewModel.SelectedUserId;
return (defaultId, true);
}
FAContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
Content = new ProfileSelectorDialog(viewModel),
Padding = new Thickness(0)
};
UserId result = UserId.Null;
bool input = false;
contentDialog.Closed += Handler;
await ContentDialogHelper.ShowAsync(contentDialog);
return (result, input);
void Handler(FAContentDialog sender, FAContentDialogClosedEventArgs eventArgs)
{
if (eventArgs.Result == FAContentDialogResult.Primary)
{
result = viewModel.SelectedUserId;
input = true;
}
else
{
result = UserId.Null;
input = false;
}
}
}
}
}
+58 -2
View File
@@ -4,6 +4,9 @@ using Avalonia.Platform;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE;
using SPB.Graphics; using SPB.Graphics;
using SPB.Platform; using SPB.Platform;
using SPB.Platform.GLX; using SPB.Platform.GLX;
@@ -30,6 +33,7 @@ namespace Ryujinx.Ava.UI.Renderer
protected nint MetalLayer { get; set; } protected nint MetalLayer { get; set; }
public delegate void UpdateBoundsCallbackDelegate(Rect rect); public delegate void UpdateBoundsCallbackDelegate(Rect rect);
private UpdateBoundsCallbackDelegate _updateBoundsCallback; private UpdateBoundsCallbackDelegate _updateBoundsCallback;
public event EventHandler<nint> WindowCreated; public event EventHandler<nint> WindowCreated;
@@ -46,6 +50,55 @@ namespace Ryujinx.Ava.UI.Renderer
protected virtual void OnWindowDestroyed() { } 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() protected virtual void OnWindowDestroying()
{ {
WindowHandle = nint.Zero; WindowHandle = nint.Zero;
@@ -124,7 +177,9 @@ namespace Ryujinx.Ava.UI.Renderer
} }
else 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;
} }
if (X11Window != null) if (X11Window != null)
@@ -164,7 +219,8 @@ namespace Ryujinx.Ava.UI.Renderer
RegisterClassEx(ref wndClassEx); 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); SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc);
@@ -7,6 +7,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@@ -25,6 +26,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
@@ -1833,6 +1835,31 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
public void ReloadRenderDocApi()
{
RenderDoc.ReloadApi(ignoreAlreadyLoaded: true);
OnPropertyChanged(nameof(ShowStartCaptureButton));
OnPropertyChanged(nameof(ShowEndCaptureButton));
OnPropertyChanged(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() public void ToggleFullscreen()
{ {
if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs)
@@ -2112,5 +2139,25 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
#endregion #endregion
#region Context Menu commands
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();
OnPropertyChanged(nameof(ShowStartCaptureButton));
OnPropertyChanged(nameof(ShowEndCaptureButton));
}
}
#endregion
} }
} }
@@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class ProfileSelectorDialogViewModel : BaseModel
{
[ObservableProperty] private UserId _selectedUserId;
[ObservableProperty] private ObservableCollection<BaseModel> _profiles = [];
}
}
+15 -1
View File
@@ -118,6 +118,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
public bool IsKosmicKrispAvailable => OperatingSystem.IsMacOSVersionAtLeast(15);
public bool GameDirectoryChanged public bool GameDirectoryChanged
{ {
get => _gameDirectoryChanged; get => _gameDirectoryChanged;
@@ -317,12 +319,22 @@ namespace Ryujinx.Ava.UI.ViewModels
get; get;
set set
{ {
field = value; field = IsMacOS ? 0 : value;
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(IsVulkanSelected)); OnPropertyChanged(nameof(IsVulkanSelected));
} }
} }
public int TranslationLayerIndex
{
get;
set
{
field = value;
OnPropertyChanged();
}
}
public int ScalingFilter public int ScalingFilter
{ {
get => _scalingFilter; get => _scalingFilter;
@@ -626,6 +638,7 @@ namespace Ryujinx.Ava.UI.ViewModels
// Graphics // Graphics
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
TranslationLayerIndex = (int)config.Graphics.TranslationLayer.Value;
// Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus().
EnableShaderCache = config.Graphics.EnableShaderCache; EnableShaderCache = config.Graphics.EnableShaderCache;
EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
@@ -746,6 +759,7 @@ namespace Ryujinx.Ava.UI.ViewModels
// Graphics // Graphics
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.Graphics.TranslationLayer.Value = (TranslationLayer)TranslationLayerIndex;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
config.Graphics.EnableShaderCache.Value = EnableShaderCache; config.Graphics.EnableShaderCache.Value = EnableShaderCache;
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
@@ -227,6 +227,26 @@
Click="OpenCheatManagerForCurrentApp" Click="OpenCheatManagerForCurrentApp"
Header="{locale:Locale GameListContextMenuManageCheat}" Header="{locale:Locale GameListContextMenuManageCheat}"
IsEnabled="{Binding IsGameRunning}" /> IsEnabled="{Binding IsGameRunning}" />
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
<MenuItem
Click="StartRenderDocCapture_Click"
IsVisible="{Binding ShowStartCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_StartCapture}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Click="EndRenderDocCapture_Click"
IsVisible="{Binding ShowEndCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_EndCapture}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Click="DiscardRenderDocCapture_Click"
IsVisible="{Binding ShowEndCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_DiscardCapture}"
ToolTip.Tip="{locale:Locale MenuBarActions_DiscardCapture_ToolTip}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem> </MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}"> <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
<MenuItem Header="{locale:Locale MenuBarToolsInstallKeys}" IsEnabled="{Binding EnableNonGameRunningControls}"> <MenuItem Header="{locale:Locale MenuBarToolsInstallKeys}" IsEnabled="{Binding EnableNonGameRunningControls}">
@@ -9,7 +9,9 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption; using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.UI.App.Common; using Ryujinx.UI.App.Common;
@@ -266,6 +268,52 @@ namespace Ryujinx.Ava.UI.Views.Main
} }
} }
public void StartRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (!RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost
.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device))
{
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void EndRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void DiscardRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public async void CheckForUpdates(object sender, RoutedEventArgs e) public async void CheckForUpdates(object sender, RoutedEventArgs e)
{ {
if (Updater.CanUpdate(true)) if (Updater.CanUpdate(true))
@@ -28,7 +28,7 @@
Spacing="10"> Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10"> <StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal" IsVisible="{Binding !IsMacOS}">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackend}" Text="{locale:Locale SettingsTabGraphicsBackend}"
@@ -45,6 +45,22 @@
Content="OpenGL" /> Content="OpenGL" />
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsMacOS}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabTranslationLayerTooltip}"
Text="{locale:Locale SettingsTabTranslationLayer}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabTranslationLayerTooltip}"
SelectedIndex="{Binding TranslationLayerIndex}">
<ComboBoxItem
Content="MoltenVK" />
<ComboBoxItem
IsEnabled="{Binding IsKosmicKrispAvailable}"
Content="KosmicKrisp" />
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}"> <StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
@@ -56,6 +72,23 @@
SelectedIndex="{Binding PreferredGpuIndex}" SelectedIndex="{Binding PreferredGpuIndex}"
ItemsSource="{Binding AvailableGpus}"/> ItemsSource="{Binding AvailableGpus}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem
Content="{locale:Locale CommonAuto}" />
<ComboBoxItem
Content="{locale:Locale CommonOff}" />
<ComboBoxItem
Content="{locale:Locale CommonOn}" />
</ComboBox>
</StackPanel>
</StackPanel> </StackPanel>
<Separator Height="1" /> <Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
+2
View File
@@ -43,6 +43,8 @@
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" /> <KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+," Command="{Binding OpenSettings}" /> <KeyBinding Gesture="Ctrl+," Command="{Binding OpenSettings}" />
<KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" /> <KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
</Window.KeyBindings> </Window.KeyBindings>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>